Skip to content

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

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

feat: add template-no-noninteractive-element-to-interactive-role#21
johanrd wants to merge 2 commits intomasterfrom
feat/template-no-noninteractive-to-interactive-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 categorises roles into widgets (interactive) and document structure / landmarks (non-interactive). HTML-AAM maps native HTML elements onto those categories — <h1>, <article>, <ul>, <table>, <form>, <img>, <p>, etc., all map to non-interactive roles.
  • Premise 2: Putting an interactive role on a non-interactive native element (<h1 role="button">) creates a widget with no backing behavior — focus, keyboard activation, and state handling all have to be added manually, and the mismatch is a recurring source of a11y bugs.
  • Conclusion: Such pairings should be flagged.

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

The non-interactive-tag set is derived by unioning two sources — aria-query's elementRoles and axobject-query's elementAXObjects — matching jsx-a11y's isNonInteractiveElement. header is explicitly excluded because its role is ancestry-dependent, mirroring jsx-a11y's carve-out. Interactive-role set is imported from the shared lib/utils/interactive-roles.js helper (see #27).

Adds axobject-query as a direct dependency. <div> and <span> aren't covered — ARIA 1.2 treats them as generic with no inherent semantics to mismatch.

Follow-up fix (Phase 3 audit F4)

Commit 4cd92691 augments the original axobject-query-only derivation with an elementRoles pass. The prior derivation dropped every tag whose axobject-query schema had an attribute constraint, producing false negatives for 18 HTML-AAM-mapped non-interactive tags:

section, address, aside, code, del, em, fieldset, hr, html, ins, optgroup, output, strong, sub, sup, tbody, tfoot, thead

<section role="button">, <fieldset role="checkbox">, <hr role="button" />, etc. are now reported, matching jsx-a11y. Audit fixture updated: 23 divergence cases moved to parity-invalid.

Flags

<h1 role="button">Click</h1>                   // jsx-a11y: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/__tests__/src/rules/no-noninteractive-element-to-interactive-role-test.js#L377
<article role="button">Story</article>         // jsx-a11y: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/__tests__/src/rules/no-noninteractive-element-to-interactive-role-test.js#L358
<li role="tab">Tab</li>                        // jsx-a11y: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/__tests__/src/rules/no-noninteractive-element-to-interactive-role-test.js#L495
<img role="link" src="/x.png" alt="link" />    // jsx-a11y: same test file — img is in the neverValid set; role="link" vs role="button" is equivalent shape
<form role="button"></form>                    // jsx-a11y: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/__tests__/src/rules/no-noninteractive-element-to-interactive-role-test.js#L376
<p role="button">Click me</p>                  // jsx-a11y: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/__tests__/src/rules/no-noninteractive-element-to-interactive-role-test.js#L432
<section role="button" aria-label="x"></section>  // added in follow-up fix — jsx-a11y neverValid
<fieldset role="checkbox"></fieldset>             // added in follow-up fix — jsx-a11y neverValid

Allows

<h1 role="heading" aria-level="1">Title</h1>                         // non-interactive role on non-interactive element — out of scope
<article role="article">Story</article>                              // redundant but not flagged by this rule
<ul role="list"></ul>                                                // same
<div role="button" tabindex="0"></div>                               // jsx-a11y treats <div> as "generic" — absent from isNonInteractiveElement set
<span role="checkbox" aria-checked="false" tabindex="0"></span>      // same — <span> is generic
<header role="button"></header>                                      // header role depends on ancestry — excluded per jsx-a11y's carve-out

Prior art

Plugin Rule Verified behavior
jsx-a11y no-noninteractive-element-to-interactive-role Flags non-interactive elements assigned interactive roles, using aria-query elementRoles + axobject-query elementAXObjects unioned set.
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/).

Not added to template-lint-migration — opt-in.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
🟢 js small 14.50 ms 14.03 ms -3.2%
🟢 js medium 7.20 ms 6.97 ms -3.2%
🟢 js large 2.83 ms 2.74 ms -3.3%
gjs small 1.24 ms 1.22 ms -1.2%
gjs medium 618.99 µs 616.01 µs -0.5%
gjs large 244.54 µs 244.70 µs +0.1%
gts small 1.23 ms 1.24 ms +0.6%
gts medium 621.15 µs 613.29 µs -1.3%
gts large 245.52 µs 242.84 µs -1.1%

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

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

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
js small (control)            16.91 ms/iter  18.08 ms █                    
                      (12.51 ms … 32.27 ms)  29.39 ms █▆▃                  
                    (  5.60 mb …  10.14 mb)   7.31 mb ███▄██▄▄▆▁▁▁▁▄▆▁▄▁▄▁▄

js small (experiment)         14.55 ms/iter  15.18 ms  █ ▄▄                
                      (12.74 ms … 20.04 ms)  19.26 ms ██▅██  █▅            
                    (  6.14 mb …   8.21 mb)   6.84 mb █████████▅█▅▅▁▁▅▁▁▁▁▅

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

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

------------------------------------------- -------------------------------
js medium (control)            7.87 ms/iter   8.19 ms  █                   
                       (6.58 ms … 15.08 ms)  14.82 ms ██▆                  
                    (  3.19 mb …   4.55 mb)   3.57 mb ███▆▅▇▄▃▂▁▁▂▁▁▁▃▁▁▁▁▃

js medium (experiment)         7.51 ms/iter   7.61 ms  █                   
                       (6.55 ms … 13.69 ms)  13.06 ms ██                   
                    (  2.28 mb …   4.72 mb)   3.54 mb ████▃▅▃▄▂▁▂▂▂▁▁▁▁▁▁▂▂

                             ┌                                            ┐
                             ╷ ┌────┬─┐                                   ╷
         js medium (control) ├─┤    │ ├───────────────────────────────────┤
                             ╵ └────┴─┘                                   ╵
                             ╷┌───┬┐                            ╷
      js medium (experiment) ├┤   │├────────────────────────────┤
                             ╵└───┴┘                            ╵
                             └                                            ┘
                             6.55 ms           10.68 ms            14.82 ms

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

------------------------------------------- -------------------------------
js large (control)             3.28 ms/iter   3.11 ms  ▅█                  
                        (2.44 ms … 8.63 ms)   7.11 ms  ██                  
                    (193.39 kb …   2.68 mb)   1.43 mb ▅██▃▄▂▂▄▂▁▂▁▂▂▁▂▂▁▁▁▁

js large (experiment)          3.02 ms/iter   2.86 ms  █                   
                        (2.52 ms … 7.76 ms)   6.69 ms  █                   
                    (510.22 kb …   2.37 mb)   1.43 mb ▇█▄▃▂▂▂▂▁▂▁▁▁▁▁▁▁▁▁▁▁

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

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

------------------------------------------- -------------------------------
gjs small (control)            1.36 ms/iter   1.33 ms █                    
                        (1.19 ms … 5.55 ms)   4.63 ms █▂                   
                    (328.55 kb …   1.62 mb)   1.06 mb ██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.33 ms/iter   1.25 ms █                    
                        (1.18 ms … 6.33 ms)   4.92 ms █                    
                    (290.25 kb …   1.84 mb)   1.06 mb █▅▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

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

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

------------------------------------------- -------------------------------
gjs medium (control)         664.15 µs/iter 632.54 µs █                    
                      (585.73 µs … 5.08 ms)   3.03 ms █                    
                    ( 59.24 kb …   1.11 mb) 542.01 kb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      661.45 µs/iter 632.15 µs  █                   
                      (582.04 µs … 5.10 ms)   1.43 ms  █                   
                    (106.84 kb …   1.21 mb) 541.16 kb ██▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                          ╷
        gjs medium (control) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             ┌┬              ╷
     gjs medium (experiment) ││──────────────┤
                             └┴              ╵
                             └                                            ┘
                             582.04 µs           1.81 ms            3.03 ms

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

------------------------------------------- -------------------------------
gjs large (control)          267.58 µs/iter 261.72 µs  █                   
                      (234.18 µs … 4.47 ms) 362.12 µs  █▃                  
                    ( 85.83 kb … 667.54 kb) 217.22 kb ▅██▄▅█▄▂▁▂▁▁▁▁▁▁▁▁▁▁▁

gjs large (experiment)       265.50 µs/iter 260.93 µs  █                   
                      (233.97 µs … 4.44 ms) 336.00 µs  █▆                  
                    ( 16.88 kb … 692.73 kb) 216.56 kb ▄███▄▅▇▅▃▂▂▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌─────────┬                                ╷
         gjs large (control) ├─┤         │────────────────────────────────┤
                             ╵ └─────────┴                                ╵
                             ╷ ┌────────┬                        ╷
      gjs large (experiment) ├─┤        │────────────────────────┤
                             ╵ └────────┴                        ╵
                             └                                            ┘
                             233.97 µs         298.04 µs          362.12 µs

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

------------------------------------------- -------------------------------
gts small (control)            1.33 ms/iter   1.26 ms █                    
                        (1.20 ms … 6.12 ms)   4.89 ms █                    
                    (165.98 kb …   1.73 mb)   1.06 mb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.34 ms/iter   1.26 ms █                    
                        (1.20 ms … 6.13 ms)   5.11 ms █                    
                    (180.06 kb …   1.79 mb)   1.05 mb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                       ╷
         gts small (control) │ │───────────────────────────────────────┤
                             └─┴                                       ╵
                             ┌─┬                                          ╷
      gts small (experiment) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             └                                            ┘
                             1.20 ms            3.15 ms             5.11 ms

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

------------------------------------------- -------------------------------
gts medium (control)         677.08 µs/iter 632.51 µs  █                   
                      (586.17 µs … 4.88 ms)   1.22 ms  █                   
                    (121.24 kb …   1.48 mb) 541.54 kb ██▆▂▁▁▁▁▁▁▁▁▁▁▁▁▁▂▁▁▁

gts medium (experiment)      657.02 µs/iter 628.87 µs  █                   
                      (582.57 µs … 4.90 ms)   1.61 ms ▇█                   
                    (292.38 kb …   1.26 mb) 541.33 kb ██▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

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

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

------------------------------------------- -------------------------------
gts large (control)          272.00 µs/iter 261.52 µs  █                   
                      (234.69 µs … 6.00 ms) 437.71 µs ██ ▂                 
                    (129.07 kb … 961.02 kb) 217.19 kb ██▅█▅▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts large (experiment)       268.19 µs/iter 260.79 µs  █                   
                      (233.58 µs … 5.29 ms) 384.95 µs  █                   
                    (152.56 kb … 957.27 kb) 216.58 kb ▆█▆▃█▄▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌──────┬                                    ╷
         gts large (control) ├┤      │────────────────────────────────────┤
                             ╵└──────┴                                    ╵
                             ╷┌──────┬                        ╷
      gts large (experiment) ├┤      │────────────────────────┤
                             ╵└──────┴                        ╵
                             └                                            ┘
                             233.58 µs         335.64 µs          437.71 µs

summary
  gts large (experiment)
   1.01x 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

Adds a new Ember template accessibility rule to flag native non-interactive HTML elements that are assigned interactive ARIA roles, mirroring the intent/coverage of eslint-plugin-jsx-a11y’s no-noninteractive-element-to-interactive-role.

Changes:

  • Introduces template-no-noninteractive-element-to-interactive-role rule, deriving non-interactive tags from aria-query + axobject-query, and reusing a shared interactive-role set.
  • Adds unit tests plus an extensive peer-parity audit fixture against jsx-a11y.
  • Adds axobject-query as a direct dependency and documents the new rule (docs + README rule list).

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
lib/rules/template-no-noninteractive-element-to-interactive-role.js Implements the new rule and non-interactive tag derivation logic.
lib/utils/interactive-roles.js Adds shared derived INTERACTIVE_ROLES helper sourced from aria-query.
tests/lib/rules/template-no-noninteractive-element-to-interactive-role.js Adds CI unit tests for the new rule (gjs + hbs parser variants).
tests/audit/no-noninteractive-element-to-interactive-role/peer-parity.js Adds non-CI audit fixture to track parity/divergences vs jsx-a11y.
docs/rules/template-no-noninteractive-element-to-interactive-role.md Adds rule documentation and examples.
README.md Adds the rule to the auto-generated rules table.
package.json / pnpm-lock.yaml Adds axobject-query dependency and lockfile entries.
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-noninteractive-element-to-interactive-role.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 7 out of 8 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/interactive-roles.js
Comment thread lib/rules/template-no-noninteractive-element-to-interactive-role.js Outdated
Comment thread tests/audit/no-noninteractive-element-to-interactive-role/peer-parity.js Outdated
@johanrd johanrd force-pushed the feat/template-no-noninteractive-to-interactive-role branch from 6020fa7 to c2c9577 Compare April 22, 2026 17:14
johanrd added a commit that referenced this pull request Apr 23, 2026
@johanrd johanrd requested a review from Copilot April 24, 2026 08:36
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 8 out of 9 changed files in this pull request and generated 1 comment.

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
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 8 out of 9 changed files in this pull request and generated no new 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.

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

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 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 Outdated
Comment thread docs/rules/template-no-noninteractive-element-to-interactive-role.md Outdated
@johanrd johanrd force-pushed the feat/template-no-noninteractive-to-interactive-role branch from e6fa9ed to 0f1c698 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.

@johanrd johanrd requested a review from Copilot April 27, 2026 11:43
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 6 out of 6 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 README.md
Comment thread tests/audit/no-noninteractive-element-to-interactive-role/peer-parity.js Outdated
Comment thread lib/utils/interactive-roles.js
@johanrd johanrd force-pushed the feat/template-no-noninteractive-to-interactive-role branch from 2410f78 to c19253d Compare April 27, 2026 14:01
@johanrd johanrd force-pushed the feat/template-no-noninteractive-to-interactive-role branch from 1386766 to 89a6f1d Compare April 27, 2026 19:26
@johanrd johanrd requested a review from Copilot April 27, 2026 19:34
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 5 out of 5 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 on lines +80 to +82
// Exclude `header` — its role depends on ancestry (banner when direct child
// of body, generic otherwise). Matches jsx-a11y's carve-out.
tags.delete('header');
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 header carve-out is a behaviorally important special case, but the new test suite doesn’t appear to assert it. Add a valid fixture for <header role=\"button\"></header> (in both the <template>...</template> and .hbs RuleTester blocks if applicable) to prevent regressions where header accidentally re-enters NON_INTERACTIVE_TAGS.

Copilot uses AI. Check for mistakes.
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