fix: template-no-autofocus-attribute — value-aware + <dialog> exception#32
fix: template-no-autofocus-attribute — value-aware + <dialog> exception#32
Conversation
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)
🏎️ Benchmark Comparison
Full mitata output |
…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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
… reword {{input}} comment)
…-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.
There was a problem hiding this comment.
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.
Note
This is part of a series where Claude has audited
eslint-plugin-emberagainst 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 intests/audit/no-autofocus/peer-parity.js) flagged two gaps intemplate-no-autofocus-attribute, tracked in PR #28 as G3 and G4:autofocuson attribute presence only, soautofocus={{false}}(which renders no attribute at all in the final HTML) was reported even though the rendered element has no autofocus.autofocuson 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
autofocusindicates TRUE regardless of value —autofocus="false"andautofocus="autofocus"are equally truthy. The only statically-known opt-out consistent with HTML boolean-attribute semantics is:<input autofocus={{false}} />— Glimmer renders noautofocusattribute 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:normalizeValuereturnsnullforfalse | undefined | null, andSimpleDynamicAttribute.update()callselement.removeAttribute(name)when the normalized value is null. Soautofocus={{false}}renders with noautofocusattribute in the DOM — notautofocus="false".Not exempted (flagged per spec):
autofocus="false"— presence = truthy.autofocus={{"false"}}— renders asautofocus="false"= truthy.{{input autofocus="false"}}— same.This diverges from jsx-a11y's
no-autofocus, which treats the literal string"false"as an opt-out (viagetPropValue). 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'sno-autofocusimplements 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.