Skip to content

Commit e3e6a02

Browse files
committed
fix+docs: add native-element gate, handle hidden anchors, fix doc examples (Copilot review)
1 parent 73b6527 commit e3e6a02

3 files changed

Lines changed: 60 additions & 4 deletions

File tree

docs/rules/template-anchor-has-content.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ This rule **forbids** the following:
4141
<a href="/x" />
4242
<a href="/x"></a>
4343
<a href="/x"> </a>
44-
<a href="/x"><span aria-hidden>X</span></a>
45-
<a href="/x"><img aria-hidden alt="Search" /></a>
44+
<a href="/x"><span aria-hidden="true">X</span></a>
45+
<a href="/x"><img aria-hidden="true" alt="Search" /></a>
4646
<a href="/x"><img /></a>
4747
<a href="/x" aria-label="" />
4848
</template>

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,17 +150,38 @@ module.exports = {
150150
const sourceCode = context.sourceCode || context.getSourceCode();
151151
return {
152152
GlimmerElementNode(node) {
153-
if (node.tag !== 'a') {
153+
// Only the native <a> element — in strict GJS, a lowercase tag can be
154+
// shadowed by an in-scope local binding, and components shouldn't be
155+
// validated here. `isNativeElement` combines authoritative html/svg/
156+
// mathml tag lists with scope-shadowing detection.
157+
if (!isNativeElement(node, sourceCode)) {
158+
return;
159+
}
160+
if (node.tag?.toLowerCase() !== 'a') {
154161
return;
155162
}
156163

157164
// Only anchors acting as links (with href) are in scope. An <a> without
158165
// href is covered by `template-link-href-attributes` / not a link.
159-
const hasHref = (node.attributes || []).some((a) => a.name === 'href');
166+
const attrs = node.attributes || [];
167+
const hasHref = attrs.some((a) => a.name === 'href');
160168
if (!hasHref) {
161169
return;
162170
}
163171

172+
// Skip anchors the author has explicitly hidden — either via the HTML
173+
// `hidden` boolean attribute (element is not rendered at all) or
174+
// `aria-hidden="true"` (element removed from the accessibility tree).
175+
// In both cases, "accessible name of an anchor" is moot.
176+
const hasHidden = attrs.some((a) => a.name === 'hidden');
177+
if (hasHidden) {
178+
return;
179+
}
180+
const ariaHiddenAttr = attrs.find((a) => a.name === 'aria-hidden');
181+
if (isAriaHiddenTrue(ariaHiddenAttr)) {
182+
return;
183+
}
184+
164185
if (hasAccessibleNameAttribute(node)) {
165186
return;
166187
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,41 @@ ruleTester.run('template-anchor-has-content', rule, {
7979
filename: 'test.gjs',
8080
code: '<template><a href="/x"><img aria-hidden alt="Nope" /></a></template>',
8181
},
82+
83+
// Anchor itself hidden via HTML `hidden` boolean attribute — element is
84+
// not rendered, so "accessible name of an anchor" is moot.
85+
{
86+
filename: 'test.gjs',
87+
code: '<template><a href="/x" hidden /></template>',
88+
},
89+
{
90+
filename: 'test.gjs',
91+
code: '<template><a href="/x" hidden></a></template>',
92+
},
93+
94+
// Anchor itself hidden via aria-hidden="true" — removed from the a11y
95+
// tree, so the accessible-name check does not apply.
96+
{
97+
filename: 'test.gjs',
98+
code: '<template><a href="/x" aria-hidden="true" /></template>',
99+
},
100+
{
101+
filename: 'test.gjs',
102+
code: '<template><a href="/x" aria-hidden={{true}}></a></template>',
103+
},
104+
105+
// Scope-shadowed lowercase `a` (local binding in GJS) — not the native
106+
// HTML anchor, so the rule does not validate it. `isNativeElement`
107+
// detects the shadowing via scope reference tracking.
108+
{
109+
filename: 'test.gjs',
110+
code: `
111+
const a = '';
112+
<template>
113+
<a href="/x" />
114+
</template>
115+
`,
116+
},
82117
],
83118

84119
invalid: [

0 commit comments

Comments
 (0)