Skip to content

BUGFIX: template-no-redundant-role — case-insensitive match + <select>→combobox#12

Closed
johanrd wants to merge 3 commits intomasterfrom
fix/no-redundant-role-case-and-select
Closed

BUGFIX: template-no-redundant-role — case-insensitive match + <select>→combobox#12
johanrd wants to merge 3 commits intomasterfrom
fix/no-redundant-role-case-and-select

Conversation

@johanrd
Copy link
Copy Markdown
Owner

@johanrd johanrd commented Apr 21, 2026

Two fixes in one PR (shared rule, no semantic overlap).

1. Case-insensitive role comparison

  • Premise: ARIA role tokens compare ASCII-case-insensitivelyrole="DOCUMENT" is the same token as role="document".
  • Problem: The rule compared the raw attribute value against a lowercase-keyed table, so <body role="DOCUMENT"> slipped through.

Fix: lowercase the role value before the ROLE_TO_ELEMENTS / LANDMARK_ROLES lookups.

2. <select> maps to combobox (conditional)

  • Premise: Per HTML-AAM (main conformance table, section 4), <select> has an implicit role of combobox only when neither multiple nor size > 1 is set; otherwise the implicit role is listbox. Both mappings exist in the spec.
  • Problem: ROLE_TO_ELEMENTS only listed listbox: ['select']. <select role="combobox"> was silently accepted.

Fix: add combobox: ['select'], and gate the combobox redundancy check on the multiple / size attributes so <select role="combobox" multiple> and <select role="combobox" size="5"> are not falsely flagged (their implicit role is listbox, not combobox, so the explicit combobox disagrees with the implicit role rather than duplicating it — this rule only flags redundancy). The implicit-role check mirrors jsx-a11y's src/util/implicitRoles/select.js.

Tests cover both fixes, including the uppercase + conditional-combobox interaction.

Prior art

Plugin Rule Notes
jsx-a11y no-redundant-roles Case-insensitive in effect, but the lowercasing happens in the getExplicitRole utility, not the rule itself. Implicit roles come from aria-query via getImplicitRole; the <select> branch lives in src/util/implicitRoles/select.js and honors multiple/size.
vuejs-accessibility no-redundant-roles Exact-match comparison (no lowercasing in the rule or its helpers); relies on aria-query's elementRoles for the implicit-role set. So <body role="DOCUMENT"> would not be flagged there either.
lit-a11y no-redundant-role Same getExplicitRole-style pattern as jsx-a11y (case-insensitive in effect via its own getExplicitRole util).

Upstream [email protected] has the same two gaps.

…x mapping

Two changes.

1. Compare role tokens case-insensitively. ARIA role values are
   ASCII-case-insensitive (HTML-AAM inherits HTML's attribute-comparison
   semantics). Before: <body role="DOCUMENT"> was not flagged because
   "DOCUMENT" didn't match "document" in ROLE_TO_ELEMENTS.

2. Add 'combobox' → ['select'] mapping. <select> without `multiple` or
   `size > 1` has an implicit role of "combobox" per HTML-AAM §4.1.
   Before: <select role="combobox"> was silently accepted as a redundant
   role.

Two new invalid tests cover the fixes.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
js small 13.69 ms 13.46 ms -1.6%
🟢 js medium 6.80 ms 6.57 ms -3.4%
js large 2.50 ms 2.54 ms +1.5%
gjs small 1.11 ms 1.12 ms +0.4%
gjs medium 557.83 µs 552.30 µs -1.0%
gjs large 219.57 µs 219.66 µs +0.0%
gts small 1.10 ms 1.11 ms +0.4%
gts medium 554.82 µs 552.78 µs -0.4%
gts large 220.93 µs 219.39 µs -0.7%

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

Full mitata output
clk: ~2.14 GHz
cpu: AMD EPYC 9V74 80-Core Processor
runtime: node 24.14.1 (x64-linux)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
js small (control)            16.69 ms/iter  18.92 ms █                    
                      (11.46 ms … 31.11 ms)  28.79 ms █ ▆                  
                    (  5.70 mb …  10.29 mb)   7.29 mb ███▆█▆▄▆▁▄▄▁▁▁▁█▁▁▁█▄

js small (experiment)         14.14 ms/iter  15.54 ms    █ ▂               
                      (11.94 ms … 20.36 ms)  17.88 ms ▂  █ █▂      ▂       
                    (  6.22 mb …   8.35 mb)   6.83 mb █▇▇█▇██▇▄▄▄▁▇█▄▇▁▁▄▁▇

                             ┌                                            ┐
                             ╷┌────────────┬────┐                         ╷
          js small (control) ├┤            │    ├─────────────────────────┤
                             ╵└────────────┴────┘                         ╵
                              ╷ ┌───┬───┐     ╷
       js small (experiment)  ├─┤   │   ├─────┤
                              ╵ └───┴───┘     ╵
                             └                                            ┘
                             11.46 ms           20.12 ms           28.79 ms

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

------------------------------------------- -------------------------------
js medium (control)            7.67 ms/iter   8.23 ms  █                   
                       (6.20 ms … 14.25 ms)  13.50 ms  █▃                  
                    (  2.53 mb …   4.52 mb)   3.54 mb ███▅▃▅▅▅▂▁▃▂▁▁▃▂▂▁▂▁▂

js medium (experiment)         7.26 ms/iter   7.90 ms  █                   
                       (6.08 ms … 13.59 ms)  13.26 ms  █                   
                    (  3.32 mb …   3.96 mb)   3.53 mb ██▇▄▅▃▄▄▄▂▂▁▁▂▁▁▁▂▁▁▂

                             ┌                                            ┐
                              ╷ ┌──────┬──┐                               ╷
         js medium (control)  ├─┤      │  ├───────────────────────────────┤
                              ╵ └──────┴──┘                               ╵
                             ╷ ┌────┬───┐                                ╷
      js medium (experiment) ├─┤    │   ├────────────────────────────────┤
                             ╵ └────┴───┘                                ╵
                             └                                            ┘
                             6.08 ms            9.79 ms            13.50 ms

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

------------------------------------------- -------------------------------
js large (control)             2.82 ms/iter   2.61 ms  █                   
                       (2.21 ms … 11.56 ms)   7.81 ms ██                   
                    (363.84 kb …   3.12 mb)   1.43 mb ██▅▂▂▁▂▁▁▂▂▁▁▂▁▁▁▁▁▁▁

js large (experiment)          2.86 ms/iter   2.71 ms  █                   
                        (2.39 ms … 7.18 ms)   5.80 ms ▄█                   
                    (317.30 kb …   2.55 mb)   1.42 mb ██▄▂▂▂▂▂▁▂▂▂▁▁▁▁▁▁▂▁▂

                             ┌                                            ┐
                             ╷┌───┬                                       ╷
          js large (control) ├┤   │───────────────────────────────────────┤
                             ╵└───┴                                       ╵
                               ┌──┬                       ╷
       js large (experiment)   │  │───────────────────────┤
                               └──┴                       ╵
                             └                                            ┘
                             2.21 ms            5.01 ms             7.81 ms

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

------------------------------------------- -------------------------------
gjs small (control)            1.23 ms/iter   1.14 ms █                    
                        (1.08 ms … 7.21 ms)   6.02 ms █                    
                    (539.00 kb …   1.62 mb)   1.06 mb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.25 ms/iter   1.13 ms █                    
                        (1.09 ms … 7.08 ms)   5.80 ms █                    
                    (196.53 kb …   1.59 mb)   1.06 mb █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
         gjs small (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌─┬                                        ╷
      gjs small (experiment) │ │────────────────────────────────────────┤
                             └─┴                                        ╵
                             └                                            ┘
                             1.08 ms            3.55 ms             6.02 ms

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

------------------------------------------- -------------------------------
gjs medium (control)         613.07 µs/iter 564.10 µs █                    
                      (536.79 µs … 6.37 ms)   3.00 ms █                    
                    (283.86 kb …   1.17 mb) 542.70 kb █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      599.59 µs/iter 560.13 µs █▄                   
                      (532.95 µs … 5.94 ms)   1.33 ms ██                   
                    ( 82.49 kb …   1.04 mb) 540.67 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
        gjs medium (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌┬             ╷
     gjs medium (experiment) ││─────────────┤
                             └┴             ╵
                             └                                            ┘
                             532.95 µs           1.77 ms            3.00 ms

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

------------------------------------------- -------------------------------
gjs large (control)          243.78 µs/iter 226.28 µs  █▄                  
                      (211.87 µs … 5.53 ms) 301.33 µs  ██                  
                    (216.09 kb … 677.38 kb) 217.38 kb ▂██▅█▅▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs large (experiment)       242.33 µs/iter 225.62 µs   █                  
                      (212.47 µs … 6.08 ms) 281.45 µs  ██                  
                    (181.19 kb …   1.00 mb) 216.62 kb ▃███▄▇▅▂▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷  ┌────────────┬                            ╷
         gjs large (control) ├──┤            │────────────────────────────┤
                             ╵  └────────────┴                            ╵
                             ╷  ┌───────────┬                   ╷
      gjs large (experiment) ├──┤           │───────────────────┤
                             ╵  └───────────┴                   ╵
                             └                                            ┘
                             211.87 µs         256.60 µs          301.33 µs

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

------------------------------------------- -------------------------------
gts small (control)            1.20 ms/iter   1.11 ms █                    
                        (1.08 ms … 6.29 ms)   5.64 ms █                    
                    (222.04 kb …   1.57 mb)   1.06 mb █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.21 ms/iter   1.11 ms █                    
                        (1.08 ms … 6.32 ms)   5.55 ms █                    
                    (218.14 kb …   1.99 mb)   1.05 mb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
         gts small (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌┬                                          ╷
      gts small (experiment) ││──────────────────────────────────────────┤
                             └┴                                          ╵
                             └                                            ┘
                             1.08 ms            3.36 ms             5.64 ms

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

------------------------------------------- -------------------------------
gts medium (control)         603.22 µs/iter 564.18 µs █                    
                      (533.70 µs … 5.91 ms)   1.51 ms █▅                   
                    ( 84.66 kb …   1.29 mb) 540.54 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      596.61 µs/iter 558.99 µs  █                   
                      (534.34 µs … 5.99 ms)   1.15 ms ██                   
                    ( 74.48 kb …   1.08 mb) 540.05 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌─┬                                         ╷
        gts medium (control) ├┤ │─────────────────────────────────────────┤
                             ╵└─┴                                         ╵
                             ╷┌─┬                        ╷
     gts medium (experiment) ├┤ │────────────────────────┤
                             ╵└─┴                        ╵
                             └                                            ┘
                             533.70 µs           1.02 ms            1.51 ms

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

------------------------------------------- -------------------------------
gts large (control)          245.56 µs/iter 227.26 µs  █                   
                      (213.77 µs … 5.89 ms) 314.07 µs  █▃                  
                    ( 74.33 kb … 808.91 kb) 216.77 kb ▄██▆▆▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts large (experiment)       241.20 µs/iter 225.91 µs  █                   
                      (212.34 µs … 5.74 ms) 299.32 µs  █▇                  
                    ( 15.02 kb …   1.24 mb) 216.53 kb ▃██▅▇▄▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                              ╷ ┌───────────┬                             ╷
         gts large (control)  ├─┤           │─────────────────────────────┤
                              ╵ └───────────┴                             ╵
                             ╷ ┌──────────┬                        ╷
      gts large (experiment) ├─┤          │────────────────────────┤
                             ╵ └──────────┴                        ╵
                             └                                            ┘
                             212.34 µs         263.20 µs          314.07 µs

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

johanrd and others added 2 commits April 21, 2026 10:51
Per HTML-AAM, <select>'s implicit role is "combobox" only when neither
`multiple` nor `size > 1` is present; otherwise it is "listbox". The
previous commit added `combobox: ['select']` unconditionally, which
caused false positives for <select role="combobox" multiple> and
<select role="combobox" size="5"> (where combobox disagrees with the
implicit listbox role and therefore is not redundant).

Add a selectHasComboboxImplicitRole helper mirroring jsx-a11y's
src/util/implicitRoles/select.js, and short-circuit the redundancy
check for <select role="combobox"> when the implicit role is actually
listbox.

Update the code comment on the `combobox: ['select']` entry to reflect
this (and drop the inaccurate "§4.1" reference — the <select> mapping
lives in the HTML-AAM main conformance table, section 4, without a
numbered subsection).

Tests:
- valid: <select role="combobox" multiple></select>
- valid: <select role="combobox" size="5"></select>
- invalid: <select role="combobox" size="1"></select>  (size=1 → combobox)
- invalid: <select role="COMBOBOX"></select>           (case + implicit)
…ases

Translates 35 cases from peer-plugin rules:
  - jsx-a11y no-redundant-roles
  - vuejs-accessibility no-redundant-roles
  - lit-a11y no-redundant-role

Fixture documents parity after this fix:
  - Case-insensitive role comparison (body role="DOCUMENT" now flagged).
  - Default <select role="combobox"> flagged; <select multiple>/size>1 still
    valid (implicit listbox, not combobox).

Remaining divergences (ul/ol role="list", <a role="link"> without href)
are annotated inline. Not wired into the default vitest run.
@johanrd
Copy link
Copy Markdown
Owner Author

johanrd commented Apr 21, 2026

Moved upstream to ember-cli#2727. 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