Skip to content

Add template-no-aria-label-misuse: flag aria-label on roles with prohibited name-from-author#42

Draft
johanrd wants to merge 20 commits intomasterfrom
html-validate/template-no-aria-label-misuse
Draft

Add template-no-aria-label-misuse: flag aria-label on roles with prohibited name-from-author#42
johanrd wants to merge 20 commits intomasterfrom
html-validate/template-no-aria-label-misuse

Conversation

@johanrd
Copy link
Copy Markdown
Owner

@johanrd johanrd commented Apr 22, 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.

Summary

Add template-no-aria-label-misuse: flag aria-label / aria-labelledby on elements whose computed ARIA role prohibits an accessible name from author. Role resolution delegates to aria-query (already a dep) — the authoritative WAI-ARIA data package.

  • Premise 1: Per WAI-ARIA 1.2 §5.2.8.6 Roles which cannot be named (Name prohibited), roles such as generic, paragraph, presentation, code, emphasis, strong, etc. cannot be named by the author. aria-query encodes this as nameFrom being absent / not allowing author, and lists aria-label/aria-labelledby in prohibitedProps for 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.
  • Premise 2: aria-query exposes this as data: roles.get(role).prohibitedProps directly lists which props are forbidden for each role, and elementRoles provides the HTML-to-role mapping (including conditional entries like <section aria-label>region and <a href>link). Using this data avoids hand-coding a subset that inevitably drifts from the spec.
  • Conclusion: Resolve the element's role via aria-query (explicit role= wins; otherwise implicit via elementRoles with attribute-constraint matching). Look up the role in roles; if prohibitedProps includes aria-label or aria-labelledby, flag. Skip when no role can be resolved (unknown tag / component / aria-query gap) — "when in doubt, don't flag."

Fix

  • New rule lib/rules/template-no-aria-label-misuse.js; tests in tests/lib/rules/template-no-aria-label-misuse.js (64 cases).
  • getImplicitRole (template-no-aria-label-misuse.js:66-82) iterates aria-query.elementRoles and picks the most-specific matching entry by scoring attribute constraints (value > set > undefined).
  • Escape hatches: role="presentation" / role="none" skip (author removed from a11y tree); tabindex (any value) skips by default — rationale below.
  • Schema: { strictTabindex: boolean } (default false). When true, the tabindex escape hatch is disabled — teams that want strict ARIA-role enforcement can opt in.

Prior art

Plugin Equivalent Verified behavior
jsx-a11y No equivalent rule.
vuejs-accessibility aria-unsupported-elements Different concern — flags any aria-* on obsolete tags like <meta> / <html> / <script>.
lit-a11y aria-unsupported-elements Same as vue-a11y. Different concern.
@angular-eslint/template No equivalent rule.
html-validate aria-label-misusespec Hand-coded allowlist + metadata lookup (interactive, labelable, landmark). Our implementation uses aria-query's authoritative prohibitedProps instead.

Flags

<div aria-label='dialog'>x</div>              {{! role=generic }}
<span aria-labelledby='title'>x</span>        {{! role=generic }}
<p aria-label='note'>text</p>                 {{! role=paragraph }}
<a aria-label='missing href'>x</a>            {{! role=generic (no href) }}
<img aria-label='x' alt='' src='/y.png' />    {{! role=presentation via alt="" }}

Allows

<button aria-label='Close'>x</button>
<main aria-label='Primary'>...</main>
<section aria-label='About'>...</section>     {{! role transitions to region }}
<form aria-label='Search'>...</form>           {{! role transitions to form }}
<div role='button' aria-label='Custom'>...</div>
<span tabindex='0' aria-label='Focusable'>...</span>   {{! escape hatch }}

Notes

  • Authoritative data source: this is a behavioral change from my first-cut implementation, which used hand-coded "interactive + labelable + allowlist" tables. Delegating to aria-query caught real bugs the hand-coded version missed (e.g. <img alt=""> is spec-role presentation and prohibits aria-label) and dropped a few edge cases that the data source doesn't classify (e.g. <audio> without controls has no aria-query entry — we skip rather than guess).
  • Tabindex escape hatch — rationale. An element with any tabindex value has been put in the sequential focus navigation order by the author, indicating author-intent-to-interact even when its computed ARIA role is still generic. Flagging aria-label in 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 stray aria-label on 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.
  • Opt-in: not added to any preset config.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
js small 13.87 ms 13.88 ms +0.1%
🟢 js medium 9.53 ms 6.97 ms -26.9%
js large 2.84 ms 2.81 ms -1.1%
gjs small 1.25 ms 1.24 ms -0.5%
gjs medium 622.63 µs 614.32 µs -1.3%
gjs large 247.42 µs 245.96 µs -0.6%
gts small 1.22 ms 1.23 ms +0.8%
gts medium 613.53 µs 616.31 µs +0.5%
gts large 243.87 µs 245.97 µs +0.9%

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

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

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
js small (control)            16.66 ms/iter  17.53 ms █                    
                      (12.28 ms … 31.22 ms)  29.84 ms █ ▃                  
                    (  5.57 mb …  10.63 mb)   7.26 mb █▇█▅█▃▅▃▁▁▃▁▅▁▃▁▁▃▃▁▃

js small (experiment)         14.60 ms/iter  15.39 ms    ▅█      ▅         
                      (12.66 ms … 21.05 ms)  18.82 ms  ▃███▃     █         
                    (  6.75 mb …   7.87 mb)   6.84 mb ██████▄▄▄█▁█▄▄▄▁▁▁▁▁▄

                             ┌                                            ┐
                             ╷┌─────────┬─┐                               ╷
          js small (control) ├┤         │ ├───────────────────────────────┤
                             ╵└─────────┴─┘                               ╵
                              ╷ ┌──┬─┐        ╷
       js small (experiment)  ├─┤  │ ├────────┤
                              ╵ └──┴─┘        ╵
                             └                                            ┘
                             12.28 ms           21.06 ms           29.84 ms

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

------------------------------------------- -------------------------------
js medium (control)            9.43 ms/iter  10.36 ms █▂     ▄▂            
                       (6.82 ms … 15.84 ms)  15.80 ms ██     ██            
                    (  2.35 mb …   4.71 mb)   3.54 mb ███▇█▃▃███▅▃▃▃▁▁▅▃▁▃▃

js medium (experiment)         7.48 ms/iter   7.56 ms █▄                   
                       (6.58 ms … 14.47 ms)  13.95 ms ██                   
                    (  2.92 mb …   4.05 mb)   3.51 mb ███▇▅▃▃▁▁▁▂▁▁▁▁▁▂▂▁▁▂

                             ┌                                            ┐
                              ╷ ┌──────────┬───┐                          ╷
         js medium (control)  ├─┤          │   ├──────────────────────────┤
                              ╵ └──────────┴───┘                          ╵
                             ╷┌──┬┐                              ╷
      js medium (experiment) ├┤  │├──────────────────────────────┤
                             ╵└──┴┘                              ╵
                             └                                            ┘
                             6.58 ms           11.19 ms            15.80 ms

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

------------------------------------------- -------------------------------
js large (control)             3.29 ms/iter   3.02 ms  █                   
                       (2.63 ms … 10.78 ms)   7.74 ms ▇█                   
                    (302.58 kb …   2.65 mb)   1.45 mb ██▃▃▂▁▁▂▁▁▁▂▂▁▁▂▁▁▁▁▁

js large (experiment)          3.10 ms/iter   2.91 ms  █                   
                        (2.61 ms … 7.48 ms)   6.04 ms  █                   
                    (889.00 kb …   2.69 mb)   1.44 mb ▇█▄▂▂▂▁▂▁▂▁▂▁▁▁▁▂▁▁▁▁

                             ┌                                            ┐
                             ╷┌────┬                                      ╷
          js large (control) ├┤    │──────────────────────────────────────┤
                             ╵└────┴                                      ╵
                             ╷┌──┬                         ╷
       js large (experiment) ├┤  │─────────────────────────┤
                             ╵└──┴                         ╵
                             └                                            ┘
                             2.61 ms            5.17 ms             7.74 ms

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

------------------------------------------- -------------------------------
gjs small (control)            1.40 ms/iter   1.36 ms █                    
                        (1.21 ms … 6.70 ms)   5.97 ms █                    
                    (218.17 kb …   1.66 mb)   1.06 mb ██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.37 ms/iter   1.27 ms █                    
                        (1.20 ms … 6.87 ms)   5.94 ms █                    
                    (314.14 kb …   1.82 mb)   1.06 mb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                          ╷
         gjs small (control) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             ┌─┬                                          ╷
      gjs small (experiment) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             └                                            ┘
                             1.20 ms            3.58 ms             5.97 ms

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

------------------------------------------- -------------------------------
gjs medium (control)         674.47 µs/iter 639.44 µs █                    
                      (589.84 µs … 6.18 ms)   2.15 ms ██                   
                    ( 92.30 kb …   1.14 mb) 542.15 kb ██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      660.40 µs/iter 629.51 µs ▃█                   
                      (585.37 µs … 5.70 ms)   1.67 ms ██                   
                    ( 28.68 kb …   1.04 mb) 540.16 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌─┬                                         ╷
        gjs medium (control) ├┤ │─────────────────────────────────────────┤
                             ╵└─┴                                         ╵
                             ╷┌┬                            ╷
     gjs medium (experiment) ├┤│────────────────────────────┤
                             ╵└┴                            ╵
                             └                                            ┘
                             585.37 µs           1.37 ms            2.15 ms

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

------------------------------------------- -------------------------------
gjs large (control)          289.80 µs/iter 267.83 µs ▂█                   
                      (234.67 µs … 5.62 ms) 549.22 µs ██▅                  
                    (144.12 kb … 740.38 kb) 217.31 kb ███▄▂▂▂▁▁▁▁▁▁▁▁▁▂▂▁▂▁

gjs large (experiment)       268.06 µs/iter 259.33 µs  █▆                  
                      (235.44 µs … 4.99 ms) 357.95 µs ▂██                  
                    (184.17 kb … 836.00 kb) 216.62 kb ███▄██▅▂▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌──────┬                                    ╷
         gjs large (control) ├┤      │────────────────────────────────────┤
                             ╵└──────┴                                    ╵
                             ╷┌───┬            ╷
      gjs large (experiment) ├┤   │────────────┤
                             ╵└───┴            ╵
                             └                                            ┘
                             234.67 µs         391.94 µs          549.22 µs

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

------------------------------------------- -------------------------------
gts small (control)            1.32 ms/iter   1.24 ms █                    
                        (1.18 ms … 5.99 ms)   5.21 ms █                    
                    (222.25 kb …   1.49 mb)   1.06 mb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.31 ms/iter   1.25 ms █                    
                        (1.20 ms … 6.22 ms)   4.98 ms █                    
                    (337.19 kb …   1.79 mb)   1.05 mb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                          ╷
         gts small (control) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             ┌┬                                        ╷
      gts small (experiment) ││────────────────────────────────────────┤
                             └┴                                        ╵
                             └                                            ┘
                             1.18 ms            3.20 ms             5.21 ms

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

------------------------------------------- -------------------------------
gts medium (control)         659.48 µs/iter 626.76 µs █                    
                      (584.25 µs … 5.05 ms)   1.92 ms █▇                   
                    (262.71 kb …   1.29 mb) 542.00 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      660.75 µs/iter 629.99 µs █                    
                      (588.10 µs … 5.42 ms)   1.87 ms █▇                   
                    (539.30 kb …   1.08 mb) 541.40 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

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

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

------------------------------------------- -------------------------------
gts large (control)          269.12 µs/iter 258.67 µs  █                   
                      (233.63 µs … 5.49 ms) 365.85 µs  █▄                  
                    (170.50 kb … 739.44 kb) 216.81 kb ▆██▃█▆▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁

gts large (experiment)       271.87 µs/iter 259.13 µs  █▆                  
                      (234.03 µs … 5.70 ms) 359.18 µs  ██                  
                    (183.81 kb … 738.27 kb) 216.89 kb ▇██▅█▇▅▂▂▂▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌─────────┬                                ╷
         gts large (control) ├─┤         │────────────────────────────────┤
                             ╵ └─────────┴                                ╵
                             ╷ ┌──────────┬                             ╷
      gts large (experiment) ├─┤          │─────────────────────────────┤
                             ╵ └──────────┴                             ╵
                             └                                            ┘
                             233.63 µs         299.74 µs          365.85 µs

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

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

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-misuse rule that resolves explicit/implicit roles and reports prohibited labeling attributes.
  • Adds comprehensive test coverage for both .hbs and .gjs templates, including the strictTabindex option 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.

Comment thread lib/rules/template-no-aria-label-misuse.js Outdated
Comment thread lib/rules/template-no-aria-label-misuse.js
Comment thread docs/rules/template-no-aria-label-misuse.md 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 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.

Comment thread lib/rules/template-no-aria-label-misuse.js Outdated
Comment thread lib/rules/template-no-aria-label-misuse.js Outdated
Comment thread lib/rules/template-no-aria-label-misuse.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.

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.

Comment thread tests/lib/rules/template-no-aria-label-misuse.js
Comment thread lib/utils/is-native-element.js Outdated
Comment thread lib/utils/is-native-element.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.

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.

Comment thread lib/rules/template-no-aria-label-misuse.js
Comment thread lib/rules/template-no-aria-label-misuse.js
Comment thread lib/rules/template-no-aria-label-misuse.js
Comment thread lib/rules/template-no-aria-label-misuse.js
Comment thread lib/utils/is-native-element.js
@johanrd johanrd requested a review from Copilot April 24, 2026 17:47
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 added a commit that referenced this pull request Apr 26, 2026
@johanrd johanrd force-pushed the html-validate/template-no-aria-label-misuse branch from d3a79af to facd88d Compare April 26, 2026 08:12
@johanrd johanrd requested a review from Copilot April 26, 2026 08:42
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 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.

Comment thread lib/rules/template-no-aria-label-misuse.js
Comment thread lib/rules/template-no-aria-label-misuse.js
Comment thread tests/lib/rules/template-no-aria-label-misuse.js Outdated
johanrd added a commit that referenced this pull request Apr 27, 2026
@johanrd johanrd force-pushed the html-validate/template-no-aria-label-misuse branch from f34aa3f to 34a3f8c Compare April 27, 2026 14:01
@johanrd johanrd force-pushed the html-validate/template-no-aria-label-misuse branch from 7e3f338 to 19039d6 Compare April 27, 2026 19:32
@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 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.

Comment thread lib/rules/template-no-aria-label-misuse.js
Comment thread lib/rules/template-no-aria-label-misuse.js Outdated
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.
…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.
@johanrd johanrd force-pushed the html-validate/template-no-aria-label-misuse branch from 87f99f0 to 04e74fb Compare April 28, 2026 12:21
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