`) carries no accessible
- // name. Treat it as empty — not as non-empty — so downstream checks don't
- // mistake it for an author-declared label.
- if (attr.value === null || attr.value === undefined) {
+ // 'present' with statically-known value: empty / whitespace renders no
+ // accessible name, so treat as empty (not a misuse).
+ if (value !== null && value.trim() === '') {
return false;
}
- if (attr.value.type === 'GlimmerTextNode') {
- return attr.value.chars.trim() !== '';
- }
- // Mustache with a static string literal path: `aria-label={{""}}` is still
- // empty, so treat it the same as an empty text node.
- if (
- attr.value.type === 'GlimmerMustacheStatement' &&
- attr.value.path?.type === 'GlimmerStringLiteral'
- ) {
- return attr.value.path.value.trim() !== '';
- }
- // All other mustache / concat forms — treat as non-empty (author has
- // declared intent).
+ // Otherwise the attribute renders a non-empty (or dynamic) value — author
+ // has declared intent.
return true;
}
@@ -190,8 +185,13 @@ function isExplicitlyDecorative(node) {
// high false-positive cost (the author wants the label read on focus)
// relative to the true-positive it would catch. Disable via
// `strictTabindex: true` to get strict spec-role enforcement.
+//
+// Per docs/glimmer-attribute-behavior.md (rows t6, t7), bare-mustache
+// `tabindex={{false}}` / `{{null}}` / `{{undefined}}` cause Glimmer to omit
+// the attribute at runtime — the element has no tabindex and the escape
+// hatch should NOT fire.
function hasTabindex(node) {
- return Boolean(findAttr(node, 'tabindex'));
+ return classifyAttribute(findAttr(node, 'tabindex')).presence === 'present';
}
/** @type {import('eslint').Rule.RuleModule} */
diff --git a/tests/lib/rules/template-no-aria-label-misuse.js b/tests/lib/rules/template-no-aria-label-misuse.js
index a6029b0d19..9be7b60c06 100644
--- a/tests/lib/rules/template-no-aria-label-misuse.js
+++ b/tests/lib/rules/template-no-aria-label-misuse.js
@@ -62,6 +62,15 @@ const validHbs = [
'
x
',
// Static string-literal mustache — empty string is treated as no label.
'
x
',
+ // Bare-mustache falsy on aria-label (rows h6, h9, h10) — Glimmer omits the
+ // attribute at runtime, so there is NO aria-label and no misuse to flag.
+ '
x
',
+ '
x
',
+ '
x
',
+ // Bare-mustache falsy on tabindex (rows t6, t7) — escape hatch should NOT
+ // fire because tabindex isn't actually rendered. The element is back to
+ // having a non-interactive implicit role and aria-label IS a misuse.
+ // (paired with an aria-label to ensure it gets flagged for the right reason)
];
const invalidHbs = [
@@ -85,6 +94,16 @@ const invalidHbs = [
code: '
x
',
errors: [{ message: err('aria-label', 'div', 'generic') }],
},
+ // Bare-mustache falsy on tabindex (rows t6, t7) — escape hatch shouldn't
+ // fire (tabindex omitted at runtime). aria-label is misuse on a generic.
+ {
+ code: '
x
',
+ errors: [{ message: err('aria-label', 'div', 'generic') }],
+ },
+ {
+ code: '
x
',
+ errors: [{ message: err('aria-label', 'div', 'generic') }],
+ },
//
![]()
is role=presentation per ARIA; aria-label contradicts the
// "decorative" hint and is prohibited.
{