BUGFIX: template-no-invalid-role — support DPUB/Graphics-ARIA and role-fallback lists#55
BUGFIX: template-no-invalid-role — support DPUB/Graphics-ARIA and role-fallback lists#55
Conversation
…pport DPUB-/Graphics-ARIA and role-fallback lists
Two related fixes, shared rewrite.
1. Replace the hand-maintained VALID_ROLES (~90 WAI-ARIA 1.2 tokens)
with a derived list from aria-query (concrete — non-abstract — role
keys), plus a small ARIA 1.3 draft-role allowlist that aria-query
doesn't yet ship.
Effect: DPUB-ARIA roles (doc-abstract, doc-chapter, …) and
Graphics-ARIA roles (graphics-document, graphics-object,
graphics-symbol) are no longer flagged as invalid.
2. Split the role value on whitespace before validating. A role
attribute is a list of tokens per ARIA 1.2 §5.4 (role fallback).
Each token must individually be valid.
Effect: role="tabpanel row", role="doc-appendix doc-bibliography",
and role="graphics-document document" now pass; role="tabpanel
row foobar" flags the first invalid token ("foobar") instead of
rejecting the whole string as one opaque role name.
Error message now names the specific offending token. Three existing
invalid tests updated accordingly (previously expected the whole
string; now the specific token).
Ten new valid tests cover DPUB/Graphics and the fallback-list shape.
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)
Translates 32 cases from peer-plugin rules:
- jsx-a11y aria-role
- vuejs-accessibility aria-role
- lit-a11y aria-role
Fixture documents parity after this fix:
- DPUB-ARIA and Graphics-ARIA roles accepted (via aria-query).
- Space-separated role tokens accepted when all are valid, and the
invalid-token variant names the specific offending token.
Remaining divergences (case-insensitive comparison, empty-string role
not flagged) are annotated inline.
…kens `associationlist`, `associationlistitemkey`, and `associationlistitemvalue` are not present in the current WAI-ARIA 1.3 editor's draft (https://w3c.github.io/aria/). Earlier commit listed all five as draft tokens; only `comment` and `suggestion` are actually proposed. Drop the three phantom tokens and the tests that accepted them as valid. With this change the rule now correctly flags `role='associationlist'` and siblings as invalid, matching peer behavior (jsx-a11y, vue-a11y, lit-a11y all reject them).
…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.
🏎️ Benchmark Comparison
Full mitata output |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Fixes false positives in template-no-invalid-role by aligning role validation with aria-query and ARIA’s whitespace-separated fallback-list semantics.
Changes:
- Derive
VALID_ROLESfromaria-queryconcrete (non-abstract) roles, covering DPUB-ARIAdoc-*and Graphics-ARIAgraphics-*. - Validate whitespace-separated
rolefallback lists token-by-token and improve invalid-role reporting to name the offending token. - Add parity/audit fixture tests (not in CI) and update/expand rule tests to cover new behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| lib/rules/template-no-invalid-role.js | Replaces the hand-maintained allowlist with aria-query roles and adds tokenized role validation. |
| tests/lib/rules/template-no-invalid-role.js | Updates expectations for per-token errors and adds new valid cases for doc-*, graphics-*, and fallback lists. |
| tests/audit/aria-role/peer-parity.js | Adds an audit fixture to compare behavior against peer ESLint accessibility plugins. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…Copilot review)
The PR body claimed the ARIA 1.3 draft allowlist covers 5 roles (associationlist,
associationlistitemkey, associationlistitemvalue, comment, suggestion). The
code only listed 2 ('comment', 'suggestion'); the gap was invisible because
no tests exercised any of them. Verified against aria-query 5.3.2:
roles.has() returns false for all 5, so all 5 belong in the inline allowlist
until aria-query catches up.
Also: when reporting presentation/none on a semantic element, include the
offending token in the message data instead of the raw role attribute
string — avoids surfacing e.g. 'presentation listbox' when only
'presentation' is the issue.
Tests: add 5 valid cases in each of the gts and hbs blocks covering all
ARIA 1.3 draft roles.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
49fafaa to
7e45da3
Compare
…the first recognised role (Copilot review) Bundle with the Q7/Q10/Q18/Q30 cross-rule first-valid-token pattern. Per WAI-ARIA §4.1, UAs walk the role-token list for the first role they recognise; subsequent tokens are author-provided fallbacks that never take effect. So `role="button presentation"` on a semantic element resolves to `button` at runtime and must NOT flag. Previously we flagged on any occurrence of presentation/none anywhere in the list. Unknown tokens are skipped per the same section, so `role="xxyxyz presentation"` correctly resolves to presentation and still flags (covered by a new regression test).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 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 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… message (Copilot review) Keep the original token alongside the lowercased one; validation stays case- insensitive (lowercases against VALID_ROLES) but the reported-back token uses the author's raw casing so the error surfaces their input verbatim rather than the normalized form. Also drop a 'line 229 of rule' hard-coded reference from the aria-role peer-parity fixture — line numbers rot on every refactor.
…t fixture Upstream maintainers don't want the per-PR `tests/audit/peer-parity` pattern. Port two cases that pinned distinct behavior: - `role=""` as VALID — documented divergence; we early-return on empty/whitespace role values where jsx-a11y / vue-a11y flag. - `role="datepicker"` as INVALID — common authoring confusion that exercises the unknown-role path with a more likely real-world typo than `role="invalid"`. Other audit cases were already covered by the regular tests.
…ibute Previously the rule early-returned when the trimmed role value was empty, silently accepting `<div role="">` and `<div role=" ">`. Both jsx-a11y and vue-a11y flag these — they're authoring mistakes, not no-ops, since the attribute is malformed (no recognized role token) and supplies zero ARIA semantics while still appearing in the DOM. Replace the early-return with a single 'invalid' report (role rendered as the empty string in the message), keeping the existing error format intact. No new messageId; the existing 'invalid' template renders the empty value clearly. Tests: move `role=""` from valid to invalid; add `role=" "` for parity with the trim-then-flag path. Documented as ports of the audit- fixture divergence cases that previously pinned the under-flag behavior.
…are-and-dialog fix: template-no-autofocus-attribute — value-aware + <dialog> exception
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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.[Mirror of ember-cli#2729 for Copilot review]
Two related fixes in one rewrite. Both were false positives that rejected documented-valid ARIA patterns.
1. DPUB-ARIA and Graphics-ARIA roles
doc-*(40+ roles) andgraphics-*(3 roles) tokens.aria-queryindexes them under itsrolesmap.VALID_ROLES(~90 tokens) covered only WAI-ARIA base roles.role="doc-abstract",role="graphics-document", etc., were flagged as invalid.Fix: derive
VALID_ROLESfromaria-query's concrete (non-abstract) role keys. Covers ~127 roles automatically.A small ARIA 1.3 draft allowlist (
associationlist,associationlistitemkey,associationlistitemvalue,comment,suggestion) is kept inline becausearia-querydoesn't yet ship those.2. Whitespace-separated role fallback lists
roleis a list of tokens. The UA picks the first one it recognises; the others are fallbacks.role="tabpanel row"as one opaque value —VALID_ROLES.has("tabpanel row")is false — and flagged the whole string as invalid.Fix: split on whitespace, validate each token. The error message now names the first offending token (
Invalid ARIA role 'foobar') rather than the whole string.Before/after
<div role="doc-abstract"><svg role="graphics-document"><div role="tabpanel row"><section role="doc-appendix doc-bibliography"><div role="tabpanel row foobar">'foobar')Three existing invalid tests updated to reflect the per-token error message; ten new valid tests cover the fixes.
Prior art
aria-rolearia-query'sroles.keys()directly;.split(' ')the value and checks each token against the set of concrete roles.aria-rolearia-roleisConcreteAriaRole, so multi-token fallback lists likerole="tabpanel row"fail.@angular-eslint/template'svalid-ariarule validatesaria-*attribute names and values rather thanroletokens, so it has no direct analog for the fallback-list fix.Audit fixture
Includes translated peer-plugin test fixture at
tests/audit/aria-role/peer-parity.jsdocumenting our rule's behavior relative to jsx-a11y / vuejs-accessibility / lit-a11y. Runs as part of the default Vitest suite (included via thetests/**/*.jsglob) and serves double-duty as an auditable peer-parity record and as regression coverage pinning current behavior.