Skip to content

Commit c7e12f8

Browse files
committed
fix: use classifyAttribute for tabindex + disabled (rows d3, d6, t6, t7)
1 parent 09ac5b4 commit c7e12f8

2 files changed

Lines changed: 25 additions & 2 deletions

File tree

lib/rules/template-no-role-presentation-on-focusable.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
'use strict';
1717

1818
const { isNativeElement } = require('../utils/is-native-element');
19+
const { classifyAttribute } = require('../utils/glimmer-attr-presence');
1920

2021
function findAttr(node, name) {
2122
return node.attributes?.find((a) => a.name === name);
@@ -102,7 +103,11 @@ function isDisabledFormControl(node, tag) {
102103
if (!DISABLEABLE_TAGS.has(tag)) {
103104
return false;
104105
}
105-
return Boolean(findAttr(node, 'disabled'));
106+
// Per docs/glimmer-attribute-behavior.md (rows d3, d6), bare-mustache
107+
// `disabled={{false}}` / `{{null}}` / `{{undefined}}` cause Glimmer to
108+
// omit the attribute at runtime — the control is NOT disabled. Use
109+
// classifyAttribute so runtime-rendered presence drives the answer.
110+
return classifyAttribute(findAttr(node, 'disabled')).presence === 'present';
106111
}
107112

108113
// Narrow rule-local "keyboard-focusable" check. Intentionally distinct from
@@ -123,7 +128,9 @@ function isKeyboardFocusable(node) {
123128

124129
// Any tabindex (including "-1") makes the element at least programmatically
125130
// focusable — still in scope for the semantic-conflict this rule targets.
126-
if (findAttr(node, 'tabindex')) {
131+
// Use classifyAttribute so bare `{{false}}` / `{{null}}` / `{{undefined}}`
132+
// (rows t6, t7) — which Glimmer omits — are not treated as having tabindex.
133+
if (classifyAttribute(findAttr(node, 'tabindex')).presence === 'present') {
127134
return true;
128135
}
129136

tests/lib/rules/template-no-role-presentation-on-focusable.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ ruleTester.run('template-no-role-presentation-on-focusable', rule, {
6767
// Disabled form controls are not keyboard-focusable (HTML §4.10.18.5).
6868
'<template><button disabled role="presentation">Click</button></template>',
6969
'<template><input type="text" disabled role="presentation" /></template>',
70+
// Bare-mustache falsy on tabindex (rows t6, t7) — Glimmer omits at runtime,
71+
// element isn't focusable from tabindex, so role="presentation" is fine.
72+
'<template><div tabindex={{false}} role="presentation"></div></template>',
73+
'<template><div tabindex={{null}} role="none"></div></template>',
7074

7175
// Non-focusable wrapper with role="presentation" around a focusable child —
7276
// the rule only checks the element that carries the role, not its subtree.
@@ -84,6 +88,18 @@ ruleTester.run('template-no-role-presentation-on-focusable', rule, {
8488
output: null,
8589
errors: [{ messageId: 'invalidPresentation' }],
8690
},
91+
// Bare-mustache falsy on disabled (rows d3, d6) — Glimmer omits the
92+
// attribute, button stays focusable, role="presentation" is invalid.
93+
{
94+
code: '<template><button disabled={{false}} role="presentation">Click</button></template>',
95+
output: null,
96+
errors: [{ messageId: 'invalidPresentation' }],
97+
},
98+
{
99+
code: '<template><button disabled={{null}} role="presentation">Click</button></template>',
100+
output: null,
101+
errors: [{ messageId: 'invalidPresentation' }],
102+
},
87103
{
88104
code: '<template><button role="none">Click</button></template>',
89105
output: null,

0 commit comments

Comments
 (0)