Skip to content

Commit 828d8e1

Browse files
committed
feat: add template-no-noninteractive-tabindex
1 parent f400aca commit 828d8e1

7 files changed

Lines changed: 780 additions & 38 deletions

README.md

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -256,40 +256,41 @@ To disable a rule for an entire `.gjs`/`.gts` file, use a regular ESLint file-le
256256

257257
### Accessibility
258258

259-
| Name                                            | Description | 💼 | 🔧 | 💡 |
260-
| :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------- | :- | :- | :- |
261-
| [template-link-href-attributes](docs/rules/template-link-href-attributes.md) | require href attribute on link elements | 📋 | | |
262-
| [template-no-abstract-roles](docs/rules/template-no-abstract-roles.md) | disallow abstract ARIA roles | 📋 | | |
263-
| [template-no-accesskey-attribute](docs/rules/template-no-accesskey-attribute.md) | disallow accesskey attribute | 📋 | 🔧 | |
264-
| [template-no-aria-hidden-body](docs/rules/template-no-aria-hidden-body.md) | disallow aria-hidden on body element | 📋 | 🔧 | |
265-
| [template-no-aria-unsupported-elements](docs/rules/template-no-aria-unsupported-elements.md) | disallow ARIA roles, states, and properties on elements that do not support them | 📋 | | |
266-
| [template-no-autofocus-attribute](docs/rules/template-no-autofocus-attribute.md) | disallow autofocus attribute | 📋 | 🔧 | |
267-
| [template-no-duplicate-landmark-elements](docs/rules/template-no-duplicate-landmark-elements.md) | disallow duplicate landmark elements without unique labels | 📋 | | |
268-
| [template-no-empty-headings](docs/rules/template-no-empty-headings.md) | disallow empty heading elements | 📋 | | |
269-
| [template-no-heading-inside-button](docs/rules/template-no-heading-inside-button.md) | disallow heading elements inside button elements | 📋 | | |
270-
| [template-no-invalid-aria-attributes](docs/rules/template-no-invalid-aria-attributes.md) | disallow invalid aria-* attributes | 📋 | | |
271-
| [template-no-invalid-interactive](docs/rules/template-no-invalid-interactive.md) | disallow non-interactive elements with interactive handlers | 📋 | | |
272-
| [template-no-invalid-link-text](docs/rules/template-no-invalid-link-text.md) | disallow invalid or uninformative link text content | 📋 | | |
273-
| [template-no-invalid-link-title](docs/rules/template-no-invalid-link-title.md) | disallow invalid title attributes on link elements | 📋 | | |
274-
| [template-no-invalid-role](docs/rules/template-no-invalid-role.md) | disallow invalid ARIA roles | 📋 | | |
275-
| [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | 📋 | | |
276-
| [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | 📋 | | |
277-
| [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | 📋 | | |
278-
| [template-no-positive-tabindex](docs/rules/template-no-positive-tabindex.md) | disallow positive tabindex values | 📋 | | |
279-
| [template-no-redundant-role](docs/rules/template-no-redundant-role.md) | disallow redundant role attributes | 📋 | 🔧 | |
280-
| [template-no-unsupported-role-attributes](docs/rules/template-no-unsupported-role-attributes.md) | disallow ARIA attributes that are not supported by the element role | 📋 | 🔧 | |
281-
| [template-no-whitespace-within-word](docs/rules/template-no-whitespace-within-word.md) | disallow excess whitespace within words (e.g. "W e l c o m e") | 📋 | | |
282-
| [template-require-aria-activedescendant-tabindex](docs/rules/template-require-aria-activedescendant-tabindex.md) | require non-interactive elements with aria-activedescendant to have tabindex | 📋 | 🔧 | |
283-
| [template-require-context-role](docs/rules/template-require-context-role.md) | require ARIA roles to be used in appropriate context | 📋 | | |
284-
| [template-require-iframe-title](docs/rules/template-require-iframe-title.md) | require iframe elements to have a title attribute | 📋 | | |
285-
| [template-require-input-label](docs/rules/template-require-input-label.md) | require label for form input elements | 📋 | | |
286-
| [template-require-lang-attribute](docs/rules/template-require-lang-attribute.md) | require lang attribute on html element | 📋 | | |
287-
| [template-require-mandatory-role-attributes](docs/rules/template-require-mandatory-role-attributes.md) | require mandatory ARIA attributes for ARIA roles | 📋 | | |
288-
| [template-require-media-caption](docs/rules/template-require-media-caption.md) | require captions for audio and video elements | 📋 | | |
289-
| [template-require-presentational-children](docs/rules/template-require-presentational-children.md) | require presentational elements to only contain presentational children | 📋 | | |
290-
| [template-require-valid-alt-text](docs/rules/template-require-valid-alt-text.md) | require valid alt text for images and other elements | 📋 | | |
291-
| [template-require-valid-form-groups](docs/rules/template-require-valid-form-groups.md) | require grouped form controls to have fieldset/legend or WAI-ARIA group labeling | | | |
292-
| [template-table-groups](docs/rules/template-table-groups.md) | require table elements to use table grouping elements | 📋 | | |
259+
| Name                                            | Description | 💼 | 🔧 | 💡 |
260+
| :--------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------- | :- | :- | :- |
261+
| [template-link-href-attributes](docs/rules/template-link-href-attributes.md) | require href attribute on link elements | 📋 | | |
262+
| [template-no-abstract-roles](docs/rules/template-no-abstract-roles.md) | disallow abstract ARIA roles | 📋 | | |
263+
| [template-no-accesskey-attribute](docs/rules/template-no-accesskey-attribute.md) | disallow accesskey attribute | 📋 | 🔧 | |
264+
| [template-no-aria-hidden-body](docs/rules/template-no-aria-hidden-body.md) | disallow aria-hidden on body element | 📋 | 🔧 | |
265+
| [template-no-aria-unsupported-elements](docs/rules/template-no-aria-unsupported-elements.md) | disallow ARIA roles, states, and properties on elements that do not support them | 📋 | | |
266+
| [template-no-autofocus-attribute](docs/rules/template-no-autofocus-attribute.md) | disallow autofocus attribute | 📋 | 🔧 | |
267+
| [template-no-duplicate-landmark-elements](docs/rules/template-no-duplicate-landmark-elements.md) | disallow duplicate landmark elements without unique labels | 📋 | | |
268+
| [template-no-empty-headings](docs/rules/template-no-empty-headings.md) | disallow empty heading elements | 📋 | | |
269+
| [template-no-heading-inside-button](docs/rules/template-no-heading-inside-button.md) | disallow heading elements inside button elements | 📋 | | |
270+
| [template-no-invalid-aria-attributes](docs/rules/template-no-invalid-aria-attributes.md) | disallow invalid aria-* attributes | 📋 | | |
271+
| [template-no-invalid-interactive](docs/rules/template-no-invalid-interactive.md) | disallow non-interactive elements with interactive handlers | 📋 | | |
272+
| [template-no-invalid-link-text](docs/rules/template-no-invalid-link-text.md) | disallow invalid or uninformative link text content | 📋 | | |
273+
| [template-no-invalid-link-title](docs/rules/template-no-invalid-link-title.md) | disallow invalid title attributes on link elements | 📋 | | |
274+
| [template-no-invalid-role](docs/rules/template-no-invalid-role.md) | disallow invalid ARIA roles | 📋 | | |
275+
| [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | 📋 | | |
276+
| [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | 📋 | | |
277+
| [template-no-noninteractive-tabindex](docs/rules/template-no-noninteractive-tabindex.md) | disallow tabindex on non-interactive elements (elements without interactive native semantics or interactive ARIA role) | | | |
278+
| [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | 📋 | | |
279+
| [template-no-positive-tabindex](docs/rules/template-no-positive-tabindex.md) | disallow positive tabindex values | 📋 | | |
280+
| [template-no-redundant-role](docs/rules/template-no-redundant-role.md) | disallow redundant role attributes | 📋 | 🔧 | |
281+
| [template-no-unsupported-role-attributes](docs/rules/template-no-unsupported-role-attributes.md) | disallow ARIA attributes that are not supported by the element role | 📋 | 🔧 | |
282+
| [template-no-whitespace-within-word](docs/rules/template-no-whitespace-within-word.md) | disallow excess whitespace within words (e.g. "W e l c o m e") | 📋 | | |
283+
| [template-require-aria-activedescendant-tabindex](docs/rules/template-require-aria-activedescendant-tabindex.md) | require non-interactive elements with aria-activedescendant to have tabindex | 📋 | 🔧 | |
284+
| [template-require-context-role](docs/rules/template-require-context-role.md) | require ARIA roles to be used in appropriate context | 📋 | | |
285+
| [template-require-iframe-title](docs/rules/template-require-iframe-title.md) | require iframe elements to have a title attribute | 📋 | | |
286+
| [template-require-input-label](docs/rules/template-require-input-label.md) | require label for form input elements | 📋 | | |
287+
| [template-require-lang-attribute](docs/rules/template-require-lang-attribute.md) | require lang attribute on html element | 📋 | | |
288+
| [template-require-mandatory-role-attributes](docs/rules/template-require-mandatory-role-attributes.md) | require mandatory ARIA attributes for ARIA roles | 📋 | | |
289+
| [template-require-media-caption](docs/rules/template-require-media-caption.md) | require captions for audio and video elements | 📋 | | |
290+
| [template-require-presentational-children](docs/rules/template-require-presentational-children.md) | require presentational elements to only contain presentational children | 📋 | | |
291+
| [template-require-valid-alt-text](docs/rules/template-require-valid-alt-text.md) | require valid alt text for images and other elements | 📋 | | |
292+
| [template-require-valid-form-groups](docs/rules/template-require-valid-form-groups.md) | require grouped form controls to have fieldset/legend or WAI-ARIA group labeling | | | |
293+
| [template-table-groups](docs/rules/template-table-groups.md) | require table elements to use table grouping elements | 📋 | | |
293294

294295
### Best Practices
295296

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# ember/template-no-noninteractive-tabindex
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallow `tabindex` on non-interactive elements.
6+
7+
Adding `tabindex="0"` to a `<div>`, `<section>`, etc. puts it in the keyboard tab order without supplying any keyboard semantics — users reach the element but have no way to operate it, and screen readers announce the tag with no hint of interactivity.
8+
9+
If the element is meant to be interactive, give it an explicit ARIA role (`button`, `checkbox`, …) **and** wire up the appropriate keyboard event handlers. If it isn't meant to be interactive, remove the tabindex.
10+
11+
`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).
12+
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+
15+
## Examples
16+
17+
This rule **forbids** the following:
18+
19+
```gjs
20+
<template>
21+
<div tabindex="0"></div>
22+
<article tabindex="0">Story</article>
23+
<div role="article" tabindex="0"></div>
24+
<a tabindex="0">Not a link (missing href)</a>
25+
</template>
26+
```
27+
28+
This rule **allows** the following:
29+
30+
```gjs
31+
<template>
32+
{{! Interactive native elements }}
33+
<button tabindex="0">Click</button>
34+
<a href="/x" tabindex="0">Link</a>
35+
<input tabindex="-1" />
36+
37+
{{! Non-interactive element with an interactive ARIA role }}
38+
<div role="button" tabindex="0"></div>
39+
<div role="checkbox" tabindex="0" aria-checked="false"></div>
40+
41+
{{! tabindex="-1" — focusable but not in tab order }}
42+
<div tabindex="-1"></div>
43+
<section tabindex="-1">scroll target</section>
44+
45+
{{! Dynamic role — conservatively skipped }}
46+
<div role={{this.role}} tabindex="0"></div>
47+
48+
{{! role="tabpanel" — default allowlist (see Options) }}
49+
<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>
53+
</template>
54+
```
55+
56+
## Options
57+
58+
- `roles` (default `["tabpanel"]`) — non-interactive ARIA roles exempted from this rule. Elements carrying one of these roles may have `tabindex` without triggering a flag.
59+
60+
The default value (`["tabpanel"]`) matches jsx-a11y's recommended config. The [WAI-ARIA APG Tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) gives panels `tabindex="0"` when the panel's content isn't itself focusable, so keyboard users can page through panels. Flagging tabpanel-with-tabindex as a violation would break the canonical Tabs pattern.
61+
62+
Use an empty array (`roles: []`) to disable the default exemption — matching jsx-a11y's strict config. Use a wider list (e.g. `roles: ["tabpanel", "region"]`) to exempt additional roles where your project uses `tabindex` legitimately (scrollable regions, etc.).
63+
64+
## References
65+
66+
- [WAI-ARIA Authoring Practices — Keyboard Interaction](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/)
67+
- [MDN — tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex)
68+
- [`no-noninteractive-tabindex` — eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-noninteractive-tabindex.md)

0 commit comments

Comments
 (0)