Skip to content

Commit 776089d

Browse files
committed
fix: clarify comments, document canvas exemption, handle tabindex={{N}} concat form
- Update is-native-element-test.js header comment to accurately describe what the file tests (list-lookup path; scope-shadowing via RuleTester) - Clarify dom.has(tag) guard comment: skips SVG/MathML and other non-HTML tags not present in aria-query's DOM map, not just custom elements - Document the <canvas> exemption in the rule docs with rationale - Handle GlimmerConcatStatement with single mustache part in getStaticTabindexValue so tabindex="{{-1}}" is treated same as tabindex={{-1}}
1 parent 8a6332f commit 776089d

3 files changed

Lines changed: 19 additions & 5 deletions

File tree

docs/rules/template-no-noninteractive-tabindex.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ If the element is meant to be interactive, give it an explicit ARIA role (`butto
1010

1111
`tabindex="-1"` is exempt — it marks an element as programmatically focusable but skipped by the Tab key, the canonical pattern for scroll-to-focus targets, focus restoration, and composite-widget children. See [`template-require-aria-activedescendant-tabindex`](./template-require-aria-activedescendant-tabindex.md).
1212

13+
`<canvas>` is always exempt. The HTML spec does not classify `<canvas>` as interactive content, but it is routinely used as an interactive drawing or game surface, and `tabindex` is required to make it keyboard-accessible. Flagging `<canvas tabindex="0">` would produce unhelpful noise for these legitimate use-cases.
14+
1315
## Examples
1416

1517
This rule **forbids** the following:
@@ -45,6 +47,9 @@ This rule **allows** the following:
4547
4648
{{! role="tabpanel" — default allowlist (see Options) }}
4749
<div role="tabpanel" tabindex="0" aria-labelledby="tab-1">Panel</div>
50+
51+
{{! <canvas> — exempted because canvas needs tabindex to be keyboard-accessible }}
52+
<canvas tabindex="0"></canvas>
4853
</template>
4954
```
5055

lib/rules/template-no-noninteractive-tabindex.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ function getStaticTabindexValue(attr) {
5555
return Number.isFinite(parsed) ? parsed : undefined;
5656
}
5757
}
58+
// tabindex="{{-1}}" → GlimmerConcatStatement with a single mustache part.
59+
if (
60+
value.type === 'GlimmerConcatStatement' &&
61+
value.parts?.length === 1 &&
62+
value.parts[0].type === 'GlimmerMustacheStatement'
63+
) {
64+
return getStaticTabindexValue({ value: value.parts[0] });
65+
}
5866
return undefined;
5967
}
6068

@@ -147,7 +155,9 @@ module.exports = {
147155
return;
148156
}
149157

150-
// Skip custom elements (not in aria-query's dom map).
158+
// Only check elements present in aria-query's DOM map. This skips SVG
159+
// and MathML tags (not in aria-query's HTML-focused map) as well as any
160+
// other non-HTML tags that passed isNativeElement.
151161
if (!dom.has(tag)) {
152162
return;
153163
}

tests/lib/utils/is-native-element-test.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22

33
const { isNativeElement, ELEMENT_TAGS } = require('../../../lib/utils/is-native-element');
44

5-
// Tests exercise the list-lookup path only. Scope-based shadowing is covered
6-
// by the rule-level test suites (see tests/lib/rules/template-no-block-params-
7-
// for-html-elements.js and siblings) because it requires a real ESLint
8-
// SourceCode / scope manager that's only built up by the rule tester.
5+
// Tests cover the list-lookup path (no sourceCode argument). Scope-based
6+
// shadowing (passing a real ESLint SourceCode) is exercised by rule-level
7+
// test suites that go through the full RuleTester pipeline.
98

109
describe('isNativeElement — list-only behavior (no sourceCode)', () => {
1110
it('returns true for lowercase HTML tag names', () => {

0 commit comments

Comments
 (0)