Add template-no-aria-label-misuse: flag aria-label on roles with prohibited name-from-author#42
Add template-no-aria-label-misuse: flag aria-label on roles with prohibited name-from-author#42
template-no-aria-label-misuse: flag aria-label on roles with prohibited name-from-author#42Conversation
🏎️ Benchmark Comparison
Full mitata output |
There was a problem hiding this comment.
Pull request overview
Adds a new Ember template accessibility lint rule to detect aria-label / aria-labelledby usage on elements whose computed ARIA role prohibits author-provided accessible names, using aria-query role metadata for resolution.
Changes:
- Introduces
template-no-aria-label-misuserule that resolves explicit/implicit roles and reports prohibited labeling attributes. - Adds comprehensive test coverage for both
.hbsand.gjstemplates, including thestrictTabindexoption behavior. - Documents the rule and lists it in the README Accessibility rules table.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
lib/rules/template-no-aria-label-misuse.js |
Implements role resolution via aria-query and reports prohibited label props, with escape hatches. |
tests/lib/rules/template-no-aria-label-misuse.js |
Adds rule tests for valid/invalid cases across hbs/gjs and config option coverage. |
docs/rules/template-no-aria-label-misuse.md |
Adds rule documentation, behavior notes, and configuration details. |
README.md |
Adds the new rule to the auto-generated Accessibility rules list. |
💡 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 4 out of 4 changed files in this pull request and generated 3 comments.
💡 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 5 out of 5 changed files in this pull request and generated 3 comments.
💡 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 5 out of 5 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
d3a79af to
facd88d
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
f34aa3f to
34a3f8c
Compare
Prepare Release v13.1.4
…pt-in requireExplicit
7e3f338 to
19039d6
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…-require-input-type feat: add template-require-input-type
Prepare Release v13.2.0
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.
…th prohibited name-from-author
…tatic attr detection
- Trim text-node chars in hasNonEmptyLabelAttr so aria-label=" " (whitespace only)
is treated as empty rather than non-empty.
- Extend getStaticAttrString to return '' for valueless attributes (e.g. <img alt>)
and return the literal value for mustache string-literal paths (e.g. alt={{""}}),
so constraint matching for alt="" → presentation role works correctly.
87f99f0 to
04e74fb
Compare
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.Summary
Add
template-no-aria-label-misuse: flagaria-label/aria-labelledbyon elements whose computed ARIA role prohibits an accessible name from author. Role resolution delegates toaria-query(already a dep) — the authoritative WAI-ARIA data package.generic,paragraph,presentation,code,emphasis,strong, etc. cannot be named by the author.aria-queryencodes this asnameFrombeing absent / not allowingauthor, and listsaria-label/aria-labelledbyinprohibitedPropsfor those roles. On such roles, assistive tech ignores author-provided labels — the attribute is silently useless and usually indicates the author expected the element to be reachable to AT.aria-queryexposes this as data:roles.get(role).prohibitedPropsdirectly lists which props are forbidden for each role, andelementRolesprovides the HTML-to-role mapping (including conditional entries like<section aria-label>→regionand<a href>→link). Using this data avoids hand-coding a subset that inevitably drifts from the spec.aria-query(explicitrole=wins; otherwise implicit viaelementRoleswith attribute-constraint matching). Look up the role inroles; ifprohibitedPropsincludesaria-labeloraria-labelledby, flag. Skip when no role can be resolved (unknown tag / component / aria-query gap) — "when in doubt, don't flag."Fix
lib/rules/template-no-aria-label-misuse.js; tests intests/lib/rules/template-no-aria-label-misuse.js(64 cases).getImplicitRole(template-no-aria-label-misuse.js:66-82) iteratesaria-query.elementRolesand picks the most-specific matching entry by scoring attribute constraints (value>set>undefined).role="presentation"/role="none"skip (author removed from a11y tree);tabindex(any value) skips by default — rationale below.{ strictTabindex: boolean }(defaultfalse). Whentrue, the tabindex escape hatch is disabled — teams that want strict ARIA-role enforcement can opt in.Prior art
aria-unsupported-elementsaria-*on obsolete tags like<meta>/<html>/<script>.aria-unsupported-elementsaria-label-misuse— specaria-query's authoritativeprohibitedPropsinstead.Flags
Allows
Notes
aria-querycaught real bugs the hand-coded version missed (e.g.<img alt="">is spec-rolepresentationand prohibitsaria-label) and dropped a few edge cases that the data source doesn't classify (e.g.<audio>withoutcontrolshas no aria-query entry — we skip rather than guess).tabindexvalue has been put in the sequential focus navigation order by the author, indicating author-intent-to-interact even when its computed ARIA role is stillgeneric. Flaggingaria-labelin this case has a high false-positive cost (the author wants the label read on focus) relative to the true-positive it would catch (a strayaria-labelon a non-interactive element). Users who want strict spec-role enforcement can override the rule locally. We have not systematically tested screen-reader behavior for this case; the hatch is motivated by author-intent, not by claimed AT support.