Skip to content

fix: template-no-autofocus-attribute — value-aware + <dialog> exception#32

Closed
johanrd wants to merge 9 commits intomasterfrom
fix/autofocus-value-aware-and-dialog
Closed

fix: template-no-autofocus-attribute — value-aware + <dialog> exception#32
johanrd wants to merge 9 commits intomasterfrom
fix/autofocus-value-aware-and-dialog

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

The Phase 3 audit on audit/phase3/no-autofocus (fixture B10 in tests/audit/no-autofocus/peer-parity.js) flagged two gaps in template-no-autofocus-attribute, tracked in PR #28 as G3 and G4:

  • G3 — The rule flagged autofocus on attribute presence only, so autofocus={{false}} (which renders no attribute at all in the final HTML) was reported even though the rendered element has no autofocus.
  • G4 — The rule flagged autofocus on any element, including <dialog> and its descendants. MDN's <dialog> reference describes autofocus-on-open as the recommended dialog behavior.

Fix

G3 — value-aware for mustache boolean-literal only

Per the HTML Living Standard on boolean attributes, the presence of autofocus indicates TRUE regardless of value — autofocus="false" and autofocus="autofocus" are equally truthy. The only statically-known opt-out consistent with HTML boolean-attribute semantics is:

  • <input autofocus={{false}} /> — Glimmer renders no autofocus attribute when the mustache-literal evaluates to false.
  • {{input autofocus=false}} — same at the hash-pair level.

Both are now exempted.

Verification of the Glimmer rendering claim: the attribute-omission behavior is authoritative per Glimmer VM's dynamic.ts: normalizeValue returns null for false | undefined | null, and SimpleDynamicAttribute.update() calls element.removeAttribute(name) when the normalized value is null. So autofocus={{false}} renders with no autofocus attribute in the DOM — not autofocus="false".

Not exempted (flagged per spec):

  • autofocus="false" — presence = truthy.
  • autofocus={{"false"}} — renders as autofocus="false" = truthy.
  • {{input autofocus="false"}} — same.
  • Dynamic mustache expressions (helpers, path references) — can't be proved safe.

This diverges from jsx-a11y's no-autofocus, which treats the literal string "false" as an opt-out (via getPropValue). vue-a11y and lit-a11y are presence-based, consistent with the HTML spec — we align with them. Matches the spec-first direction established in ember-cli#2717, #19, and #33.

G4 — exemption

autofocus on a <dialog> or any descendant of a <dialog> is exempt. MDN-backed editorial guidance (not a normative spec requirement); angular-eslint's no-autofocus implements the same exemption.

Tests

22 tests total. Moved 3 cases from valid → invalid (per the G3 spec correction); 11 dialog / non-literal-falsy / peer-parity cases unchanged.

Closes two gaps identified in PR #28 Phase 3 audit (B10 fixture on
`audit/phase3/no-autofocus`):

G3 — value-aware detection. The rule previously flagged `autofocus` purely
on presence, producing false positives for explicit opt-out forms:
  - `autofocus="false"` (GlimmerTextNode chars === "false")
  - `autofocus={{false}}` (BooleanLiteral false)
  - `autofocus={{"false"}}` (StringLiteral "false")
These are now treated as valid. jsx-a11y's `no-autofocus` reads the value
via `getPropValue` and exits early on falsy results, which is the behavior
encoded here. Truthy literals (`="true"`, `="autofocus"`, `={{true}}`,
`={{"true"}}`) and any dynamic expression (`={{this.x}}`) still flag —
the rule cannot prove a dynamic value is safe. Mustache-hash-pair forms
(`{{input autofocus=false}}`, `{{input autofocus="false"}}`) receive the
same value-aware treatment so `autofocus=true` and `autofocus=false` do
not behave inconsistently between syntaxes.

G4 — `<dialog>` exception. The rule now skips reporting when the element
carrying `autofocus` is a `<dialog>` itself OR is nested at any depth
inside a `<dialog>`. Per MDN's `<dialog>` documentation, a dialog is
expected to move focus to its initial control on open, so
`autofocus` on or within a dialog is the recommended pattern rather
than an accessibility defect. This matches
`@angular-eslint/template/no-autofocus`, which exempts the same subtree.

The descendant walk is a full parent-chain traversal via `node.parent`;
the Glimmer AST in this codebase exposes `parent` on every element node,
so no scope narrowing was required.

Behavior unchanged for: bare `<input autofocus>`, truthy string/mustache
values, dynamic mustache values, and any `autofocus` outside a dialog
subtree. 22 rule tests (10 valid, 12 invalid) pass; full suite (9089
tests) green.

Audit fixture `tests/audit/no-autofocus/peer-parity.js` lives on branch
`audit/phase3/no-autofocus` and will need separate updating to reflect
the new parity status for G3 and G4.

Refs: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
Refs: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/rules/no-autofocus.js
Refs: #28 (G3, G4)
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
js small 15.26 ms 15.00 ms -1.7%
🟢 js medium 7.41 ms 7.09 ms -4.3%
🟢 js large 2.90 ms 2.83 ms -2.4%
gjs small 1.26 ms 1.25 ms -1.0%
🟢 gjs medium 632.94 µs 615.83 µs -2.7%
gjs large 244.55 µs 245.19 µs +0.3%
gts small 1.24 ms 1.23 ms -0.7%
gts medium 614.01 µs 623.33 µs +1.5%
gts large 246.40 µs 246.56 µs +0.1%

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

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

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
js small (control)            17.55 ms/iter  19.39 ms █                    
                      (12.90 ms … 29.64 ms)  27.65 ms █▅▅                  
                    (  5.69 mb …  10.13 mb)   7.26 mb ███▇▇▇▇▄▁▄▄▁▄▁▁▁▄▁▄▇▇

js small (experiment)         15.94 ms/iter  16.07 ms  █                   
                      (13.56 ms … 24.75 ms)  22.96 ms  █ ▃                 
                    (  6.37 mb …   8.32 mb)   6.84 mb ▅█▅███▁▇▁▃▅▁▁▁▁▁▁▁▁▃▅

                             ┌                                            ┐
                             ╷┌────────────┬─────┐                        ╷
          js small (control) ├┤            │     ├────────────────────────┤
                             ╵└────────────┴─────┘                        ╵
                               ╷┌─────┬┐                    ╷
       js small (experiment)   ├┤     │├────────────────────┤
                               ╵└─────┴┘                    ╵
                             └                                            ┘
                             12.90 ms           20.27 ms           27.65 ms

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

------------------------------------------- -------------------------------
js medium (control)            8.09 ms/iter   8.18 ms  █▂                  
                       (6.89 ms … 14.71 ms)  13.44 ms ▆██                  
                    (  2.37 mb …   4.67 mb)   3.54 mb ███▃▇▅▃▄▁▂▂▁▁▂▃▁▁▂▂▂▂

js medium (experiment)         7.71 ms/iter   7.73 ms █▂                   
                       (6.72 ms … 14.97 ms)  14.23 ms ██                   
                    (  2.25 mb …   4.74 mb)   3.51 mb ██▆▃▅▅▂▁▁▂▁▂▂▁▁▁▁▁▂▁▂

                             ┌                                            ┐
                              ╷┌─────┬┐                              ╷
         js medium (control)  ├┤     │├──────────────────────────────┤
                              ╵└─────┴┘                              ╵
                             ╷┌────┬                                      ╷
      js medium (experiment) ├┤    │──────────────────────────────────────┤
                             ╵└────┴                                      ╵
                             └                                            ┘
                             6.72 ms           10.47 ms            14.23 ms

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

------------------------------------------- -------------------------------
js large (control)             3.34 ms/iter   3.05 ms ▅█                   
                       (2.71 ms … 11.57 ms)   8.61 ms ██                   
                    (506.72 kb …   3.08 mb)   1.45 mb ██▃▃▂▁▂▂▂▁▂▂▂▁▁▁▁▁▁▁▁

js large (experiment)          3.10 ms/iter   2.92 ms  █                   
                        (2.63 ms … 7.74 ms)   6.22 ms  █                   
                    (670.88 kb …   2.32 mb)   1.43 mb ██▄▂▂▂▂▁▁▁▁▂▁▁▂▁▁▁▁▁▁

                             ┌                                            ┐
                              ╷┌──┬                                       ╷
          js large (control)  ├┤  │───────────────────────────────────────┤
                              ╵└──┴                                       ╵
                             ╷┌──┬                      ╷
       js large (experiment) ├┤  │──────────────────────┤
                             ╵└──┴                      ╵
                             └                                            ┘
                             2.63 ms            5.62 ms             8.61 ms

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

------------------------------------------- -------------------------------
gjs small (control)            1.39 ms/iter   1.34 ms █                    
                        (1.21 ms … 6.45 ms)   5.57 ms █                    
                    (584.27 kb …   1.60 mb)   1.06 mb ██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.38 ms/iter   1.28 ms █                    
                        (1.21 ms … 7.08 ms)   5.93 ms █                    
                    (380.73 kb …   1.76 mb)   1.06 mb █▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                       ╷
         gjs small (control) │ │───────────────────────────────────────┤
                             └─┴                                       ╵
                             ┌─┬                                          ╷
      gjs small (experiment) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             └                                            ┘
                             1.21 ms            3.57 ms             5.93 ms

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

------------------------------------------- -------------------------------
gjs medium (control)         776.83 µs/iter 654.62 µs █                    
                      (591.16 µs … 6.75 ms)   2.89 ms █                    
                    (121.22 kb …   1.12 mb) 541.92 kb █▅▁▁▂▂▂▃▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      661.93 µs/iter 631.63 µs ▆█                   
                      (587.85 µs … 5.51 ms)   1.65 ms ██                   
                    (252.93 kb …   1.11 mb) 541.35 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌──┬                                        ╷
        gjs medium (control) ├┤  │────────────────────────────────────────┤
                             ╵└──┴                                        ╵
                             ┌┬                   ╷
     gjs medium (experiment) ││───────────────────┤
                             └┴                   ╵
                             └                                            ┘
                             587.85 µs           1.74 ms            2.89 ms

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

------------------------------------------- -------------------------------
gjs large (control)          269.66 µs/iter 260.41 µs ▅█                   
                      (233.88 µs … 5.36 ms) 448.47 µs ██ ▂                 
                    (216.10 kb … 808.91 kb) 217.21 kb ████▄▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs large (experiment)       270.09 µs/iter 261.31 µs  █                   
                      (235.61 µs … 5.51 ms) 359.00 µs  █▇                  
                    (215.70 kb … 901.18 kb) 216.81 kb ███▃▆▇▅▂▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌──────┬                                    ╷
         gjs large (control) ├┤      │────────────────────────────────────┤
                             ╵└──────┴                                    ╵
                             ╷┌──────┬                 ╷
      gjs large (experiment) ├┤      │─────────────────┤
                             ╵└──────┴                 ╵
                             └                                            ┘
                             233.88 µs         341.17 µs          448.47 µs

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

------------------------------------------- -------------------------------
gts small (control)            1.35 ms/iter   1.26 ms █                    
                        (1.22 ms … 6.64 ms)   5.46 ms █                    
                    (506.66 kb …   1.64 mb)   1.06 mb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.33 ms/iter   1.25 ms █                    
                        (1.20 ms … 6.67 ms)   5.42 ms █                    
                    (196.13 kb …   1.77 mb)   1.06 mb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

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

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

------------------------------------------- -------------------------------
gts medium (control)         663.08 µs/iter 630.52 µs █▂                   
                      (584.25 µs … 6.00 ms)   1.83 ms ██                   
                    (103.11 kb …   1.11 mb) 541.47 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      666.42 µs/iter 637.27 µs  █                   
                      (592.70 µs … 5.78 ms)   1.28 ms  █                   
                    (144.60 kb … 984.99 kb) 540.61 kb ██▄▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌─┬                                         ╷
        gts medium (control) ├┤ │─────────────────────────────────────────┤
                             ╵└─┴                                         ╵
                             ╷┌─┬                     ╷
     gts medium (experiment) ├┤ │─────────────────────┤
                             ╵└─┴                     ╵
                             └                                            ┘
                             584.25 µs           1.21 ms            1.83 ms

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

------------------------------------------- -------------------------------
gts large (control)          269.15 µs/iter 261.76 µs  █                   
                      (234.70 µs … 5.53 ms) 348.87 µs  ██                  
                    (146.12 kb …   1.00 mb) 216.95 kb ▅██▇▄█▅▅▂▂▁▁▁▁▁▁▁▁▁▁▁

gts large (experiment)       270.75 µs/iter 262.71 µs ▃█                   
                      (236.38 µs … 5.41 ms) 400.21 µs ██                   
                    (152.52 kb … 965.91 kb) 216.58 kb █████▄▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌───────┬                     ╷
         gts large (control) ├┤       │─────────────────────┤
                             ╵└───────┴                     ╵
                             ╷ ┌───────┬                                  ╷
      gts large (experiment) ├─┤       │──────────────────────────────────┤
                             ╵ └───────┴                                  ╵
                             └                                            ┘
                             234.70 µs         317.45 µs          400.21 µs

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

johanrd added 3 commits April 21, 2026 16:26
…ML boolean semantics

Per HTML Living Standard on boolean attributes, the presence of `autofocus`
indicates TRUE regardless of value — `autofocus="false"` and
`autofocus="autofocus"` are equally truthy. jsx-a11y's `no-autofocus`
treats the literal string `"false"` as an opt-out (via `getPropValue`),
but that's a peer-plugin convention that diverges from HTML semantics;
vue-a11y and lit-a11y are presence-based, consistent with the spec.

Narrow opt-out to the only case that is spec-consistent:
- `autofocus={{false}}` in angle-bracket syntax — renders no attribute.
- `{{input autofocus=false}}` in mustache hash-pair syntax — no attribute.

Revert peer-parity opt-outs for `autofocus="false"`, `autofocus={{"false"}}`,
and `{{input autofocus="false"}}` — these are now flagged per HTML spec
semantics. Moved from valid → invalid in the test suite.

Dialog exemption unchanged — keeps MDN-backed behavior for autofocus on
and within <dialog>.

Follows the spec-first direction established in ember-cli#2717 (aria-hidden),
#19, #33.
…ibute

The G3 exemption's load-bearing premise (Glimmer renders no attribute
when given a mustache-literal false) was asserted in the PR body but
unverified in the rule's own documentation. Verified against Glimmer
VM's attribute-normalization path:

  glimmer-vm/packages/@glimmer/runtime/lib/vm/attributes/dynamic.ts

`normalizeValue` returns null for false/undefined/null; then
`SimpleDynamicAttribute.update()` calls element.removeAttribute(name)
when the value is null. So autofocus={{false}} genuinely omits the
attribute from the rendered DOM — not autofocus="false".

Inlined the citation into the rule's JSDoc so future readers don't
have to trace it.
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

Updates the template-no-autofocus-attribute rule to reduce false positives by treating statically-known false mustache literals as “no rendered attribute”, and to exempt <dialog>-scoped autofocus usage.

Changes:

  • Make the rule value-aware for the mustache boolean-literal false (both element-attr and mustache hash-pair forms).
  • Add a <dialog> (and descendants) exemption path.
  • Extend/adjust tests and rule documentation to cover the new behaviors.

Reviewed changes

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

File Description
tests/lib/rules/template-no-autofocus-attribute.js Adds valid/invalid cases for literal-false handling and <dialog> exemption.
lib/rules/template-no-autofocus-attribute.js Implements mustache-literal-false opt-outs and a <dialog> ancestor exemption.
docs/rules/template-no-autofocus-attribute.md Documents new opt-out and <dialog> exemption behavior.

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

Comment thread docs/rules/template-no-autofocus-attribute.md Outdated
Comment thread lib/rules/template-no-autofocus-attribute.js
Comment thread lib/rules/template-no-autofocus-attribute.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 3 out of 3 changed files in this pull request and generated 1 comment.


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

Comment thread lib/rules/template-no-autofocus-attribute.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 3 out of 3 changed files in this pull request and generated 3 comments.


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

Comment thread lib/rules/template-no-autofocus-attribute.js
Comment thread docs/rules/template-no-autofocus-attribute.md
Comment thread tests/lib/rules/template-no-autofocus-attribute.js Outdated
johanrd added 3 commits April 24, 2026 13:25
…-input helpers (Copilot review)

Previously the GlimmerMustacheStatement visitor fired on ANY mustache
with an autofocus hash pair — but arbitrary custom components taking
'autofocus' as a prop are opaque. We can't statically tell whether the
prop forwards to a native <input autofocus> or is used for something
else. Narrow to the two built-ins that deterministically render a
native input:
  - {{input …}}
  - {{component "input" …}}

Future: when type-aware linting lands (Glint integration or template-
type-check), we can resolve custom components that forward 'autofocus'
to a native <input> and flag those too. For now we stay conservative
to avoid false positives on unrelated helpers.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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


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

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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

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 closed this Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants