Skip to content

Commit c98c909

Browse files
committed
fix(template-anchor-has-content): only treat bare-mustache hidden={{false}} as omitted; add test
1 parent 0172580 commit c98c909

2 files changed

Lines changed: 31 additions & 3 deletions

File tree

lib/rules/template-anchor-has-content.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ function isAriaHiddenTrue(attr) {
2727
return resolved.trim().toLowerCase() === 'true';
2828
}
2929

30+
// True if the HTML boolean `hidden` attribute renders as present at runtime.
31+
// Per docs/glimmer-attribute-behavior.md, only bare-mustache boolean-false
32+
// (`hidden={{false}}`) is omitted at runtime. Concat (`hidden="{{false}}"`)
33+
// and string-literal (`hidden={{"false"}}`) forms keep the attribute present
34+
// (and HTML treats any presence — including value "false" — as ON).
35+
function rendersHidden(attr) {
36+
if (!attr) {
37+
return false;
38+
}
39+
const v = attr.value;
40+
if (
41+
v?.type === 'GlimmerMustacheStatement' &&
42+
v.path?.type === 'GlimmerBooleanLiteral' &&
43+
v.path.value === false
44+
) {
45+
return false;
46+
}
47+
return true;
48+
}
49+
3050
// True if the anchor itself declares an accessible name via a statically
3151
// non-empty `aria-label`, `aria-labelledby`, or `title`, OR via a dynamic
3252
// value (we can't know at lint time whether a mustache resolves to an empty
@@ -105,7 +125,7 @@ function evaluateChild(child, sourceCode) {
105125
// from the accessibility tree — equivalent to aria-hidden="true" for
106126
// accessible-name purposes. A <span hidden>Backup</span> inside an
107127
// anchor contributes no name at runtime.
108-
if (attrs.some((a) => a.name === 'hidden')) {
128+
if (rendersHidden(attrs.find((a) => a.name === 'hidden'))) {
109129
return { dynamic: false, accessible: false };
110130
}
111131

@@ -216,8 +236,7 @@ module.exports = {
216236
// `hidden` boolean attribute (element is not rendered at all) or
217237
// `aria-hidden="true"` (element removed from the accessibility tree).
218238
// In both cases, "accessible name of an anchor" is moot.
219-
const hasHidden = attrs.some((a) => a.name === 'hidden');
220-
if (hasHidden) {
239+
if (rendersHidden(attrs.find((a) => a.name === 'hidden'))) {
221240
return;
222241
}
223242
const ariaHiddenAttr = attrs.find((a) => a.name === 'aria-hidden');

tests/lib/rules/template-anchor-has-content.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,15 @@ ruleTester.run('template-anchor-has-content', rule, {
145145
output: null,
146146
errors: [{ messageId: 'anchorHasContent' }],
147147
},
148+
// hidden={{false}} is the bare-mustache boolean-false case — Glimmer omits
149+
// the attribute at runtime (per docs/glimmer-attribute-behavior.md), so
150+
// the anchor is rendered and visible; an empty body still needs a name.
151+
{
152+
filename: 'test.gjs',
153+
code: '<template><a href="/x" hidden={{false}}></a></template>',
154+
output: null,
155+
errors: [{ messageId: 'anchorHasContent' }],
156+
},
148157
// Empty anchor.
149158
{
150159
filename: 'test.gjs',

0 commit comments

Comments
 (0)