Skip to content

BUGFIX: template-require-mandatory-role-attributes — accept <input type=checkbox> + role=switch#7

Closed
johanrd wants to merge 3 commits intomasterfrom
fix/role-required-aria-checkbox-switch
Closed

BUGFIX: template-require-mandatory-role-attributes — accept <input type=checkbox> + role=switch#7
johanrd wants to merge 3 commits intomasterfrom
fix/role-required-aria-checkbox-switch

Conversation

@johanrd
Copy link
Copy Markdown
Owner

@johanrd johanrd commented Apr 21, 2026

  • Premise 1: The WAI-ARIA APG Switch pattern explicitly documents <input type="checkbox" role="switch"> as an accessible switch — the native <input>'s checked state exposes aria-checked to assistive technology.
  • Premise 2: Our rule required an explicit aria-checked attribute on every element with a role that needs it.
  • Conclusion: The rule flags <input type="checkbox" role="switch"> as missing aria-checked, a documented false positive.

Fix

Treat aria-checked as satisfied when the host element is an <input> whose {type, role} pair appears in the following explicit whitelist:

<input type> role Source
checkbox checkbox redundant but valid; axobject-query elementAXObjects
checkbox switch WAI-ARIA APG Switch pattern
checkbox menuitemcheckbox axobject-query elementAXObjects
radio radio redundant but valid; axobject-query elementAXObjects
radio menuitemradio axobject-query elementAXObjects

Exempted pairings match axobject-query's elementAXObjects for the input-role combinations and the WAI-ARIA APG Switch pattern. Undocumented pairings (e.g. input[type=checkbox] role=radio, input[type=radio] role=switch) are NOT exempted and remain flagged for missing aria-checked.

HTML type keyword values are ASCII case-insensitive per the HTML spec, so the comparison lowercases the attribute value before matching.

Our fix covers the common cases inline without adding axobject-query as a dependency. Switching to axobject-query (used by jsx-a11y / angular-eslint) would be a worthwhile follow-up, generalising the exemption to every documented semantic-role pairing.

Prior art

Plugin Rule Mechanism
jsx-a11y role-has-required-aria-props Calls isSemanticRoleElement(type, attributes), which uses axobject-query to recognise element+role pairings where the native element already exposes the required state.
@angular-eslint/template role-has-required-aria Delegates to a similar helper using axobject-query.
vuejs-accessibility role-has-required-aria-props Hardcoded filterRequiredPropsExceptions that matches only {role: switch, type: checkbox} — no axobject-query involvement.
lit-a11y role-has-required-aria-attrs No semantic-native exemption at all.

Upstream [email protected] has the same false positive.

…a-checked from <input type=checkbox|radio>

The rule required an explicit `aria-checked` on every element with a
role that needs it — even when the host element is a semantic form
control that contributes the state natively.

WAI-ARIA APG documents <input type="checkbox" role="switch"> as an
accessible switch pattern (see https://www.w3.org/WAI/ARIA/apg/patterns/switch/);
the <input>'s native checked state maps to aria-checked.

Fix: when the host is <input type="checkbox"> or <input type="radio">
and the role is one of {checkbox, menuitemcheckbox, menuitemradio,
radio, switch}, treat aria-checked as satisfied.

Matches eslint-plugin-jsx-a11y's role-has-required-aria-props, which
uses axobject-query's isSemanticRoleElement to skip the check for
element+role combinations where the native element already exposes the
required state. Our implementation covers the common cases without
pulling in the axobject-query dependency.

Five new test cases added covering the documented pattern. Rule doc
updated with an explicit example and an APG link.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
🟠 js small 13.48 ms 14.13 ms +4.8%
🟠 js medium 7.03 ms 7.19 ms +2.3%
🟢 js large 2.81 ms 2.75 ms -2.3%
gjs small 1.22 ms 1.21 ms -0.8%
gjs medium 612.36 µs 610.47 µs -0.3%
gjs large 242.92 µs 240.56 µs -1.0%
gts small 1.21 ms 1.22 ms +0.6%
gts medium 609.24 µs 607.60 µs -0.3%
gts large 241.74 µs 240.16 µs -0.7%

🟢 faster · 🔴 slower · 🟠 slightly slower · ⚪ within 2%

Full mitata output
clk: ~3.11 GHz
cpu: AMD EPYC 7763 64-Core Processor
runtime: node 24.14.1 (x64-linux)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
js small (control)            16.54 ms/iter  17.44 ms █                    
                      (12.08 ms … 31.10 ms)  27.07 ms █▂▇    ▂             
                    (  6.33 mb …  10.13 mb)   7.23 mb ███▃▆▃▁█▆▁▁▁▁▃▃▁▁▁▃▆▆

js small (experiment)         14.63 ms/iter  15.30 ms  █ ▅                 
                      (12.74 ms … 19.61 ms)  18.71 ms  █ █▃▃▆ ▃▆           
                    (  6.38 mb …   8.00 mb)   6.85 mb ███████▁███▁▄▄▁▁▁▁▄▁█

                             ┌                                            ┐
                             ╷┌───────────┬──┐                            ╷
          js small (control) ├┤           │  ├────────────────────────────┤
                             ╵└───────────┴──┘                            ╵
                               ╷ ┌───┬─┐         ╷
       js small (experiment)   ├─┤   │ ├─────────┤
                               ╵ └───┴─┘         ╵
                             └                                            ┘
                             12.08 ms           19.57 ms           27.07 ms

summary
  js small (experiment)
   1.13x faster than js small (control)

------------------------------------------- -------------------------------
js medium (control)            7.69 ms/iter   7.61 ms  █                   
                       (6.66 ms … 14.18 ms)  13.71 ms ██                   
                    (  2.61 mb …   4.66 mb)   3.53 mb ██▅▅▅▃▂▂▂▁▁▂▁▂▁▁▂▂▁▂▂

js medium (experiment)         7.87 ms/iter   8.35 ms  █                   
                       (6.47 ms … 14.16 ms)  14.09 ms ██▂                  
                    (  2.00 mb …   5.00 mb)   3.55 mb ████▄▆▅▅▅▂▁▁▁▁▁▁▃▃▁▁▂

                             ┌                                            ┐
                              ╷┌────┬                                   ╷
         js medium (control)  ├┤    │───────────────────────────────────┤
                              ╵└────┴                                   ╵
                             ╷┌──────┬──┐                                 ╷
      js medium (experiment) ├┤      │  ├─────────────────────────────────┤
                             ╵└──────┴──┘                                 ╵
                             └                                            ┘
                             6.47 ms           10.28 ms            14.09 ms

summary
  js medium (control)
   1.02x faster than js medium (experiment)

------------------------------------------- -------------------------------
js large (control)             3.28 ms/iter   3.15 ms  █                   
                       (2.44 ms … 10.57 ms)   7.44 ms  █▅                  
                    (600.02 kb …   3.13 mb)   1.45 mb ▅██▃▃▃▂▂▂▁▁▂▂▂▂▁▁▁▁▁▂

js large (experiment)          2.94 ms/iter   2.85 ms  █                   
                        (2.55 ms … 5.90 ms)   5.77 ms ▂█                   
                    (227.61 kb …   2.79 mb)   1.43 mb ██▅▃▂▂▂▁▂▂▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷  ┌───┬                                     ╷
          js large (control) ├──┤   │─────────────────────────────────────┤
                             ╵  └───┴                                     ╵
                              ╷┌──┬                        ╷
       js large (experiment)  ├┤  │────────────────────────┤
                              ╵└──┴                        ╵
                             └                                            ┘
                             2.44 ms            4.94 ms             7.44 ms

summary
  js large (experiment)
   1.11x faster than js large (control)

------------------------------------------- -------------------------------
gjs small (control)            1.35 ms/iter   1.29 ms █                    
                        (1.18 ms … 6.36 ms)   5.13 ms █                    
                    (547.02 kb …   1.60 mb)   1.06 mb ██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.33 ms/iter   1.24 ms █                    
                        (1.18 ms … 6.05 ms)   5.07 ms █                    
                    (512.04 kb …   1.63 mb)   1.06 mb █▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                          ╷
         gjs small (control) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             ┌─┬                                         ╷
      gjs small (experiment) │ │─────────────────────────────────────────┤
                             └─┴                                         ╵
                             └                                            ┘
                             1.18 ms            3.15 ms             5.13 ms

summary
  gjs small (experiment)
   1.02x faster than gjs small (control)

------------------------------------------- -------------------------------
gjs medium (control)         660.84 µs/iter 625.78 µs █                    
                      (579.84 µs … 5.25 ms)   2.99 ms █                    
                    ( 44.95 kb …   1.68 mb) 541.81 kb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      655.14 µs/iter 623.21 µs ▅█                   
                      (581.94 µs … 5.47 ms)   1.67 ms ██                   
                    (294.30 kb …   1.41 mb) 541.86 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                          ╷
        gjs medium (control) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             ┌┬                  ╷
     gjs medium (experiment) ││──────────────────┤
                             └┴                  ╵
                             └                                            ┘
                             579.84 µs           1.78 ms            2.99 ms

summary
  gjs medium (experiment)
   1.01x faster than gjs medium (control)

------------------------------------------- -------------------------------
gjs large (control)          275.76 µs/iter 258.87 µs ██                   
                      (232.15 µs … 4.74 ms) 493.85 µs ██▃                  
                    ( 73.27 kb … 681.25 kb) 217.17 kb ███▇▂▁▁▁▁▁▁▁▁▁▁▁▁▂▁▂▁

gjs large (experiment)       262.33 µs/iter 255.60 µs  █                   
                      (230.44 µs … 4.67 ms) 321.42 µs  █▅▂                 
                    (215.70 kb … 776.58 kb) 216.53 kb ▄███▃▃█▅▄▂▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌──────┬                                    ╷
         gjs large (control) ├┤      │────────────────────────────────────┤
                             ╵└──────┴                                    ╵
                             ╷┌───┬          ╷
      gjs large (experiment) ├┤   │──────────┤
                             ╵└───┴          ╵
                             └                                            ┘
                             230.44 µs         362.15 µs          493.85 µs

summary
  gjs large (experiment)
   1.05x faster than gjs large (control)

------------------------------------------- -------------------------------
gts small (control)            1.29 ms/iter   1.23 ms █                    
                        (1.18 ms … 6.13 ms)   5.19 ms █                    
                    (436.71 kb …   1.69 mb)   1.06 mb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.31 ms/iter   1.24 ms █                    
                        (1.18 ms … 5.68 ms)   5.04 ms █                    
                    (350.34 kb …   1.77 mb)   1.05 mb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
         gts small (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌┬                                         ╷
      gts small (experiment) ││─────────────────────────────────────────┤
                             └┴                                         ╵
                             └                                            ┘
                             1.18 ms            3.18 ms             5.19 ms

summary
  gts small (control)
   1.01x faster than gts small (experiment)

------------------------------------------- -------------------------------
gts medium (control)         650.86 µs/iter 624.38 µs  █                   
                      (580.98 µs … 5.20 ms)   1.28 ms  █                   
                    ( 37.28 kb …   1.20 mb) 541.29 kb ██▄▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      651.36 µs/iter 623.01 µs  █                   
                      (579.24 µs … 5.10 ms)   1.61 ms ██                   
                    (494.86 kb …   1.38 mb) 541.57 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌─┬                           ╷
        gts medium (control) ├┤ │───────────────────────────┤
                             ╵└─┴                           ╵
                             ╷┌─┬                                         ╷
     gts medium (experiment) ├┤ │─────────────────────────────────────────┤
                             ╵└─┴                                         ╵
                             └                                            ┘
                             579.24 µs           1.09 ms            1.61 ms

summary
  gts medium (control)
   1x faster than gts medium (experiment)

------------------------------------------- -------------------------------
gts large (control)          263.88 µs/iter 256.52 µs ▂█▂                  
                      (231.87 µs … 5.22 ms) 352.12 µs ███ ▂                
                    ( 14.23 kb … 770.16 kb) 216.79 kb ███▆█▆▆▃▂▂▁▁▁▁▁▁▁▁▁▁▁

gts large (experiment)       261.85 µs/iter 255.50 µs  █                   
                      (231.31 µs … 4.63 ms) 311.56 µs  █▆                  
                    ( 79.30 kb …   1.27 mb) 216.66 kb ▆███▄▂▆▆▅▄▂▂▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌─────────┬                                ╷
         gts large (control) ├─┤         │────────────────────────────────┤
                             ╵ └─────────┴                                ╵
                             ╷┌─────────┬                  ╷
      gts large (experiment) ├┤         │──────────────────┤
                             ╵└─────────┴                  ╵
                             └                                            ┘
                             231.31 µs         291.71 µs          352.12 µs

summary
  gts large (experiment)
   1.01x faster than gts large (control)

johanrd and others added 2 commits April 21, 2026 10:49
The previous exemption used a cartesian product of
`{checkbox, menuitemcheckbox, menuitemradio, radio, switch}` against
input types `{checkbox, radio}`, which silently accepted undocumented
pairings such as `input[type=checkbox] role=radio` or
`input[type=radio] role=switch`.

Replace that with an explicit whitelist of `{type}:{role}` pairs:

- `input[type=checkbox]` + `role=checkbox` (redundant but valid)
- `input[type=checkbox]` + `role=switch` (WAI-ARIA APG Switch pattern)
- `input[type=checkbox]` + `role=menuitemcheckbox`
- `input[type=radio]` + `role=radio` (redundant but valid)
- `input[type=radio]` + `role=menuitemradio`

The whitelist matches axobject-query's `elementAXObjects` for these
input-role combinations plus the WAI-ARIA APG Switch pattern
(https://www.w3.org/WAI/ARIA/apg/patterns/switch/). Other combinations
remain flagged for missing `aria-checked`.

Also lowercase the `type` attribute value before matching; HTML `type`
keywords are ASCII case-insensitive per the HTML spec, so values like
`Checkbox` must be treated the same as `checkbox`.
…er cases

Translates 41 cases from peer-plugin rules:
  - jsx-a11y role-has-required-aria-props
  - vuejs-accessibility role-has-required-aria-props
  - @angular-eslint/template role-has-required-aria
  - lit-a11y role-has-required-aria-attrs

The fixture documents where our rule matches peer behavior (e.g. the
input+role semantic whitelist now recognized after this fix) and where
it still diverges (space-separated role tokens, case-insensitivity,
unknown roles, undocumented input+role pairings).

Not wired into the default vitest run; provided for behavioral
parity auditing.
@johanrd
Copy link
Copy Markdown
Owner Author

johanrd commented Apr 21, 2026

Moved upstream to ember-cli#2725. See that PR.

@johanrd johanrd closed this Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant