Skip to content

Commit 34a3f8c

Browse files
committed
fix: first-recognized-token in decorative check; treat mustache empty string as empty label
- isExplicitlyDecorative now walks the role token list for the first recognised role (per WAI-ARIA §4.1), so `role="foo none"` is correctly treated as decorative instead of only checking the literal first token. - hasNonEmptyLabelAttr now treats `aria-label={{""}}` (GlimmerMustache wrapping a GlimmerStringLiteral with an empty/whitespace value) as empty, consistent with a plain empty text-node value. - Update misleading test comment: "No aria-label/labelledby" → "…or only empty values" to reflect the `aria-label=""` case in the same group. - Add valid test cases for both fixed behaviours.
1 parent fdc336b commit 34a3f8c

2 files changed

Lines changed: 27 additions & 4 deletions

File tree

lib/rules/template-no-aria-label-misuse.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,16 @@ function hasNonEmptyLabelAttr(node, name) {
139139
if (attr.value.type === 'GlimmerTextNode') {
140140
return attr.value.chars !== '';
141141
}
142-
// Mustache — treat as non-empty (author has declared intent).
142+
// Mustache with a static string literal path: `aria-label={{""}}` is still
143+
// empty, so treat it the same as an empty text node.
144+
if (
145+
attr.value.type === 'GlimmerMustacheStatement' &&
146+
attr.value.path?.type === 'GlimmerStringLiteral'
147+
) {
148+
return attr.value.path.value.trim() !== '';
149+
}
150+
// All other mustache / concat forms — treat as non-empty (author has
151+
// declared intent).
143152
return true;
144153
}
145154

@@ -148,8 +157,16 @@ function isExplicitlyDecorative(node) {
148157
if (!role) {
149158
return false;
150159
}
151-
const first = role.trim().split(/\s+/)[0]?.toLowerCase();
152-
return first === 'presentation' || first === 'none';
160+
// Walk the token list for the first *recognised* role, mirroring WAI-ARIA
161+
// §4.1 fallback semantics (UAs skip unknown tokens). `role="foo none"` is
162+
// decorative because the first recognised token is "none".
163+
const tokens = role.trim().toLowerCase().split(/\s+/u);
164+
for (const token of tokens) {
165+
if (token && roles.get(token)) {
166+
return token === 'presentation' || token === 'none';
167+
}
168+
}
169+
return false;
153170
}
154171

155172
// Escape hatch: any `tabindex` value signals author-intent-to-interact,

tests/lib/rules/template-no-aria-label-misuse.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,20 @@ const validHbs = [
2020
// role="presentation" / "none" — author opted out; nothing to lint.
2121
'<div role="presentation" aria-label="decoration">x</div>',
2222
'<span role="none" aria-label="decoration">x</span>',
23+
// First *recognised* token is "none"/"presentation" — still decorative.
24+
'<div role="foo none" aria-label="decoration">x</div>',
25+
'<div role="bogus presentation" aria-label="decoration">x</div>',
2326
// Tabindex escape hatch: real screen readers read aria-label on a
2427
// tabindexed element even when the implicit role is generic.
2528
'<span tabindex="0" aria-label="Focusable">x</span>',
2629
'<div tabindex="-1" aria-label="x">x</div>',
27-
// No aria-label/labelledby.
30+
// No aria-label/labelledby, or only empty values.
2831
'<div>plain</div>',
2932
'<span>text</span>',
3033
'<div aria-label=""></div>',
34+
// Mustache with a static empty string — treated as empty, not as a label.
35+
'<div aria-label={{""}}></div>',
36+
'<span aria-label={{""}}></span>',
3137
// Ember component — skipped (role unknowable).
3238
'<MyButton aria-label="x" />',
3339
// Custom elements — not in HTML/SVG/MathML tag lists, skipped.

0 commit comments

Comments
 (0)