Skip to content

feat: add template-no-interactive-element-to-noninteractive-role#20

Open
johanrd wants to merge 19 commits intomasterfrom
feat/template-no-interactive-to-noninteractive-role
Open

feat: add template-no-interactive-element-to-noninteractive-role#20
johanrd wants to merge 19 commits intomasterfrom
feat/template-no-interactive-to-noninteractive-role

Conversation

@johanrd
Copy link
Copy Markdown
Owner

@johanrd johanrd commented Apr 21, 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.

  • Premise 1: WAI-ARIA 1.2 §5.3 "Categorization of Roles" defines six role categories — Abstract, Widget, Document Structure, Landmark, Live Region, Window. The two this rule cares about are widgets (interactive) and document-structure (non-interactive). Using ARIA Rule 1 recommends using native HTML over ARIA overrides: "If you can use a native HTML element… with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so."
  • Premise 2: Assigning a non-interactive role (heading, article, presentation, …) to a native interactive element (<button>, <a href>, <input>, …) strips those semantics. <button role="heading"> becomes a visible, un-operable "button".
  • Conclusion: Such pairings should be flagged.

Fix: add template-no-interactive-element-to-noninteractive-role.

Interactive-element set mirrors jsx-a11y's layered derivation: aria-query's elementRoles (with attribute constraints — e.g. <a href>, <input type="…">, <select multiple>) is primary; jsx-a11y has an intermediate short-circuit on non-interactive elementRoles matches; we then consult axobject-query (AX-tree data) as a fallback for tags with no interactive elementRoles entry. Interactive-role set imported from the shared lib/utils/interactive-roles.js helper (see #27) — aria-query's widget-ancestor derivation + toolbar.

Two deviations from the unconstrained AX fallback, driven by common real-world patterns (pragmatic carve-outs; see diff for details):

  • <canvas> excluded — axobject-query marks canvas as widget (via CanvasRole); aria-query assigns no inherent role. Pragmatic carve-out because role="img" / role="presentation" on canvas is a legitimate pattern.
  • <audio> / <video> gated on controls — stricter than axobject-query (which marks bare <audio>/<video> as widget unconditionally); matches the HTML spec's controls semantics.

Neither carve-out is spec-cited; both are pragmatic.

Prior art

Plugin Rule Verified behavior
jsx-a11y no-interactive-element-to-noninteractive-role Flags interactive elements assigned non-interactive roles using aria-query + axobject-query layered derivation.
vuejs-accessibility No equivalent rule (verified: no matching file in src/rules/).
@angular-eslint/template No equivalent rule (verified: no matching file in packages/eslint-plugin-template/src/rules/).
lit-a11y No equivalent rule (verified: no matching file in lib/rules/).

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
🟢 js small 14.36 ms 13.23 ms -7.9%
🟢 js medium 6.68 ms 6.35 ms -5.0%
🟢 js large 2.62 ms 2.56 ms -2.4%
gjs small 1.10 ms 1.09 ms -0.5%
gjs medium 548.53 µs 548.58 µs +0.0%
gjs large 216.66 µs 217.14 µs +0.2%
gts small 1.09 ms 1.09 ms +0.1%
gts medium 548.80 µs 546.86 µs -0.4%
gts large 217.28 µs 215.37 µs -0.9%

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

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

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
js small (control)            16.20 ms/iter  17.69 ms █                    
                      (11.19 ms … 29.58 ms)  28.35 ms █▅▇ ▇                
                    (  6.10 mb …  10.69 mb)   7.25 mb ███▁█▇▄▇▄▇▄▄▁▁▁▄▁▁▇▄▄

js small (experiment)         13.74 ms/iter  14.84 ms    █ ▅               
                      (11.58 ms … 18.54 ms)  18.15 ms ▃█ █▃█▃   ▃   ▃      
                    (  6.21 mb …   8.34 mb)   6.84 mb ████████▄▁██▁▄█▄▁▁▁▁█

                             ┌                                            ┐
                             ╷ ┌──────────┬───┐                           ╷
          js small (control) ├─┤          │   ├───────────────────────────┤
                             ╵ └──────────┴───┘                           ╵
                              ╷ ┌───┬──┐       ╷
       js small (experiment)  ├─┤   │  ├───────┤
                              ╵ └───┴──┘       ╵
                             └                                            ┘
                             11.19 ms           19.77 ms           28.35 ms

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

------------------------------------------- -------------------------------
js medium (control)            7.29 ms/iter   7.60 ms  █▂                  
                       (6.07 ms … 14.33 ms)  13.92 ms ███                  
                    (  2.71 mb …   4.35 mb)   3.53 mb ███▅▅▄▅▅▂▃▂▂▂▁▁▁▂▁▁▁▂

js medium (experiment)         7.01 ms/iter   6.95 ms  █                   
                       (5.90 ms … 14.16 ms)  12.11 ms  █▂                  
                    (  2.60 mb …   4.40 mb)   3.52 mb ███▆▂▄▂▂▂▂▁▃▂▁▁▂▂▁▁▁▃

                             ┌                                            ┐
                              ╷ ┌────┬─┐                                  ╷
         js medium (control)  ├─┤    │ ├──────────────────────────────────┤
                              ╵ └────┴─┘                                  ╵
                             ╷ ┌───┬                            ╷
      js medium (experiment) ├─┤   │────────────────────────────┤
                             ╵ └───┴                            ╵
                             └                                            ┘
                             5.90 ms            9.91 ms            13.92 ms

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

------------------------------------------- -------------------------------
js large (control)             3.07 ms/iter   3.02 ms ▃█                   
                       (2.23 ms … 11.12 ms)   8.35 ms ██                   
                    (227.16 kb …   2.78 mb)   1.44 mb ███▄▅▃▁▂▁▂▂▂▂▁▂▁▁▁▁▁▁

js large (experiment)          2.84 ms/iter   2.65 ms  █                   
                        (2.36 ms … 8.88 ms)   5.59 ms ▆█                   
                    (793.65 kb …   3.08 mb)   1.43 mb ███▂▁▂▂▃▂▂▁▁▁▁▁▂▁▁▁▁▂

                             ┌                                            ┐
                             ╷┌────┬                                      ╷
          js large (control) ├┤    │──────────────────────────────────────┤
                             ╵└────┴                                      ╵
                              ╷┌─┬                    ╷
       js large (experiment)  ├┤ │────────────────────┤
                              ╵└─┴                    ╵
                             └                                            ┘
                             2.23 ms            5.29 ms             8.35 ms

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

------------------------------------------- -------------------------------
gjs small (control)            1.21 ms/iter   1.13 ms █                    
                        (1.07 ms … 5.58 ms)   5.36 ms █                    
                    (402.91 kb …   2.23 mb)   1.06 mb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.20 ms/iter   1.11 ms █                    
                        (1.08 ms … 5.75 ms)   5.09 ms █                    
                    (314.30 kb …   1.70 mb)   1.06 mb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
         gjs small (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌┬                                        ╷
      gjs small (experiment) ││────────────────────────────────────────┤
                             └┴                                        ╵
                             └                                            ┘
                             1.07 ms            3.22 ms             5.36 ms

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

------------------------------------------- -------------------------------
gjs medium (control)         597.00 µs/iter 555.82 µs █                    
                      (529.58 µs … 4.89 ms)   3.08 ms █                    
                    ( 72.52 kb …   1.08 mb) 541.68 kb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      594.36 µs/iter 554.68 µs █                    
                      (530.43 µs … 5.29 ms)   1.83 ms █                    
                    ( 86.65 kb …   1.29 mb) 541.34 kb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
        gjs medium (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌┬                     ╷
     gjs medium (experiment) ││─────────────────────┤
                             └┴                     ╵
                             └                                            ┘
                             529.58 µs           1.80 ms            3.08 ms

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

------------------------------------------- -------------------------------
gjs large (control)          236.33 µs/iter 223.88 µs  █                   
                      (210.25 µs … 4.61 ms) 292.92 µs  █▅                  
                    ( 81.17 kb … 953.66 kb) 217.10 kb ▆██▅▇▅▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs large (experiment)       238.48 µs/iter 224.43 µs  █                   
                      (211.58 µs … 5.18 ms) 300.32 µs  █                   
                    (215.70 kb … 965.81 kb) 216.76 kb ▇██▅▆▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌──────────┬                           ╷
         gjs large (control) ├─┤          │───────────────────────────┤
                             ╵ └──────────┴                           ╵
                              ╷┌───────────┬                              ╷
      gjs large (experiment)  ├┤           │──────────────────────────────┤
                              ╵└───────────┴                              ╵
                             └                                            ┘
                             210.25 µs         255.29 µs          300.32 µs

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

------------------------------------------- -------------------------------
gts small (control)            1.18 ms/iter   1.11 ms █                    
                        (1.07 ms … 5.78 ms)   5.32 ms █                    
                    (197.98 kb …   1.55 mb)   1.06 mb █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.18 ms/iter   1.10 ms █                    
                        (1.07 ms … 5.97 ms)   5.32 ms █                    
                    (212.04 kb …   1.53 mb)   1.05 mb █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
         gts small (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌┬                                           ╷
      gts small (experiment) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             └                                            ┘
                             1.07 ms            3.20 ms             5.32 ms

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

------------------------------------------- -------------------------------
gts medium (control)         593.63 µs/iter 556.12 µs █                    
                      (530.20 µs … 5.19 ms)   1.57 ms █                    
                    ( 87.23 kb …   1.23 mb) 541.87 kb ██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      594.42 µs/iter 553.23 µs █                    
                      (528.48 µs … 5.29 ms)   2.84 ms █                    
                    (  7.51 kb …   1.26 mb) 540.02 kb █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                  ╷
        gts medium (control) ││──────────────────┤
                             └┴                  ╵
                             ┌┬                                           ╷
     gts medium (experiment) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             └                                            ┘
                             528.48 µs           1.68 ms            2.84 ms

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

------------------------------------------- -------------------------------
gts large (control)          242.28 µs/iter 225.32 µs ▆█                   
                      (210.84 µs … 5.81 ms) 378.55 µs ██▃                  
                    ( 85.37 kb …   1.06 mb) 217.16 kb ███▅▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts large (experiment)       236.81 µs/iter 222.87 µs  █                   
                      (209.23 µs … 4.95 ms) 288.94 µs  █▃                  
                    (177.09 kb … 660.73 kb) 216.54 kb ▃██▅▇▄▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌───────┬                                   ╷
         gts large (control) ├┤       │───────────────────────────────────┤
                             ╵└───────┴                                   ╵
                             ╷┌─────┬             ╷
      gts large (experiment) ├┤     │─────────────┤
                             ╵└─────┴             ╵
                             └                                            ┘
                             209.23 µs         293.89 µs          378.55 µs

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

johanrd added a commit that referenced this pull request Apr 21, 2026
…ractive-role

Translates jsx-a11y's no-interactive-element-to-noninteractive-role test
suite (the only peer source — vuejs-accessibility and lit-a11y do not ship
this rule) into GTS + HBS RuleTester cases under tests/audit/. Pins cases
where we agree with jsx-a11y plus each deliberate divergence:

- Canvas is NOT treated as interactive (PR #20 carve-out) — matches
  jsx-a11y :recommended, diverges from :strict.
- <audio>/<video> without `controls` are non-interactive (PR #20 carve-out).
- <video controls role="presentation"> is FLAGGED by us, VALID in jsx-a11y.
- <tr role="presentation"> FLAGGED by us — matches jsx-a11y :strict,
  diverges from :recommended.
- <input type="hidden" role="img"> VALID for us (hidden guard), INVALID in
  jsx-a11y — captured in a dedicated sub-run.
- <embed>, <summary>, <td>, <th>, <datalist> with a non-interactive role
  FLAGGED by us via aria-query/axobject-query mappings; jsx-a11y treats
  these as static. Potential false positives worth revisiting.

184 tests, all passing. Not part of main CI — lives under tests/audit/.
@johanrd johanrd requested a review from Copilot April 22, 2026 10:28
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

Adds a new Ember template accessibility rule that flags native interactive HTML elements being assigned non-interactive ARIA roles (e.g. <button role="heading">), using a layered aria-query + axobject-query derivation similar to eslint-plugin-jsx-a11y.

Changes:

  • Introduces template-no-interactive-element-to-noninteractive-role rule with layered interactive-element detection plus pragmatic carve-outs (canvas excluded; audio/video require controls).
  • Adds shared utilities for interactive-role derivation, HTML interactive-content classification, and component-invocation detection.
  • Adds comprehensive unit tests plus an audit/parity fixture, and updates docs/README and dependencies (axobject-query).

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated no comments.

Show a summary per file
File Description
lib/rules/template-no-interactive-element-to-noninteractive-role.js New rule implementation using aria-query + axobject-query layered detection and role-fallback parsing.
lib/utils/interactive-roles.js Shared INTERACTIVE_ROLES derivation from aria-query’s taxonomy (+ toolbar).
lib/utils/is-component-invocation.js Helper to skip Glimmer component-invocation tag forms (PascalCase, @, this., dot-path, ::).
lib/utils/html-interactive-content.js Spec-based HTML “interactive content” classifier (+ ignoreUsemap option).
tests/lib/rules/template-no-interactive-element-to-noninteractive-role.js RuleTester coverage for both gts (<template>) and hbs parser modes, including carve-outs.
tests/lib/utils/is-component-invocation-test.js Unit tests for component-invocation tag classification.
tests/lib/utils/html-interactive-content-test.js Unit tests pinning HTML interactive-content classification behavior and edge cases.
tests/audit/no-interactive-element-to-noninteractive-role/peer-parity.js Large audit fixture to track parity/divergences vs jsx-a11y behavior.
docs/rules/template-no-interactive-element-to-noninteractive-role.md New rule documentation with rationale, examples, and references.
README.md Adds the new rule to the auto-generated rules list table.
package.json Adds axobject-query dependency.
pnpm-lock.yaml Locks [email protected].
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

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 11 out of 12 changed files in this pull request and generated 5 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment thread lib/rules/template-no-interactive-element-to-noninteractive-role.js
Comment thread lib/rules/template-no-interactive-element-to-noninteractive-role.js
Comment thread lib/rules/template-no-interactive-element-to-noninteractive-role.js Outdated
Comment thread tests/audit/no-interactive-element-to-noninteractive-role/peer-parity.js Outdated
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 12 out of 13 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment thread lib/rules/template-no-interactive-element-to-noninteractive-role.js
Comment thread lib/utils/is-component-invocation.js
Comment thread tests/lib/utils/is-component-invocation-test.js
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 12 out of 13 changed files in this pull request and generated 4 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment thread lib/utils/is-native-element.js
Comment thread lib/utils/is-native-element.js
Comment thread lib/utils/interactive-roles.js
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.

@johanrd johanrd requested a review from Copilot April 24, 2026 18:15
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.

johanrd added a commit that referenced this pull request Apr 26, 2026
…ractive-role

Translates jsx-a11y's no-interactive-element-to-noninteractive-role test
suite (the only peer source — vuejs-accessibility and lit-a11y do not ship
this rule) into GTS + HBS RuleTester cases under tests/audit/. Pins cases
where we agree with jsx-a11y plus each deliberate divergence:

- Canvas is NOT treated as interactive (PR #20 carve-out) — matches
  jsx-a11y :recommended, diverges from :strict.
- <audio>/<video> without `controls` are non-interactive (PR #20 carve-out).
- <video controls role="presentation"> is FLAGGED by us, VALID in jsx-a11y.
- <tr role="presentation"> FLAGGED by us — matches jsx-a11y :strict,
  diverges from :recommended.
- <input type="hidden" role="img"> VALID for us (hidden guard), INVALID in
  jsx-a11y — captured in a dedicated sub-run.
- <embed>, <summary>, <td>, <th>, <datalist> with a non-interactive role
  FLAGGED by us via aria-query/axobject-query mappings; jsx-a11y treats
  these as static. Potential false positives worth revisiting.

184 tests, all passing. Not part of main CI — lives under tests/audit/.
@johanrd johanrd force-pushed the feat/template-no-interactive-to-noninteractive-role branch from 6b35b8c to c0cb08d Compare April 26, 2026 08:11
@johanrd johanrd requested a review from Copilot April 26, 2026 08:41
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.

Pull request overview

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


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

Comment thread lib/utils/is-component-invocation.js
Comment thread lib/rules/template-no-interactive-element-to-noninteractive-role.js Outdated
johanrd added a commit that referenced this pull request Apr 27, 2026
…ractive-role

Translates jsx-a11y's no-interactive-element-to-noninteractive-role test
suite (the only peer source — vuejs-accessibility and lit-a11y do not ship
this rule) into GTS + HBS RuleTester cases under tests/audit/. Pins cases
where we agree with jsx-a11y plus each deliberate divergence:

- Canvas is NOT treated as interactive (PR #20 carve-out) — matches
  jsx-a11y :recommended, diverges from :strict.
- <audio>/<video> without `controls` are non-interactive (PR #20 carve-out).
- <video controls role="presentation"> is FLAGGED by us, VALID in jsx-a11y.
- <tr role="presentation"> FLAGGED by us — matches jsx-a11y :strict,
  diverges from :recommended.
- <input type="hidden" role="img"> VALID for us (hidden guard), INVALID in
  jsx-a11y — captured in a dedicated sub-run.
- <embed>, <summary>, <td>, <th>, <datalist> with a non-interactive role
  FLAGGED by us via aria-query/axobject-query mappings; jsx-a11y treats
  these as static. Potential false positives worth revisiting.

184 tests, all passing. Not part of main CI — lives under tests/audit/.
@johanrd johanrd force-pushed the feat/template-no-interactive-to-noninteractive-role branch from 78266b8 to 03d35f7 Compare April 27, 2026 14:01
@johanrd johanrd force-pushed the feat/template-no-interactive-to-noninteractive-role branch from 03d35f7 to 3c36f55 Compare April 27, 2026 19:26
@johanrd johanrd requested a review from Copilot April 27, 2026 19:33
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 10 out of 10 changed files in this pull request and generated 2 comments.


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

Comment on lines +4 to +11
* Returns true if the Glimmer element node is a component invocation
* rather than a native HTML element. Excludes:
* - PascalCase tags (<Button>, <MyWidget>)
* - Named-arg invocations (<@heading>, <@tag.foo>)
* - This-path invocations (<this.myComponent>, <this.comp.sub>)
* - Dot-path invocations (<foo.bar>)
* - Named-block syntax (<foo::bar>)
*/
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

The doc comment says “Excludes:” but the listed cases are exactly the ones that return true (i.e., are treated as component invocations). Update wording to “Includes:” (or rephrase) so the documentation matches the implemented behavior.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,276 @@
const { roles, elementRoles } = require('aria-query');
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

This module is missing the 'use strict'; directive used across the other newly added CommonJS files in this PR. Adding it improves consistency and avoids subtle differences in strict-mode behavior across modules.

Copilot uses AI. Check for mistakes.
NullVoxPopuli and others added 16 commits April 27, 2026 21:47
…-require-input-type

feat: add template-require-input-type
Captures empirically-verified Glimmer rendering behavior for HTML
attributes with mustache values, so rule authors classifying
GlimmerBooleanLiteral / GlimmerStringLiteral / GlimmerConcatStatement
have a ground-truth reference instead of intuition.

Notable findings the doc pins down:
- attr={{"false"}} (bare string "false") renders as attr="false" — TRUTHY,
  not falsy as the literal suggests.
- attr="{{false}}" (concat) sets the IDL property to true regardless of
  the literal value inside, even when HTML serialization shows nothing.
  Verified against <video muted="{{false}}"> → videoEl.muted === true.
- Non-reflecting boolean attrs (muted, autoplay) and reflecting ones
  (disabled, hidden) diverge in HTML serialization but agree at the IDL
  property layer.

Includes a copy-pasteable reproduction template so future readers can
re-verify if Glimmer behavior changes.

Adds a pointer in README's "Creating a New Rule" section.
Replaces the prior tables (which mixed verified data with extrapolations
marked "(assumed)") with strictly-verified per-attribute tables. Every cell
populated from rendering and IDL inspection in ember-source 6.12.

Structure:
- One "Reference table" section, five per-attribute sub-tables
  (muted, disabled, aria-hidden, tabindex, autocomplete)
- One "To reproduce the reference table" section with the exact template
  and JS console snippet, inline (no separate fixture file)
- Cross-attribute observations summarizing the rules the data reveals

Findings the new tables make explicit:
- Bare-mustache falsy set is {{false}}/{{null}}/{{undefined}}/{{0}} for
  boolean-coerced attrs (boolean HTML, ARIA, numeric); {{""}} is kept as
  attr="".
- Bare-mustache string literals never coerce — attr={{"false"}} renders
  as attr="false".
- Concat-mustache for boolean HTML attrs sets the IDL property to true
  regardless of the literal value inside (verified for both reflecting
  and non-reflecting attrs).
- Concat-mustache for ARIA/string attrs renders the stringified value
  literally — no boolean coercion. aria-hidden="{{false}}" is visible.
- Plain string attrs (autocomplete) skip Glimmer's boolean coercion
  entirely; autocomplete={{false}} renders as autocomplete="false".

The video.muted snapshot reads IDL muted=false for static attribute forms
(m1-m4, m7-m8, m11) because the test runs before media load — the doc
explains how defaultMuted reflects to muted at load time, so the rule's
"At play time" column is the lint-truth column rule authors should use.
…les" guide

Adds a practical-implementation section between the reference table and
the reproduction template. It maps each AST shape (GlimmerTextNode /
GlimmerMustacheStatement with each path type / GlimmerConcatStatement)
to a verdict, citing the row IDs from the reference table so rule
authors can implement classification correctly without re-deriving the
model.

Includes:
- AST-shape verdict table — direct mapping rule authors can copy from
- Six common mistakes section, each tied to specific row IDs
- Pointer to the (forthcoming) lib/utils/glimmer-attr-presence.js
  utility that will encode the verdict table once and let rules consume
  the resolved kind + value rather than re-walking the AST

The audit of master rules and the open feature PRs found 18 REAL_BUG
findings (12 in PRs, 6 in master) — all classifiable into the bullet-1
through bullet-4 footguns this guide enumerates.
…ng model

Adds lib/utils/glimmer-attr-presence.js exporting:

- classifyAttribute(attrNode, options?) → { presence, value }
  Maps every AST shape (valueless / GlimmerTextNode / GlimmerMustacheStatement
  with each path type / GlimmerConcatStatement) to a (presence, value) pair
  per the verified model in docs/glimmer-attribute-behavior.md. Each branch
  cites the relevant doc row IDs (m1–m19, h1–h15, d1–d10, t1–t7, i1–i5).
- inferAttrKind(name) → 'boolean' | 'aria' | 'numeric' | 'plain-string'
  Used when classifyAttribute callers don't pass options.kind explicitly.
- BOOLEAN_HTML_ATTRS, NUMERIC_ATTRS — exported sets, useful for callers
  that want to extend the kind model.

Key empirical asymmetries this util encodes correctly (and that audit
findings show several rules currently misclassify):

- Bare {{false}} / {{null}} / {{undefined}} on falsy-coerced kinds
  (boolean / aria / numeric) → presence='absent' (Glimmer omits attribute).
  Same forms on plain-string → presence='present', value='false' / etc.
- Bare {{"false"}} (StringLiteral) is JS-truthy, never coerced — renders
  the literal value across all attribute kinds.
- aria-hidden={{true}} renders aria-hidden="" (h5, contested), not
  aria-hidden="true" — the util surfaces value='' here so callers
  comparing value === 'true' don't false-match.
- Concat is never falsy: any concat form is presence='present'; the
  resolved value comes from the existing getStaticAttrValue helper.

Tests: 35 unit tests covering every doc row + the kind-override option.

Updates docs/glimmer-attribute-behavior.md to reference the actual file
and replaces the "(forthcoming)" sketch with a working example.
…ation / html-void-elements

Correctness fixes from PR ember-cli#2769 review:

- Boolean concat now returns canonical `value: 'true'` instead of leaking
  the inner literal. Per doc rows m13-m19, d7-d10 the IDL is set true
  regardless of inner value, so callers checking `value === 'false'` to
  detect "off" no longer get a wrong answer for `<video muted="{{false}}">`.
- {{true}} on numeric / plain-string now returns `unknown` (untested in
  doc; was previously claiming `value: 'true'` by extrapolation).
- `inferAttrKind` is now case-insensitive (`Disabled`, `ARIA-Hidden`, etc.).

Drop hand-rolled spec lists in favor of upstream packages:

- `property-information` for boolean / overloadedBoolean / number attribute
  classification, replacing the 24-entry BOOLEAN_HTML_ATTRS and 3-entry
  NUMERIC_ATTRS Sets. `colspan` is added as a small NUMERIC_OVERRIDES Set
  to compensate for an upstream gap in property-information 7.1.0 (rowspan
  and cols are marked `number: true`, colspan isn't).
- `html-void-elements` in template-block-indentation.js and
  template-self-closing-void-elements.js, deduplicating two parallel
  16-entry VOID_TAGS Sets.

Internal API change: BOOLEAN_HTML_ATTRS and NUMERIC_ATTRS are no longer
exported from glimmer-attr-presence. The util's public surface is now
`classifyAttribute` and `inferAttrKind`. Callers wanting the underlying
classification can use property-information directly.
…oss-attr m6/d3 analog)

`controls` on `<audio>`/`<video>` is an HTML boolean attribute, so per the
cross-attribute observation in docs/glimmer-attribute-behavior.md, bare
`{{false}}` / `{{null}}` / `{{undefined}}` cause Glimmer to omit the
attribute at runtime. The helper's previous AST-presence check
(hasAttribute) treated `<video controls={{false}}>` as still having
controls — a false positive that propagated to every rule using
isHtmlInteractiveContent: nested-interactive, no-noninteractive-tabindex,
interactive-supports-focus, click-events-have-key-events, etc.

After:
- `controls` flows through classifyAttribute. Bare-mustache falsy literals
  now correctly classify as 'absent' → element is NOT interactive at
  runtime.
- `href` and `usemap` continue to use AST-presence — those are plain
  string attributes that don't falsy-coerce (i4 analog: bare `{{false}}`
  on a plain string renders as the literal `"false"`, attribute kept).

Concat forms (`controls="{{X}}"`) still classify as 'present' because
concat is never falsy at runtime — so the existing pass-through behavior
for concat is preserved.

Tests:
- New: `<video controls={{false}}>` → not interactive (regression-locking).
- New: `<audio controls={{null}}>` → not interactive.
- New: `<video controls="{{false}}">` → IS interactive (concat sets IDL
  true regardless of inner literal).
- All 9555 existing tests pass — no rule was relying on the buggy
  behavior, so this fix lands cleanly across all consumers.
@johanrd johanrd force-pushed the feat/template-no-interactive-to-noninteractive-role branch from 0b5fb88 to 0ecb3da Compare April 28, 2026 12:41
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.

3 participants