Skip to content

Commit 2529828

Browse files
committed
fix(template-no-empty-headings): recognize boolean aria-hidden as hidden
Before: isHidden only matched aria-hidden="true" as a string literal. Boolean / valueless / empty / mustache forms (<h1 aria-hidden />, <h1 aria-hidden="" />, <h1 aria-hidden={{true}} />) slipped past as "not hidden", so empty headings in those forms were flagged as empty even when the author had intentionally hidden them from AT. Fix: extract isAriaHiddenTruthy(). Recognize: - valueless attribute (HBS AST has value=null or empty-string TextNode) - "true" string literal (preserved) - "" empty string - {{true}} boolean mustache literal - {{"true"}} string mustache literal Per HTML boolean-attribute semantics (and jsx-a11y/vue-a11y convention), presence of aria-hidden without an explicit "false" value is treated as truthy. The strict ARIA spec treats bare aria-hidden as "undefined" rather than "true", but every major linter in the ecosystem (and most screen readers) treats it as true. Four new test cases covering each of the recognized forms.
1 parent 24882a3 commit 2529828

2 files changed

Lines changed: 33 additions & 5 deletions

File tree

lib/rules/template-no-empty-headings.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,37 @@
11
const HEADINGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
22

3+
function isAriaHiddenTruthy(attr) {
4+
if (!attr) {
5+
return false;
6+
}
7+
const value = attr.value;
8+
// Valueless or empty-string attribute — <h1 aria-hidden />. Per HTML boolean
9+
// attribute semantics (and jsx-a11y/vue-a11y convention), presence = truthy.
10+
if (!value || (value.type === 'GlimmerTextNode' && value.chars === '')) {
11+
return true;
12+
}
13+
if (value.type === 'GlimmerTextNode') {
14+
return value.chars === 'true';
15+
}
16+
if (value.type === 'GlimmerMustacheStatement' && value.path) {
17+
if (value.path.type === 'GlimmerBooleanLiteral') {
18+
return value.path.value === true;
19+
}
20+
if (value.path.type === 'GlimmerStringLiteral') {
21+
return value.path.value === 'true';
22+
}
23+
}
24+
return false;
25+
}
26+
327
function isHidden(node) {
428
if (!node.attributes) {
529
return false;
630
}
731
if (node.attributes.some((a) => a.name === 'hidden')) {
832
return true;
933
}
10-
const ariaHidden = node.attributes.find((a) => a.name === 'aria-hidden');
11-
if (ariaHidden?.value?.type === 'GlimmerTextNode' && ariaHidden.value.chars === 'true') {
12-
return true;
13-
}
14-
return false;
34+
return isAriaHiddenTruthy(node.attributes.find((a) => a.name === 'aria-hidden'));
1535
}
1636

1737
function isComponent(node) {

tests/lib/rules/template-no-empty-headings.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ ruleTester.run('template-no-empty-headings', rule, {
4343
'<template><h1><this.Heading /></h1></template>',
4444
'<template><h2><@heading /></h2></template>',
4545
'<template><h3><ns.Heading /></h3></template>',
46+
47+
// aria-hidden as a boolean / valueless / empty / mustache attribute — all
48+
// should exempt the heading (aligns with jsx-a11y / vue-a11y treatment of
49+
// boolean HTML attributes).
50+
'<template><h1 aria-hidden></h1></template>',
51+
'<template><h1 aria-hidden=""></h1></template>',
52+
'<template><h1 aria-hidden={{true}}></h1></template>',
53+
'<template><h1 aria-hidden="true">Visible to sighted only</h1></template>',
4654
],
4755
invalid: [
4856
{

0 commit comments

Comments
 (0)