Skip to content

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

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

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

Conversation

@johanrd
Copy link
Copy Markdown
Owner

@johanrd johanrd commented Apr 22, 2026

Note

This is part of a series where Claude has audited eslint-plugin-ember against jsx-a11y, vuejs-accessibility, angular-eslint, lit-a11y and html-validate, ember-template-lint, and the HTML and WCAG specs.

[Mirror of ember-cli#2727 for Copilot review]

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

1. Case-insensitive role comparison

  • Premise: Authors are guided to lowercase ARIA role tokens, and modern browsers parse them ASCII case-insensitively. Per html-aria §4.4 — Case requirements for ARIA role, state and property attributes: authors "SHOULD use ASCII lowercase for all role token values…"; the section's note observes that "modern browsers treat the role or aria- attribute values as ASCII case-insensitive"*. The HTML enumerated-attribute machinery the role attribute relies on is also case-insensitive (HTML Living Standard — enumerated attributes).
  • 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)

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. 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

Verified each peer in source:

Plugin Rule Verified behavior
jsx-a11y no-redundant-roles Calls getExplicitRole (lowercases via role.toLowerCase() at src/util/getExplicitRole.js:18–23); src/util/implicitRoles/select.js:9–17 honors multiple and size > 1.
vuejs-accessibility no-redundant-roles Exact-match comparison at line 78 (implicitRoleSet.includes(explicitRole)); no lowercasing in the rule or its utils. <body role="DOCUMENT"> is not flagged there either.
@angular-eslint/template no equivalent rule angular-eslint ships valid-aria, role-has-required-aria, and related ARIA-validity rules, but no redundant-role check. Role-vs-implicit-role redundancy is not audited.
lit-a11y no-redundant-role Same getExplicitRole-style pattern as jsx-a11y (case-insensitive via its own lib/utils/getExplicitRole.js:11–12).

Audit fixture

Translated peer-plugin test fixture at tests/audit/no-redundant-roles/peer-parity.js. Runs as part of the default Vitest suite (picked up by the tests/**/*.js include glob) — serves double-duty as a peer-parity audit and as regression coverage pinning CURRENT behavior.

johanrd added 3 commits April 21, 2026 07:41
…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.
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 johanrd requested a review from Copilot April 22, 2026 10:41
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
js small 14.26 ms 14.25 ms -0.1%
🟢 js medium 7.30 ms 7.09 ms -2.8%
🔴 js large 2.92 ms 4.26 ms +45.6%
gjs small 2.13 ms 2.13 ms +0.2%
gjs medium 628.84 µs 627.12 µs -0.3%
gjs large 248.42 µs 247.23 µs -0.5%
gts small 1.26 ms 1.26 ms -0.3%
gts medium 629.68 µs 623.92 µs -0.9%
gts large 250.59 µs 246.80 µs -1.5%

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

Full mitata output
clk: ~3.09 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)            17.20 ms/iter  17.98 ms █ ▅                  
                      (12.51 ms … 32.55 ms)  27.53 ms █▂█ ▂▂             ▂ 
                    (  5.66 mb …  10.40 mb)   7.31 mb ███▁██▄▇▁▄▁▁▁▄▄▁▁▁▁█▇

js small (experiment)         14.89 ms/iter  15.64 ms   ▂█                 
                      (13.18 ms … 21.07 ms)  19.58 ms ▂▇██ ▅               
                    (  6.14 mb …   8.23 mb)   6.84 mb ████▇█▄▄▇▇▇▁▄▄▁▄▁▁▁▁▄

                             ┌                                            ┐
                             ╷ ┌───────────┬─┐                            ╷
          js small (control) ├─┤           │ ├────────────────────────────┤
                             ╵ └───────────┴─┘                            ╵
                               ╷ ┌──┬─┐           ╷
       js small (experiment)   ├─┤  │ ├───────────┤
                               ╵ └──┴─┘           ╵
                             └                                            ┘
                             12.51 ms           20.02 ms           27.53 ms

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

------------------------------------------- -------------------------------
js medium (control)            7.97 ms/iter   8.57 ms  █▂                  
                       (6.77 ms … 13.87 ms)  12.86 ms ▄██                  
                    (  2.41 mb …   4.67 mb)   3.54 mb ███▆▂▃█▅▁▁▃▄▁▁▁▃▁▁▁▂▃

js medium (experiment)         7.74 ms/iter   7.97 ms  █                   
                       (6.71 ms … 14.06 ms)  13.16 ms ▄█                   
                    (  2.44 mb …   4.56 mb)   3.52 mb ██▆▅▃▄▂▂▂▂▁▁▁▂▂▁▁▁▁▂▂

                             ┌                                            ┐
                             ╷ ┌──────┬───┐                             ╷
         js medium (control) ├─┤      │   ├─────────────────────────────┤
                             ╵ └──────┴───┘                             ╵
                             ╷┌─────┬─┐                                   ╷
      js medium (experiment) ├┤     │ ├───────────────────────────────────┤
                             ╵└─────┴─┘                                   ╵
                             └                                            ┘
                             6.71 ms            9.93 ms            13.16 ms

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

------------------------------------------- -------------------------------
js large (control)             3.42 ms/iter   3.45 ms  █                   
                       (2.60 ms … 10.94 ms)   6.86 ms  █▄                  
                    (  3.19 kb …   3.39 mb)   1.45 mb ▃██▃▃▂▂▃▂▂▂▁▂▂▁▃▁▂▁▁▁

js large (experiment)          4.50 ms/iter   4.31 ms       █              
                        (2.88 ms … 8.57 ms)   7.61 ms       █              
                    (767.77 kb …   3.07 mb)   1.44 mb ▃▁▁▂▁▆█▂▃▂▁▁▁▂▁▂▂▁▁▂▁

                             ┌                                            ┐
                             ╷ ┌────┬┐                             ╷
          js large (control) ├─┤    │├─────────────────────────────┤
                             ╵ └────┴┘                             ╵
                               ╷           ┌──┬                           ╷
       js large (experiment)   ├───────────┤  │───────────────────────────┤
                               ╵           └──┴                           ╵
                             └                                            ┘
                             2.60 ms            5.11 ms             7.61 ms

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

------------------------------------------- -------------------------------
gjs small (control)            2.08 ms/iter   2.16 ms     █                
                        (1.38 ms … 9.14 ms)   5.36 ms ▂   █                
                    (636.74 kb …   1.88 mb)   1.06 mb █▃▁▁█▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         2.35 ms/iter   2.18 ms   █                  
                        (1.48 ms … 9.26 ms)   7.85 ms   █                  
                    (114.45 kb …   2.02 mb)   1.06 mb ▁▁█▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌───┬                      ╷
         gjs small (control) ├┤   │──────────────────────┤
                             ╵└───┴                      ╵
                              ╷   ┌─┬                                     ╷
      gjs small (experiment)  ├───┤ │─────────────────────────────────────┤
                              ╵   └─┴                                     ╵
                             └                                            ┘
                             1.38 ms            4.61 ms             7.85 ms

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

------------------------------------------- -------------------------------
gjs medium (control)         688.21 µs/iter 651.05 µs █                    
                      (589.75 µs … 6.06 ms)   3.61 ms █                    
                    ( 90.69 kb …   1.12 mb) 540.99 kb █▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      666.86 µs/iter 644.28 µs  █                   
                      (589.36 µs … 5.19 ms)   1.27 ms  █▄                  
                    (292.74 kb …   1.20 mb) 541.61 kb ███▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
        gjs medium (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌┬        ╷
     gjs medium (experiment) ││────────┤
                             └┴        ╵
                             └                                            ┘
                             589.36 µs           2.10 ms            3.61 ms

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

------------------------------------------- -------------------------------
gjs large (control)          274.25 µs/iter 266.09 µs  █                   
                      (235.91 µs … 4.87 ms) 365.23 µs  ██                  
                    (138.33 kb … 836.38 kb) 217.42 kb ▅██▆▅▇▆▃▃▁▂▁▁▁▁▁▁▁▁▁▁

gjs large (experiment)       267.46 µs/iter 264.71 µs  █▂                  
                      (236.58 µs … 4.51 ms) 322.18 µs  ██▄                 
                    (170.52 kb … 864.90 kb) 216.78 kb ▄███▅▃▅▇▆▅▃▂▂▂▂▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌──────────┬                               ╷
         gjs large (control) ├─┤          │───────────────────────────────┤
                             ╵ └──────────┴                               ╵
                             ╷ ┌────────┬                  ╷
      gjs large (experiment) ├─┤        │──────────────────┤
                             ╵ └────────┴                  ╵
                             └                                            ┘
                             235.91 µs         300.57 µs          365.23 µs

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

------------------------------------------- -------------------------------
gts small (control)            1.35 ms/iter   1.28 ms █                    
                        (1.22 ms … 6.28 ms)   4.63 ms █                    
                    (198.73 kb …   1.81 mb)   1.06 mb █▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.35 ms/iter   1.29 ms █                    
                        (1.21 ms … 6.34 ms)   5.06 ms █                    
                    (511.08 kb …   1.63 mb)   1.05 mb █▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                     ╷
         gts small (control) │ │─────────────────────────────────────┤
                             └─┴                                     ╵
                             ┌─┬                                          ╷
      gts small (experiment) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             └                                            ┘
                             1.21 ms            3.14 ms             5.06 ms

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

------------------------------------------- -------------------------------
gts medium (control)         673.15 µs/iter 646.49 µs  █                   
                      (591.17 µs … 5.56 ms)   1.36 ms  █                   
                    (293.37 kb …   1.04 mb) 542.41 kb ███▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      667.82 µs/iter 642.24 µs  █                   
                      (586.73 µs … 5.21 ms)   1.32 ms  █                   
                    (124.38 kb …   1.25 mb) 540.50 kb ███▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌──┬                                       ╷
        gts medium (control) ├─┤  │───────────────────────────────────────┤
                             ╵ └──┴                                       ╵
                             ╷ ┌──┬                                     ╷
     gts medium (experiment) ├─┤  │─────────────────────────────────────┤
                             ╵ └──┴                                     ╵
                             └                                            ┘
                             586.73 µs          972.58 µs           1.36 ms

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

------------------------------------------- -------------------------------
gts large (control)          275.24 µs/iter 268.21 µs  █▄                  
                      (236.85 µs … 5.04 ms) 362.63 µs  ██                  
                    ( 38.97 kb … 738.66 kb) 216.97 kb ▃███▄█▇▅▃▃▂▁▁▁▁▁▁▁▁▁▁

gts large (experiment)       270.56 µs/iter 264.24 µs  █                   
                      (234.16 µs … 4.79 ms) 369.09 µs  █▇                  
                    (143.36 kb … 965.80 kb) 216.62 kb ▄██▆▅█▅▃▂▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                              ╷ ┌──────────┬                            ╷
         gts large (control)  ├─┤          │────────────────────────────┤
                              ╵ └──────────┴                            ╵
                             ╷ ┌─────────┬                                ╷
      gts large (experiment) ├─┤         │────────────────────────────────┤
                             ╵ └─────────┴                                ╵
                             └                                            ┘
                             234.16 µs         301.62 µs          369.09 µs

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the template-no-redundant-role rule to better match HTML/ARIA behavior by normalizing role matching case-insensitively and by treating <select>’s implicit role as combobox only in the single-select configuration.

Changes:

  • Lowercase static role="..." values before LANDMARK_ROLES / ROLE_TO_ELEMENTS lookups to enforce ASCII case-insensitive matching.
  • Add combobox: ['select'] to ROLE_TO_ELEMENTS and gate <select role="combobox"> redundancy detection based on multiple / size > 1.
  • Extend rule tests and add a peer-parity audit fixture.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
lib/rules/template-no-redundant-role.js Implements case-insensitive role lookup and conditional <select> implicit-role handling.
tests/lib/rules/template-no-redundant-role.js Adds valid/invalid coverage for <select> combobox/listbox conditions and uppercase role values.
tests/audit/no-redundant-roles/peer-parity.js Adds a peer-parity audit fixture (currently placed under tests/**, so it will be picked up by Vitest).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-no-redundant-role.js
Comment thread lib/rules/template-no-redundant-role.js Outdated
Comment thread tests/audit/no-redundant-roles/peer-parity.js Outdated
johanrd added 2 commits April 22, 2026 14:03
- Normalize role via trim() + first-token split before lookup so values
  like 'COMBOBOX' or 'combobox listbox' are handled consistently
  (ARIA role attribute is a space-separated fallback list; only the
  first supported token is effective).
- getSelectImplicitRole() now returns 'combobox' | 'listbox' | 'unknown'.
  A dynamic <select size={{...}}> previously fell through as 'combobox'
  and produced false positives on role="combobox"; we now bail on
  'unknown' instead of flagging.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-no-redundant-role.js Outdated
Comment thread lib/rules/template-no-redundant-role.js Outdated
Comment thread tests/audit/no-redundant-roles/peer-parity.js Outdated
@johanrd johanrd force-pushed the fix/no-redundant-role-case-and-select branch from df79aaf to 00c823d Compare April 22, 2026 17:10
johanrd added 2 commits April 23, 2026 21:28
…d role per ARIA §4.1 (Copilot review)

Bundle with Q7/Q10/Q18/Q30 cross-rule pattern. Use aria-query's roles
set as the "recognised" oracle (not the local ROLE_TO_ELEMENTS table,
which would over-walk to roles it has mappings for).

`role="xxyxyz button"` on <button> → first recognised is button → flag
redundancy (autofix drops the whole role attr; implicit button role is
preserved natively). `role="tab button"` on <button> → first recognised
is tab → no implicit mapping for tab → nothing to flag.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-no-redundant-role.js Outdated
Comment thread lib/rules/template-no-redundant-role.js Outdated
Comment thread tests/lib/rules/template-no-redundant-role.js
johanrd added 2 commits April 24, 2026 13:38
…elect> implicit role (Copilot review)

Ember omits bound boolean attributes at runtime when the value is falsy, so
`<select multiple={{this.isMulti}}>` can resolve to either the combobox or
listbox implicit role. Previously treated any presence as listbox, which
could misclassify redundancy. Return 'unknown' for the dynamic case so the
caller skips flagging — matching the existing 'unknown' handling for dynamic
`size`.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-no-redundant-role.js
…ox (Copilot review)

Per HTML boolean-attr semantics, a missing-value attribute's value is the
empty string — Number('') is 0; 0 is not > 1, so the implicit role of a
<select size> (no value) stays combobox. Previously we bailed out to
'unknown' for any sizeAttr.value that wasn't a GlimmerTextNode, conflating
valueless with dynamic. Split the two: valueless → combobox (static),
dynamic (mustache / concat) → unknown (skip).

Add an invalid test: <select role='combobox' size> flags as redundant
combobox.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…dit fixture

Upstream maintainers don't want the per-PR `tests/audit/peer-parity`
pattern. Port two basic non-landmark redundancy cases that were only
in the audit fixture into the regular suite:
- `<button role="button">` (autofix drops role)
- `<img role="img">` (autofix drops role)

Other audit cases were already covered by the regular tests.
@johanrd johanrd closed this Apr 25, 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.

2 participants