Skip to content

Commit 110e21e

Browse files
committed
fix(template-no-aria-hidden-on-focusable): align with ARIA 1.2 aria-hidden default
Per WAI-ARIA 1.2 §6.6 + aria-hidden value table, a missing or empty-string aria-hidden resolves to default `undefined` — NOT `true`. Valueless `<button aria-hidden>` and empty `<button aria-hidden="">` are therefore NOT spec-hidden; they do not create a focus-trap anti-pattern and the rule should not flag them. The prior behavior inherited jsx-a11y's convention (jsx-ast-utils coerces valueless JSX attrs to boolean true) and vue-a11y's "anything-not-literal-false" shortcut. Both are peer-plugin conventions, not normative ARIA interpretations. Matching ember-cli#2717's spec-first resolution. Also corrects the rule-doc comment: the claim attributed to WAI-ARIA 1.2 ("Authors SHOULD NOT use aria-hidden='true' on any element that has focus or may receive focus") is not in the WAI-ARIA spec. The spec only says authors MAY "with caution" use aria-hidden. The rule's concern (keyboard trap) comes from community/axe guidance, which this comment now accurately attributes. Net: flagged values are now `aria-hidden="true"` (ASCII case-insensitive), `aria-hidden={{true}}`, and `aria-hidden={{"true"}}`. Valueless, empty, `false`, and `{{false}}` are all accepted.
1 parent cfd4c86 commit 110e21e

2 files changed

Lines changed: 28 additions & 24 deletions

File tree

lib/rules/template-no-aria-hidden-on-focusable.js

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,25 @@ function getTextAttrValue(node, name) {
1515
return undefined;
1616
}
1717

18-
function isAriaHiddenTruthy(node) {
19-
const attr = findAttr(node, 'aria-hidden');
20-
if (!attr) {
18+
// Per WAI-ARIA 1.2 §6.6 + aria-hidden value table, a missing or empty-string
19+
// aria-hidden resolves to the default `undefined` — NOT `true`. So only an
20+
// explicit `"true"` (ASCII case-insensitive per HTML enumerated-attribute
21+
// rules) hides the element. Mustache boolean-literal `{{true}}` and
22+
// string-literal `{{"true"}}` also qualify.
23+
function isAriaHiddenTrue(node) {
24+
const value = findAttr(node, 'aria-hidden')?.value;
25+
if (!value) {
2126
return false;
2227
}
23-
const value = attr.value;
24-
// Valueless or empty-string → truthy boolean attr. Matches jsx-a11y/vue-a11y.
25-
if (!value || (value.type === 'GlimmerTextNode' && value.chars === '')) {
26-
return true;
27-
}
2828
if (value.type === 'GlimmerTextNode') {
29-
return value.chars === 'true';
29+
return value.chars.toLowerCase() === 'true';
3030
}
3131
if (value.type === 'GlimmerMustacheStatement' && value.path) {
3232
if (value.path.type === 'GlimmerBooleanLiteral') {
3333
return value.path.value === true;
3434
}
3535
if (value.path.type === 'GlimmerStringLiteral') {
36-
return value.path.value === 'true';
36+
return value.path.value.toLowerCase() === 'true';
3737
}
3838
}
3939
return false;
@@ -62,11 +62,14 @@ function isFocusable(node) {
6262
return isNativeInteractive(node, getTextAttrValue);
6363
}
6464

65-
// Per WAI-ARIA 1.2 §aria-hidden: "Authors SHOULD NOT use aria-hidden='true' on
66-
// any element that has focus or may receive focus". A focusable descendant of
67-
// an aria-hidden ancestor can still receive focus (aria-hidden does not remove
68-
// elements from the tab order), so the ancestor hides AT-visible content that
69-
// remains keyboard-reachable — a keyboard trap.
65+
// A focusable descendant of an aria-hidden="true" ancestor can still receive
66+
// focus (aria-hidden does not remove elements from the tab order), so the
67+
// ancestor hides AT-visible content that remains keyboard-reachable — a
68+
// keyboard trap. This rule targets the anti-pattern flagged by axe's
69+
// `aria-hidden-focus` check and by jsx-a11y's `no-aria-hidden-on-focusable`;
70+
// the WAI-ARIA 1.2 spec itself only says authors MAY "with caution" use
71+
// aria-hidden, so the rule rests on community a11y guidance, not a
72+
// normative WAI-ARIA MUST-NOT.
7073
function hasFocusableDescendant(node) {
7174
const children = node.children;
7275
if (!children || children.length === 0) {
@@ -115,7 +118,7 @@ module.exports = {
115118
create(context) {
116119
return {
117120
GlimmerElementNode(node) {
118-
if (!isAriaHiddenTruthy(node)) {
121+
if (!isAriaHiddenTrue(node)) {
119122
return;
120123
}
121124
if (isFocusable(node)) {

tests/lib/rules/template-no-aria-hidden-on-focusable.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ ruleTester.run('template-no-aria-hidden-on-focusable', rule, {
2323
// aria-hidden="false" — explicit opt-out. Not flagged.
2424
'<template><button aria-hidden="false">Click me</button></template>',
2525

26+
// Valueless / empty aria-hidden resolves to default `undefined` per
27+
// WAI-ARIA 1.2 §6.6 — not hidden, not flagged even on focusable hosts.
28+
'<template><button aria-hidden>Click me</button></template>',
29+
'<template><button aria-hidden="">Click me</button></template>',
30+
'<template><button aria-hidden={{false}}>Click me</button></template>',
31+
2632
// <input type="hidden"> isn't focusable, so aria-hidden on it is fine.
2733
'<template><input type="hidden" aria-hidden="true" /></template>',
2834

@@ -95,19 +101,14 @@ ruleTester.run('template-no-aria-hidden-on-focusable', rule, {
95101
errors: [{ messageId: 'noAriaHiddenOnFocusable' }],
96102
},
97103

98-
// Boolean / valueless / mustache-boolean aria-hidden — all truthy.
104+
// Mustache-boolean + case-variant aria-hidden = true — truthy per spec.
99105
{
100-
code: '<template><button aria-hidden></button></template>',
101-
output: null,
102-
errors: [{ messageId: 'noAriaHiddenOnFocusable' }],
103-
},
104-
{
105-
code: '<template><button aria-hidden=""></button></template>',
106+
code: '<template><button aria-hidden={{true}}></button></template>',
106107
output: null,
107108
errors: [{ messageId: 'noAriaHiddenOnFocusable' }],
108109
},
109110
{
110-
code: '<template><button aria-hidden={{true}}></button></template>',
111+
code: '<template><button aria-hidden="TRUE"></button></template>',
111112
output: null,
112113
errors: [{ messageId: 'noAriaHiddenOnFocusable' }],
113114
},

0 commit comments

Comments
 (0)