Skip to content

feat: add template-no-noninteractive-tabindex#24

Open
johanrd wants to merge 20 commits intomasterfrom
feat/template-no-noninteractive-tabindex
Open

feat: add template-no-noninteractive-tabindex#24
johanrd wants to merge 20 commits intomasterfrom
feat/template-no-noninteractive-tabindex

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: tabindex="0" on a <div> puts the element in the keyboard tab order without supplying any keyboard semantics — users reach it but can't operate it, and screen readers announce the tag with no hint of interactivity. Normative basis: WCAG 2.1 SC 2.1.1 Keyboard (Level A) + SC 4.1.2 Name, Role, Value. Implementation patterns align with jsx-a11y's no-noninteractive-tabindex.
  • Problem: No rule catches this today.

Fix: add template-no-noninteractive-tabindex.

Not flagged (interactive):

  • HTML interactive content (per HTML Living Standard §3.2.5.2.7, via the shared html-interactive-content util): button, details, embed, iframe, label, select, summary, textarea, plus conditional <a href>, <input> (except type=hidden), <img usemap>, <audio controls>, <video controls>. Note: for <audio>/<video> we align with §3.2.5.2.7 (only controls counts) — stricter than axobject-query, which marks bare <audio>/<video> as widget unconditionally.
  • <canvas> — not in §3.2.5.2.7, but commonly wired as a drawing/game surface with tabindex; preserved via rule-level defensive for upstream ember-template-lint parity.
  • Non-interactive tag with an interactive ARIA role (button, checkbox, tab, …).
  • tabindex="-1" — per HTML Living Standard §6.6.3, negative values make the element focusable but omit it from sequential focus navigation. Legitimate use cases include programmatic focus (modals, error messages) and roving-tabindex composite widgets. MDN documents these. Consistent with template-require-aria-activedescendant-tabindex which accepts both 0 and -1, and jsx-a11y's same exemption.
  • Dynamic role values — conservatively skipped.
  • role="tabpanel" — exempt by default via the roles option (see below). The WAI-ARIA APG Tabs pattern gives panels tabindex="0" when the panel's content isn't itself focusable; flagging that would break canonical tab code. Matches jsx-a11y's recommended config.

Options

  • roles (default ["tabpanel"]) — non-interactive ARIA roles exempted. Use roles: [] for strict jsx-a11y-strict-equivalent behavior; widen to ["tabpanel", "region", …] where your project uses tabindex on scrollable regions or similar.

Prior art

Plugin Rule Verified behavior
jsx-a11y no-noninteractive-tabindex Flags tabindex on elements that are neither natively interactive nor carry an interactive role; accepts tabindex="-1" as the roving-tabindex exemption.
vuejs-accessibility No equivalent rule (verified: no matching file in src/rules/; only tabindex-no-positive exists, which is a different check).
@angular-eslint/template No equivalent rule (verified: no matching file in packages/eslint-plugin-template/src/rules/; only no-positive-tabindex exists, a different check).
lit-a11y No equivalent rule (verified: no matching file in lib/rules/; only tabindex-no-positive exists, a different check).

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
🟢 js small 12.84 ms 12.24 ms -4.7%
js medium 6.07 ms 6.00 ms -1.1%
🟠 js large 2.26 ms 2.34 ms +3.6%
🟢 gjs small 1.23 ms 1.16 ms -5.7%
gjs medium 584.00 µs 580.09 µs -0.7%
gjs large 229.52 µs 228.71 µs -0.4%
gts small 1.16 ms 1.16 ms -0.1%
gts medium 579.05 µs 578.66 µs -0.1%
gts large 229.31 µs 228.32 µs -0.4%

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

Full mitata output
clk: ~3.35 GHz
cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
runtime: node 24.15.0 (x64-linux)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
js small (control)            15.16 ms/iter  16.43 ms █                    
                      (10.68 ms … 29.82 ms)  28.35 ms █▂█ ▂                
                    (  5.58 mb …  10.13 mb)   7.21 mb ███▆█▆▃█▁▁▁▃▃▁▃▁▃▃▃▁▃

js small (experiment)         12.96 ms/iter  13.97 ms ▅  █                 
                      (10.99 ms … 18.90 ms)  17.26 ms █▆▆█▆▃▆▃             
                    (  6.31 mb …   7.86 mb)   6.83 mb ████████▁▄█▄█▁█▄▁▁▄▁█

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

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

------------------------------------------- -------------------------------
js medium (control)            6.81 ms/iter   7.07 ms  █                   
                       (5.63 ms … 13.43 ms)  12.91 ms ▃█                   
                    (  2.93 mb …   4.27 mb)   3.53 mb ███▅▃▄▄▁▃▃▁▂▂▁▁▁▂▁▂▁▂

js medium (experiment)         6.68 ms/iter   6.99 ms  █                   
                       (5.67 ms … 12.78 ms)  12.37 ms ▅█                   
                    (  2.94 mb …   4.09 mb)   3.53 mb ██▄▂▃▃▃▃▁▃▂▂▁▁▃▁▁▁▂▁▂

                             ┌                                            ┐
                             ╷┌─────┬─┐                                   ╷
         js medium (control) ├┤     │ ├───────────────────────────────────┤
                             ╵└─────┴─┘                                   ╵
                             ╷ ┌────┬┐                                 ╷
      js medium (experiment) ├─┤    │├─────────────────────────────────┤
                             ╵ └────┴┘                                 ╵
                             └                                            ┘
                             5.63 ms            9.27 ms            12.91 ms

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

------------------------------------------- -------------------------------
js large (control)             2.74 ms/iter   2.62 ms ██                   
                       (2.09 ms … 10.43 ms)   7.10 ms ██                   
                    ( 75.36 kb …   3.12 mb)   1.43 mb ██▄▃▁▄▃▃▂▂▁▂▁▂▁▂▁▂▁▁▁

js large (experiment)          2.61 ms/iter   2.48 ms ▃█                   
                        (2.22 ms … 7.00 ms)   5.74 ms ██                   
                    (416.22 kb …   2.46 mb)   1.43 mb ██▄▃▂▂▂▂▁▂▂▁▁▁▁▁▁▁▁▁▂

                             ┌                                            ┐
                             ╷┌────┬                                      ╷
          js large (control) ├┤    │──────────────────────────────────────┤
                             ╵└────┴                                      ╵
                              ╷┌──┬                           ╷
       js large (experiment)  ├┤  │───────────────────────────┤
                              ╵└──┴                           ╵
                             └                                            ┘
                             2.09 ms            4.60 ms             7.10 ms

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

------------------------------------------- -------------------------------
gjs small (control)            1.50 ms/iter   1.76 ms █                    
                        (1.13 ms … 6.20 ms)   5.33 ms █                    
                    (476.48 kb …   1.79 mb)   1.06 mb █▇▃▅▇▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.30 ms/iter   1.21 ms █                    
                        (1.13 ms … 5.81 ms)   5.48 ms █                    
                    (306.26 kb …   1.72 mb)   1.06 mb █▅▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌───┬──┐                                   ╷
         gjs small (control) │   │  ├───────────────────────────────────┤
                             └───┴──┘                                   ╵
                             ┌─┬                                          ╷
      gjs small (experiment) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             └                                            ┘
                             1.13 ms            3.31 ms             5.48 ms

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

------------------------------------------- -------------------------------
gjs medium (control)         639.54 µs/iter 603.10 µs █                    
                      (556.25 µs … 5.21 ms)   2.99 ms █                    
                    (227.26 kb …   1.13 mb) 542.16 kb █▅▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      634.51 µs/iter 597.77 µs █                    
                      (553.31 µs … 5.53 ms)   1.79 ms ██                   
                    ( 80.75 kb …   1.26 mb) 541.00 kb ██▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

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

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

------------------------------------------- -------------------------------
gjs large (control)          253.05 µs/iter 242.28 µs  █▃                  
                      (220.08 µs … 4.92 ms) 313.12 µs  ██                  
                    ( 74.17 kb … 966.59 kb) 217.15 kb ▇██▇▅█▇▄▂▂▂▂▁▁▁▁▁▁▁▁▁

gjs large (experiment)       251.49 µs/iter 243.42 µs  █                   
                      (220.40 µs … 4.95 ms) 293.66 µs  █▇                  
                    (170.19 kb …   1.31 mb) 216.75 kb ▆███▄▄██▅▂▂▂▂▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌─────────────┬                            ╷
         gjs large (control) ├─┤             │────────────────────────────┤
                             ╵ └─────────────┴                            ╵
                             ╷ ┌────────────┬                    ╷
      gjs large (experiment) ├─┤            │────────────────────┤
                             ╵ └────────────┴                    ╵
                             └                                            ┘
                             220.08 µs         266.60 µs          313.12 µs

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

------------------------------------------- -------------------------------
gts small (control)            1.26 ms/iter   1.19 ms █                    
                        (1.13 ms … 5.98 ms)   5.26 ms █                    
                    (648.78 kb …   1.79 mb)   1.06 mb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.27 ms/iter   1.20 ms █                    
                        (1.13 ms … 5.96 ms)   5.21 ms █                    
                    (222.23 kb …   1.91 mb)   1.05 mb █▆▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
         gts small (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌─┬                                          ╷
      gts small (experiment) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             └                                            ┘
                             1.13 ms            3.19 ms             5.26 ms

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

------------------------------------------- -------------------------------
gts medium (control)         626.98 µs/iter 594.56 µs  █                   
                      (552.30 µs … 5.32 ms)   1.19 ms ▂█                   
                    (217.92 kb …   1.28 mb) 541.58 kb ██▆▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      637.43 µs/iter 590.40 µs █                    
                      (551.62 µs … 5.80 ms)   3.46 ms █                    
                    (272.34 kb …   1.10 mb) 540.48 kb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬        ╷
        gts medium (control) ││────────┤
                             └┴        ╵
                             ┌┬                                           ╷
     gts medium (experiment) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             └                                            ┘
                             551.62 µs           2.01 ms            3.46 ms

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

------------------------------------------- -------------------------------
gts large (control)          251.89 µs/iter 242.20 µs  █                   
                      (219.85 µs … 5.32 ms) 292.75 µs  ██▄                 
                    (170.56 kb …   1.49 mb) 217.08 kb ▃███▅▅▇█▅▃▂▂▂▂▁▁▁▁▁▁▁

gts large (experiment)       251.75 µs/iter 240.81 µs  █                   
                      (220.44 µs … 4.93 ms) 299.79 µs  ██                  
                    (112.50 kb … 739.09 kb) 216.42 kb ▆██▇▅▆▇▅▃▂▂▂▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷  ┌──────────────┬                      ╷
         gts large (control) ├──┤              │──────────────────────┤
                             ╵  └──────────────┴                      ╵
                             ╷  ┌──────────────┬                          ╷
      gts large (experiment) ├──┤              │──────────────────────────┤
                             ╵  └──────────────┴                          ╵
                             └                                            ┘
                             219.85 µs         259.82 µs          299.79 µs

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

johanrd added a commit that referenced this pull request Apr 21, 2026
Translates jsx-a11y's no-noninteractive-tabindex test list to HBS to pin
our rule's behavior against PR #24's interactive-element and tabindex="-1"
exemption fixes. Parity holds on all upstream always-valid and never-valid
cases. Annotated divergences:

- tabpanel: we flag (jsx-a11y strict parity); jsx-a11y recommended
  whitelists it via `roles: ['tabpanel']`.
- Dynamic role expressions: we skip conservatively; matches jsx-a11y
  recommended (allowExpressionValues: true), diverges from strict.
- Component name collides with a native tag (e.g. <Article>): we
  lowercase tag before the aria-query dom lookup so <Article> validates
  as <article>. False positive relative to jsx-a11y's no-settings default.
@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

This PR adds a new accessibility rule (ember/template-no-noninteractive-tabindex) to flag tabindex="0" on non-interactive elements in Ember templates, aligning with JSX a11y’s no-noninteractive-tabindex while preserving Ember-specific component/element discrimination.

Changes:

  • Added template-no-noninteractive-tabindex rule with an allowlist option for non-interactive roles (defaulting to ["tabpanel"]) and exemptions like tabindex="-1".
  • Introduced supporting utilities for native-element detection and HTML interactive-content classification.
  • Added comprehensive tests (rule + utils) and documentation, plus README rule list update.

Reviewed changes

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

Show a summary per file
File Description
lib/rules/template-no-noninteractive-tabindex.js Implements the new rule, including role classification and exemptions.
lib/utils/is-native-element.js Adds authoritative native-element detection with scope shadowing support.
lib/utils/html-interactive-content.js Adds spec-based “interactive content” classification helper used by the rule.
tests/lib/rules/template-no-noninteractive-tabindex.js RuleTester coverage for valid/invalid cases and options behavior.
tests/lib/utils/is-native-element-test.js Unit tests for list-based native-element identification behavior.
tests/lib/utils/html-interactive-content-test.js Unit tests for HTML interactive-content classification.
tests/audit/no-noninteractive-tabindex/peer-parity.js Non-CI audit fixture comparing behavior to jsx-a11y (needs comment update).
docs/rules/template-no-noninteractive-tabindex.md New rule documentation and examples, including options.
README.md Adds the new rule to the Accessibility rules table.

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

Comment thread lib/rules/template-no-noninteractive-tabindex.js Outdated
Comment thread tests/audit/no-noninteractive-tabindex/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 9 out of 9 changed files in this pull request and generated 5 comments.


💡 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 lib/utils/html-interactive-content.js
Comment thread lib/rules/template-no-noninteractive-tabindex.js
Comment thread lib/rules/template-no-noninteractive-tabindex.js
Comment thread tests/audit/no-noninteractive-tabindex/peer-parity.js Outdated
@johanrd johanrd force-pushed the feat/template-no-noninteractive-tabindex branch from c710c9a to b7bae63 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 10 out of 10 changed files in this pull request and generated 4 comments.


💡 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/html-interactive-content.js
Comment thread lib/utils/interactive-roles.js
Comment thread lib/rules/template-no-noninteractive-tabindex.js
johanrd added a commit that referenced this pull request Apr 24, 2026
@johanrd johanrd requested a review from Copilot April 24, 2026 13:39
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 1 comment.


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

Comment thread lib/rules/template-no-noninteractive-tabindex.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.

Pull request overview

Copilot reviewed 12 out of 12 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 tests/lib/utils/is-native-element-test.js Outdated
Comment thread lib/rules/template-no-noninteractive-tabindex.js Outdated
Comment thread docs/rules/template-no-noninteractive-tabindex.md
johanrd added a commit that referenced this pull request Apr 26, 2026
Translates jsx-a11y's no-noninteractive-tabindex test list to HBS to pin
our rule's behavior against PR #24's interactive-element and tabindex="-1"
exemption fixes. Parity holds on all upstream always-valid and never-valid
cases. Annotated divergences:

- tabpanel: we flag (jsx-a11y strict parity); jsx-a11y recommended
  whitelists it via `roles: ['tabpanel']`.
- Dynamic role expressions: we skip conservatively; matches jsx-a11y
  recommended (allowExpressionValues: true), diverges from strict.
- Component name collides with a native tag (e.g. <Article>): we
  lowercase tag before the aria-query dom lookup so <Article> validates
  as <article>. False positive relative to jsx-a11y's no-settings default.
@johanrd johanrd force-pushed the feat/template-no-noninteractive-tabindex branch from 66815af to 8a6332f Compare April 26, 2026 08:10
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 7 out of 7 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/html-interactive-content.js
Comment thread lib/rules/template-no-noninteractive-tabindex.js
johanrd added a commit that referenced this pull request Apr 27, 2026
Translates jsx-a11y's no-noninteractive-tabindex test list to HBS to pin
our rule's behavior against PR #24's interactive-element and tabindex="-1"
exemption fixes. Parity holds on all upstream always-valid and never-valid
cases. Annotated divergences:

- tabpanel: we flag (jsx-a11y strict parity); jsx-a11y recommended
  whitelists it via `roles: ['tabpanel']`.
- Dynamic role expressions: we skip conservatively; matches jsx-a11y
  recommended (allowExpressionValues: true), diverges from strict.
- Component name collides with a native tag (e.g. <Article>): we
  lowercase tag before the aria-query dom lookup so <Article> validates
  as <article>. False positive relative to jsx-a11y's no-settings default.
@johanrd johanrd force-pushed the feat/template-no-noninteractive-tabindex branch from 01a3cfe to 5997752 Compare April 27, 2026 14:03
@johanrd johanrd force-pushed the feat/template-no-noninteractive-tabindex branch from 5997752 to 828d8e1 Compare April 27, 2026 19:28
@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 7 out of 7 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/rules/template-no-noninteractive-tabindex.js Outdated
Comment on lines +58 to +62
// tabindex="{{-1}}" → GlimmerConcatStatement with a single mustache part.
if (
value.type === 'GlimmerConcatStatement' &&
value.parts?.length === 1 &&
value.parts[0].type === 'GlimmerMustacheStatement'
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.

There’s a code path to handle GlimmerConcatStatement values like tabindex="{{-1}}", but the rule’s tests don’t currently cover that representation. Adding at least one valid/invalid case for the concat form would help prevent regressions in this parsing logic.

Copilot uses AI. Check for mistakes.
NullVoxPopuli and others added 17 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.
@johanrd johanrd force-pushed the feat/template-no-noninteractive-tabindex branch from 56c8425 to ca9b971 Compare April 28, 2026 12:04
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