Skip to content

Commit 2086fca

Browse files
committed
fix: use classifyAttribute for tabindex + disabled (rows d3, d6, t6, t7)
1 parent f2a690d commit 2086fca

2 files changed

Lines changed: 31 additions & 18 deletions

File tree

lib/rules/template-no-aria-hidden-on-focusable.js

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const { isNativeElement } = require('../utils/is-native-element');
44
const { getStaticAttrValue } = require('../utils/static-attr-value');
5+
const { classifyAttribute } = require('../utils/glimmer-attr-presence');
56

67
function findAttr(node, name) {
78
return node.attributes?.find((a) => a.name === name);
@@ -63,22 +64,12 @@ function isDisabledFormControl(node, tag) {
6364
return false;
6465
}
6566
const attr = findAttr(node, 'disabled');
66-
if (!attr) {
67-
return false;
68-
}
69-
// Per docs/glimmer-attribute-behavior.md, ONLY bare-mustache boolean-false
70-
// (`disabled={{false}}`) renders as omitted at runtime — concat
71-
// (`disabled="{{false}}"`) and string-literal (`disabled={{"false"}}`) forms
72-
// still render the attribute as present and the control IS disabled.
73-
const v = attr.value;
74-
if (
75-
v?.type === 'GlimmerMustacheStatement' &&
76-
v.path?.type === 'GlimmerBooleanLiteral' &&
77-
v.path.value === false
78-
) {
79-
return false;
80-
}
81-
return true;
67+
// Per docs/glimmer-attribute-behavior.md (rows d3, d6 plus cross-attribute
68+
// observation on falsy-coercion), bare-mustache falsy literals on a boolean
69+
// HTML attribute cause Glimmer to omit the attribute at runtime. We use
70+
// classifyAttribute so the runtime-rendered presence drives the answer
71+
// rather than AST-presence.
72+
return classifyAttribute(attr).presence === 'present';
8273
}
8374

8475
// Narrow rule-local "keyboard-focusable" check. Intentionally distinct from
@@ -98,8 +89,11 @@ function isKeyboardFocusable(node, getTextAttrValueFn) {
9889
}
9990

10091
// Any tabindex (including "-1") makes the element at least programmatically
101-
// focusable — still a keyboard-trap risk under aria-hidden.
102-
if (findAttr(node, 'tabindex')) {
92+
// focusable — still a keyboard-trap risk under aria-hidden. Use
93+
// classifyAttribute so bare `{{false}}` / `{{null}}` / `{{undefined}}`
94+
// (rows t6, t7) — which Glimmer omits at runtime — are NOT treated as
95+
// having a tabindex.
96+
if (classifyAttribute(findAttr(node, 'tabindex')).presence === 'present') {
10397
return true;
10498
}
10599

tests/lib/rules/template-no-aria-hidden-on-focusable.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ ruleTester.run('template-no-aria-hidden-on-focusable', rule, {
4747
'<template><button disabled aria-hidden="true">Click me</button></template>',
4848
'<template><input disabled aria-hidden="true" /></template>',
4949

50+
// Bare-mustache falsy on tabindex (rows t6, t7) — Glimmer omits the
51+
// attribute at runtime, so the element is NOT focusable from tabindex
52+
// and aria-hidden isn't trapping anything. AST-presence check would
53+
// have false-positive flagged these.
54+
'<template><div tabindex={{false}} aria-hidden="true"></div></template>',
55+
'<template><div tabindex={{null}} aria-hidden="true"></div></template>',
56+
'<template><div tabindex={{undefined}} aria-hidden="true"></div></template>',
57+
5058
// Components — we don't know if they render a focusable element.
5159
'<template><CustomBtn aria-hidden="true" /></template>',
5260

@@ -148,6 +156,17 @@ ruleTester.run('template-no-aria-hidden-on-focusable', rule, {
148156
output: null,
149157
errors: [{ messageId: 'noAriaHiddenOnFocusable' }],
150158
},
159+
// Same shape with bare-mustache null/undefined — also omitted (rows d6 + cross-attr observation).
160+
{
161+
code: '<template><button aria-hidden="true" disabled={{null}}>click</button></template>',
162+
output: null,
163+
errors: [{ messageId: 'noAriaHiddenOnFocusable' }],
164+
},
165+
{
166+
code: '<template><button aria-hidden="true" disabled={{undefined}}>click</button></template>',
167+
output: null,
168+
errors: [{ messageId: 'noAriaHiddenOnFocusable' }],
169+
},
151170
{
152171
code: '<template><button aria-hidden="TRUE"></button></template>',
153172
output: null,

0 commit comments

Comments
 (0)