Skip to content

Commit 99774f7

Browse files
committed
fix: use classifyAttribute for tabindex (rows t6, t7)
1 parent daad132 commit 99774f7

2 files changed

Lines changed: 24 additions & 2 deletions

File tree

lib/rules/template-interactive-supports-focus.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const { dom, roles } = require('aria-query');
4+
const { classifyAttribute } = require('../utils/glimmer-attr-presence');
45

56
// Interactive ARIA roles — non-abstract roles that descend from `widget`, plus
67
// `toolbar` (per jsx-a11y's convention: toolbar behaves as a widget even
@@ -226,7 +227,13 @@ module.exports = {
226227
// targets still exists.
227228
// HTML attribute names are case-insensitive, so accept `tabindex` or
228229
// any other casing (e.g. `tabIndex`, the React-style camelCase).
229-
const hasTabindex = node.attributes?.some((a) => a.name?.toLowerCase() === 'tabindex');
230+
// Use classifyAttribute so bare `{{false}}` / `{{null}}` /
231+
// `{{undefined}}` (rows t6, t7) — which Glimmer omits at runtime —
232+
// are NOT treated as satisfying the focus requirement. Dynamic
233+
// values (`tabindex={{this.x}}` → 'unknown') keep the previous
234+
// benefit-of-the-doubt: the runtime value could be a valid number.
235+
const tabindexAttr = node.attributes?.find((a) => a.name?.toLowerCase() === 'tabindex');
236+
const hasTabindex = classifyAttribute(tabindexAttr).presence !== 'absent';
230237
if (hasTabindex && !isSuppressedFromFocus(node, tag, getTextAttrValue)) {
231238
return;
232239
}

tests/lib/rules/template-interactive-supports-focus.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ ruleTester.run('template-interactive-supports-focus', rule, {
5252
'<template><div role="treeitem" tabindex="0"></div></template>',
5353
// tabindex="-1" is also sufficient — the role still has a focus target.
5454
'<template><div role="button" tabindex="-1"></div></template>',
55-
// Dynamic tabindex satisfies the check (the attribute is present).
55+
// Dynamic tabindex satisfies the check (runtime value unknown — give
56+
// benefit of the doubt; the runtime value may be a valid number).
5657
'<template><div role="button" tabindex={{this.ti}}></div></template>',
5758

5859
// === Interactive role on a non-focusable host but contenteditable is truthy. ===
@@ -130,6 +131,20 @@ ruleTester.run('template-interactive-supports-focus', rule, {
130131
output: null,
131132
errors: [{ messageId: 'focusable', data: { tag: 'div', role: 'button' } }],
132133
},
134+
// Bare-mustache falsy on tabindex (rows t6, t7) — Glimmer omits the
135+
// attribute at runtime, so tabindex is NOT actually present and does not
136+
// satisfy the focus requirement. AST-presence check would have missed
137+
// these (false negatives — rule silently let invalid templates through).
138+
{
139+
code: '<template><div role="button" tabindex={{false}}></div></template>',
140+
output: null,
141+
errors: [{ messageId: 'focusable', data: { tag: 'div', role: 'button' } }],
142+
},
143+
{
144+
code: '<template><div role="button" tabindex={{null}}></div></template>',
145+
output: null,
146+
errors: [{ messageId: 'focusable', data: { tag: 'div', role: 'button' } }],
147+
},
133148
// Role-fallback: UAs walk past unknown leading tokens to the first
134149
// recognised role (`button` here). Rule should require focusability.
135150
// LLM guardrail: models sometimes emit speculative unknown-first lists.

0 commit comments

Comments
 (0)