Skip to content

Add template-no-aria-hidden-focusable: flag aria-hidden on focusable elements and ancestors#41

Closed
johanrd wants to merge 7 commits into
masterfrom
html-validate/template-no-aria-hidden-focusable
Closed

Add template-no-aria-hidden-focusable: flag aria-hidden on focusable elements and ancestors#41
johanrd wants to merge 7 commits into
masterfrom
html-validate/template-no-aria-hidden-focusable

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-hidden-focusable: flag aria-hidden="true" on a focusable element, and on an element whose focusable descendant is in the same template. Matches the existing jsx-a11y / vuejs-accessibility rule name (peers check self only; we also walk descendants within the template).

  • Premise 1: The W3C "Using ARIA" 4th Rule states normatively: "Do not use role='presentation' or aria-hidden='true' on a focusable element." — with the rationale that doing so "will result in some users focusing on 'nothing'." The W3C ACT rule 6cfa84 ("Element with aria-hidden has no content in sequential focus navigation") operationalizes this: a focusable descendant of an aria-hidden="true" subtree is a conformance failure because the element is reachable via Tab but removed from the accessibility tree.
  • Premise 2: WAI-ARIA 1.2's aria-hidden section itself uses MAY-level language ("MAY, with caution, use aria-hidden to hide visibly rendered content") and does not contain the MUST-NOT phrasing sometimes attributed to it. The authoritative normative statement against hiding focusable content lives in Using ARIA (W3C Working Group Note) and is codified by the WCAG 2.1 failure techniques F70 / F86 and axe-core's aria-hidden-focus rule. jsx-a11y and vue-a11y also implement it (self-only), though their implementations miss the ancestor-descendant case that "Using ARIA" and the ACT rule explicitly cover.
  • Conclusion: Flag aria-hidden="true" on any element that is focusable, and on any element that contains a focusable descendant within the same template. Skip mustache-unknown aria-hidden / tabindex values. Walking descendants is the spec-grounded behavior (peers' self-only check is incomplete); the scope caveat is template-file-only visibility, not an opinion about ARIA.

Fix

  • New rule lib/rules/template-no-aria-hidden-focusable.js; tests in tests/lib/rules/template-no-aria-hidden-focusable.js (56 cases).
  • isAriaHiddenTruthy (template-no-aria-hidden-focusable.js:19-42) — classifies the attribute; valueless and bare true / "true" count as hidden; {{false}} / "false" / mustache-unknown are not.
  • isFocusable (template-no-aria-hidden-focusable.js:80-88) — hand-coded focusability: inherently focusable tags (button, select, textarea, iframe, summary, details), <a href>, <input> (not hidden), media with controls, contenteditable, and explicit tabindex >= 0. Mustache tabindex={{...}} is neither focusable nor non-focusable — skipped.
  • walkElementDescendants (template-no-aria-hidden-focusable.js:141-151) — recurses into same-template GlimmerElementNode children only. Does not cross component invocation or {{yield}} boundaries.

Prior art

Plugin Equivalent Verified behavior
jsx-a11y no-aria-hidden-on-focusable Flags aria-hidden="true" only on the element itself; reuses isFocusable util. Does not walk descendants.
vuejs-accessibility no-aria-hidden-on-focusable Same as jsx-a11y; self-only.
lit-a11y No equivalent rule.
@angular-eslint/template No equivalent rule.
html-validate hidden-focusablespec Flags self + any focusable descendant (uses a full DOM walk with metadata). Our template-scoped rule is a subset of this behavior.

Flags

<button aria-hidden='true'>Submit</button>
<a href='/x' aria-hidden='true'>Link</a>

<div aria-hidden='true'>
  <button>Hidden but still tabbable</button>
</div>

<div aria-hidden>
  <a href='/profile'>Profile</a>       {{! valueless aria-hidden: truthy }}
</div>

Allows

{{! Decorative subtree — no focusable descendants }}
<div aria-hidden='true'>
  <span>Decorative text only</span>
</div>

{{! Explicit tabindex=-1 removes the tab stop, so hiding is consistent }}
<button aria-hidden='true' tabindex='-1'>Not tabbable</button>

{{! Dynamic aria-hidden — skipped }}
<div aria-hidden={{this.hidden}}>
  <button>ok</button>
</div>

{{! Component invocation hides its subtree from our check (scope caveat) }}
<div aria-hidden='true'>
  <MyComponent />
</div>

Notes

  • Scope caveat: our descendant walk stops at component invocations and {{yield}} boundaries. A focusable element rendered by a child component under aria-hidden won't be flagged. The alternative (flag every component under aria-hidden) produced unacceptable noise in early testing. This trade matches the plugin's general "fewer false positives" stance.
  • Self-vs-descendants is where we diverge from jsx-a11y / vue-a11y. Matching html-validate, we walk the subtree because hiding a container from AT while keeping its focusable children reachable is a common real-world accessibility bug that the self-only check misses.
  • 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.81 ms 13.83 ms +0.1%
🟢 js medium 6.98 ms 6.74 ms -3.5%
🟢 js large 2.77 ms 2.69 ms -2.9%
gjs small 1.21 ms 1.23 ms +1.4%
gjs medium 611.82 µs 609.89 µs -0.3%
gjs large 243.16 µs 243.47 µs +0.1%
gts small 1.23 ms 1.23 ms -0.6%
gts medium 611.00 µs 613.42 µs +0.4%
gts large 242.47 µs 244.52 µs +0.8%

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

Full mitata output
clk: ~3.09 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.53 ms/iter  17.39 ms █                    
                      (11.96 ms … 31.03 ms)  28.21 ms █▃█                  
                    (  5.58 mb …  10.42 mb)   7.26 mb ███▆█▄▆▆▄▁▁▁▆▁▁▁▄▄▄▁▆

js small (experiment)         14.35 ms/iter  15.19 ms      █               
                      (12.48 ms … 18.33 ms)  17.78 ms ▆ ▃▆██ ▃       ▃     
                    (  6.20 mb …   8.40 mb)   6.84 mb █▄██████▄▁█▄▁█▄██▁▁▁▄

                             ┌                                            ┐
                             ╷ ┌──────────┬─┐                             ╷
          js small (control) ├─┤          │ ├─────────────────────────────┤
                             ╵ └──────────┴─┘                             ╵
                              ╷  ┌──┬─┐      ╷
       js small (experiment)  ├──┤  │ ├──────┤
                              ╵  └──┴─┘      ╵
                             └                                            ┘
                             11.96 ms           20.08 ms           28.21 ms

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

------------------------------------------- -------------------------------
js medium (control)            7.65 ms/iter   7.96 ms  █                   
                       (6.50 ms … 12.63 ms)  12.55 ms ▂██                  
                    (  2.72 mb …   4.35 mb)   3.53 mb ███▃▃▃▃▃▂▁▅▂▂▁▃▁▃▂▁▁▂

js medium (experiment)         7.38 ms/iter   7.35 ms  █                   
                       (6.33 ms … 13.49 ms)  13.45 ms  █                   
                    (  2.33 mb …   4.69 mb)   3.51 mb ███▄▃▃▃▁▁▂▃▁▂▁▁▁▂▁▂▁▂

                             ┌                                            ┐
                              ╷┌─────┬─┐                            ╷
         js medium (control)  ├┤     │ ├────────────────────────────┤
                              ╵└─────┴─┘                            ╵
                             ╷ ┌────┬                                     ╷
      js medium (experiment) ├─┤    │─────────────────────────────────────┤
                             ╵ └────┴                                     ╵
                             └                                            ┘
                             6.33 ms            9.89 ms            13.45 ms

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

------------------------------------------- -------------------------------
js large (control)             3.17 ms/iter   2.92 ms  █                   
                       (2.39 ms … 10.06 ms)   7.83 ms ▇█▃                  
                    ( 73.10 kb …   3.09 mb)   1.44 mb ███▂▅▂▁▂▂▂▁▂▂▂▂▁▁▁▂▁▁

js large (experiment)          2.91 ms/iter   2.76 ms  █                   
                        (2.48 ms … 5.99 ms)   5.79 ms ▃█                   
                    (659.69 kb …   2.27 mb)   1.42 mb ██▅▂▂▂▂▁▁▂▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌───┬                                      ╷
          js large (control) ├─┤   │──────────────────────────────────────┤
                             ╵ └───┴                                      ╵
                              ╷┌─┬                       ╷
       js large (experiment)  ├┤ │───────────────────────┤
                              ╵└─┴                       ╵
                             └                                            ┘
                             2.39 ms            5.11 ms             7.83 ms

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

------------------------------------------- -------------------------------
gjs small (control)            1.34 ms/iter   1.29 ms █                    
                        (1.18 ms … 6.68 ms)   4.91 ms █                    
                    (220.49 kb …   2.34 mb)   1.06 mb ██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.35 ms/iter   1.26 ms █                    
                        (1.19 ms … 6.57 ms)   4.86 ms █                    
                    (403.34 kb …   1.72 mb)   1.06 mb █▅▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                          ╷
         gjs small (control) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             ┌─┬                                         ╷
      gjs small (experiment) │ │─────────────────────────────────────────┤
                             └─┴                                         ╵
                             └                                            ┘
                             1.18 ms            3.04 ms             4.91 ms

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

------------------------------------------- -------------------------------
gjs medium (control)         662.80 µs/iter 628.58 µs █                    
                      (584.70 µs … 4.88 ms)   3.45 ms █                    
                    ( 72.30 kb …   1.25 mb) 542.33 kb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      653.07 µs/iter 624.60 µs  █                   
                      (582.53 µs … 5.15 ms)   1.55 ms ▆█                   
                    ( 28.30 kb …   1.47 mb) 540.86 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
        gjs medium (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌┬             ╷
     gjs medium (experiment) ││─────────────┤
                             └┴             ╵
                             └                                            ┘
                             582.53 µs           2.02 ms            3.45 ms

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

------------------------------------------- -------------------------------
gjs large (control)          263.10 µs/iter 258.71 µs  █                   
                      (234.22 µs … 4.49 ms) 344.76 µs  ██                  
                    (170.92 kb … 836.72 kb) 217.26 kb ███▃▄█▆▂▁▂▁▁▁▁▁▁▁▁▁▁▁

gjs large (experiment)       264.96 µs/iter 258.74 µs  █▂                  
                      (233.62 µs … 4.89 ms) 338.40 µs  ██                  
                    (170.17 kb … 965.80 kb) 216.62 kb ▅██▄▃▇▆▄▂▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌─────────┬                                ╷
         gjs large (control) ├─┤         │────────────────────────────────┤
                             ╵ └─────────┴                                ╵
                             ╷ ┌──────────┬                            ╷
      gjs large (experiment) ├─┤          │────────────────────────────┤
                             ╵ └──────────┴                            ╵
                             └                                            ┘
                             233.62 µs         289.19 µs          344.76 µs

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

------------------------------------------- -------------------------------
gts small (control)            1.33 ms/iter   1.26 ms █                    
                        (1.20 ms … 5.99 ms)   4.49 ms █                    
                    (468.05 kb …   1.66 mb)   1.06 mb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.31 ms/iter   1.24 ms █                    
                        (1.19 ms … 5.85 ms)   4.91 ms █                    
                    (504.58 kb …   1.63 mb)   1.05 mb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                     ╷
         gts small (control) │ │─────────────────────────────────────┤
                             └─┴                                     ╵
                             ┌─┬                                          ╷
      gts small (experiment) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             └                                            ┘
                             1.19 ms            3.05 ms             4.91 ms

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

------------------------------------------- -------------------------------
gts medium (control)         655.34 µs/iter 623.68 µs  █                   
                      (582.53 µs … 5.16 ms)   1.39 ms ▄█                   
                    (309.48 kb …   1.20 mb) 542.23 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      660.05 µs/iter 627.02 µs █                    
                      (583.69 µs … 5.09 ms)   1.96 ms █▅                   
                    (495.50 kb …   1.07 mb) 541.34 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌┬                       ╷
        gts medium (control) ├┤│───────────────────────┤
                             ╵└┴                       ╵
                             ╷┌─┬                                         ╷
     gts medium (experiment) ├┤ │─────────────────────────────────────────┤
                             ╵└─┴                                         ╵
                             └                                            ┘
                             582.53 µs           1.27 ms            1.96 ms

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

------------------------------------------- -------------------------------
gts large (control)          266.48 µs/iter 258.67 µs  █                   
                      (233.45 µs … 5.52 ms) 344.04 µs ▂█▇                  
                    (216.09 kb … 836.37 kb) 217.03 kb ███▅▃█▆▃▂▂▁▁▁▁▁▁▁▁▁▁▁

gts large (experiment)       266.93 µs/iter 260.50 µs  █                   
                      (235.57 µs … 4.85 ms) 351.38 µs  █▇                  
                    ( 13.85 kb … 769.88 kb) 216.44 kb ███▃▄▇▆▂▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌──────────┬                            ╷
         gts large (control) ├─┤          │────────────────────────────┤
                             ╵ └──────────┴                            ╵
                              ╷┌──────────┬                               ╷
      gts large (experiment)  ├┤          │───────────────────────────────┤
                              ╵└──────────┴                               ╵
                             └                                            ┘
                             233.45 µs         292.41 µs          351.38 µs

summary
  gts large (control)
   1x 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 rule to detect aria-hidden="true" applied to focusable elements and to ancestors of focusable elements (within the same template), aligning with “aria-hidden on focusable” guidance and similar ecosystem rule names.

Changes:

  • Added template-no-aria-hidden-focusable rule implementation with focusability and descendant-walk logic.
  • Added comprehensive rule documentation and a README rules-list entry.
  • Added RuleTester coverage for both .hbs and <template> (GJS) parsing modes.

Reviewed changes

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

File Description
lib/rules/template-no-aria-hidden-focusable.js Implements the new rule (aria-hidden truthiness, focusability detection, descendant scanning, reporting).
tests/lib/rules/template-no-aria-hidden-focusable.js Adds valid/invalid cases for .hbs and <template> wrapped inputs.
docs/rules/template-no-aria-hidden-focusable.md Documents rule intent, scope caveats, examples, and references.
README.md Adds the rule to the auto-generated rules list table.

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

Comment thread lib/rules/template-no-aria-hidden-focusable.js
Comment thread lib/rules/template-no-aria-hidden-focusable.js
Comment thread lib/rules/template-no-aria-hidden-focusable.js
Comment thread lib/rules/template-no-aria-hidden-focusable.js Outdated
Comment thread tests/lib/rules/template-no-aria-hidden-focusable.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

Adds a new Ember template accessibility rule to catch cases where aria-hidden hides focusable content within the same template, aligning behavior with ACT/axe guidance while remaining template-scoped.

Changes:

  • Introduces new rule template-no-aria-hidden-focusable to flag aria-hidden on focusable elements and on ancestors with focusable descendants (within-template walk).
  • Adds rule docs + README rule index entry.
  • Adds HBS + GJS RuleTester coverage for valid/invalid scenarios.

Reviewed changes

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

Show a summary per file
File Description
tests/lib/rules/template-no-aria-hidden-focusable.js Adds comprehensive test matrix for the new rule in both HBS and GJS modes.
lib/utils/is-native-element.js Introduces a native-element detection helper with scope-shadowing awareness.
lib/rules/template-no-aria-hidden-focusable.js Implements the new rule, including aria-hidden truthiness + focusability classification + descendant walk.
docs/rules/template-no-aria-hidden-focusable.md Documents rule intent, scope caveat, examples, and references.
README.md Adds the rule to the public rule list.

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

Comment thread lib/rules/template-no-aria-hidden-focusable.js Outdated
Comment thread lib/rules/template-no-aria-hidden-focusable.js
Comment thread lib/utils/is-native-element.js
Comment thread lib/rules/template-no-aria-hidden-focusable.js
@johanrd johanrd force-pushed the html-validate/template-no-aria-hidden-focusable branch from 02b03f9 to 52f2aa1 Compare April 22, 2026 17:12
johanrd added a commit that referenced this pull request Apr 23, 2026
@johanrd johanrd requested a review from Copilot April 24, 2026 08:37
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

This PR adds a new opt-in Ember template accessibility rule, template-no-aria-hidden-focusable, to detect cases where aria-hidden is applied to focusable content (either directly or via an aria-hidden ancestor within the same template scope).

Changes:

  • Added template-no-aria-hidden-focusable rule that reports aria-hidden on focusable elements and aria-hidden ancestors containing focusable descendants (template-scoped walk).
  • Added comprehensive RuleTester coverage for both GJS (<template>…</template>) and standalone HBS parsing.
  • Added end-user documentation and listed the rule in the README rules table.

Reviewed changes

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

Show a summary per file
File Description
lib/rules/template-no-aria-hidden-focusable.js Implements focusability detection, aria-hidden truthiness parsing, and descendant walking/reporting.
tests/lib/rules/template-no-aria-hidden-focusable.js Adds valid/invalid cases covering self + ancestor/descendant reporting for both HBS and GJS.
lib/utils/is-native-element.js Provides a shared helper to distinguish native elements from component invocations (including scope-shadowing).
docs/rules/template-no-aria-hidden-focusable.md Documents rule behavior, scope caveats, examples, fixes, and references.
README.md Adds the rule to the Accessibility rules list.

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

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

Adds a new opt-in accessibility rule to catch aria-hidden applied to focusable elements (including when the focusable element is a descendant of an aria-hidden subtree) in Ember templates.

Changes:

  • Introduce template-no-aria-hidden-focusable rule with descendant walking limited to same-template native elements.
  • Add comprehensive test coverage for both .gjs <template> and .hbs parsing modes.
  • Document the rule and list it in the README rules table.

Reviewed changes

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

Show a summary per file
File Description
lib/rules/template-no-aria-hidden-focusable.js Implements the rule logic: aria-hidden truthiness parsing, focusability detection, and descendant traversal with deduped reports.
tests/lib/rules/template-no-aria-hidden-focusable.js Adds valid/invalid cases for self + ancestor/descendant scenarios across HBS and GJS parsing.
lib/utils/is-native-element.js Adds/updates a utility for native element detection with scope-shadowing awareness.
docs/rules/template-no-aria-hidden-focusable.md Documents intent, scope caveats, examples, and references.
README.md Adds the rule to the Accessibility rules list.

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

Comment thread lib/rules/template-no-aria-hidden-focusable.js
Comment thread lib/rules/template-no-aria-hidden-focusable.js Outdated
Comment thread lib/utils/is-native-element.js
johanrd added a commit that referenced this pull request Apr 24, 2026
… via shared helper (Copilot review)

Extract a new `getStaticAttrValue` util that resolves literal-valued
mustaches (`{{"foo"}}`, `{{true}}`, `{{-1}}`) and single-part concat
statements (`"{{true}}"`) to their static string value. `isAriaHiddenTruthy`
now delegates to the helper and compares the resolved string to `'true'`
(case-insensitive, whitespace-trimmed).

Behavior change: valueless `<h1 aria-hidden>`, `aria-hidden=""`, and the
mustache-empty-string equivalents (`aria-hidden={{""}}`, `aria-hidden="{{""}}"`,
`aria-hidden={{" "}}`) are no longer treated as hidden. Per WAI-ARIA 1.2
§6.6 value table, those shapes resolve to the default `undefined` — NOT
`true` — so the empty-content check still applies. Drops the previous
"fewer false positives" deviation rationale in favour of spec-literal
consistency with sibling rules (#19, #35, #41) that share the same
aria-hidden resolution.

Byte-identical carrier of lib/utils/static-attr-value.js across all PRs
that land it.
@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 johanrd force-pushed the html-validate/template-no-aria-hidden-focusable branch from 4c99210 to 8c3af11 Compare April 26, 2026 08:11
@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.

@johanrd johanrd requested a review from Copilot April 27, 2026 11:19
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 4 comments.


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

Comment thread tests/lib/rules/template-no-aria-hidden-focusable.js
Comment thread lib/rules/template-no-aria-hidden-focusable.js Outdated
Comment thread lib/rules/template-no-aria-hidden-focusable.js
Comment thread lib/rules/template-no-aria-hidden-focusable.js Outdated
johanrd added 7 commits April 27, 2026 16:00
Ports html-validate's hidden-focusable. Reports aria-hidden=true on focusable
elements and on elements that contain a focusable descendant (walked within
the current template only — component/yield boundaries are out of scope).

Focusability is approximated without MDN-scale metadata: buttons, select,
textarea, iframe, details/summary, links with href, non-hidden inputs,
media with controls, contenteditable, and explicit tabindex>=0.

Logic adapted from html-validate (MIT).
WAI-ARIA 1.2's aria-hidden section uses MAY-level language and does not
contain a MUST-NOT against hiding focusable content. The authoritative
normative statement is W3C "Using ARIA" 4th Rule: "Do not use
role='presentation' or aria-hidden='true' on a focusable element."

Expanded References to: Using ARIA 4th Rule, ACT rule 6cfa84, axe-core
aria-hidden-focus, WCAG F70/F86 failure techniques, and WAI-ARIA 1.2 as the
attribute definition (not the prohibition).
… handling; add GJS shadow test

- isImplicitlyFocusable now calls isNativeElement(node, sourceCode) instead of
  the heuristic isHtmlElementNode, so GJS/GTS scope-shadowed lowercase tags are
  not mistaken for native elements; sourceCode is threaded through isFocusable
- isContentEditable recognises GlimmerMustacheStatement with BooleanLiteral and
  StringLiteral paths, so contenteditable={{true}} / contenteditable={{"true"}}
  are correctly identified as focusable
- isAriaHiddenTruthy no longer treats empty-string as truthy; per WAI-ARIA 1.2
  only the explicit string "true" enables aria-hidden, so bare aria-hidden and
  aria-hidden="" are no longer flagged; tests updated accordingly
- Adds a GJS-only valid test case where a lowercase tag is bound to a component
  in scope, confirming it is not flagged as a native focusable element
@johanrd johanrd force-pushed the html-validate/template-no-aria-hidden-focusable branch from 42f149e to a75d262 Compare April 27, 2026 14:01
@johanrd
Copy link
Copy Markdown
Owner Author

johanrd commented Apr 27, 2026

Closing in favour of #19 (template-no-aria-hidden-on-focusable), which implements the same rule — including descendant walking — under the clearer name. #19 is already non-draft and uses the shared getStaticAttrValue util.

@johanrd johanrd closed this Apr 27, 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