Skip to content

Commit 82e7652

Browse files
committed
fix(template-require-aria-activedescendant-tabindex): accept tabindex="-1"
Before: any tabindex below 0 was flagged, and the autofix replaced tabindex="-1" with tabindex="0". That silently changed semantics — -1 is the canonical "focusable but not in tab order" value that composite widgets with aria-activedescendant specifically want. tabindex semantics: "0" — focusable, in the natural tab order "-1" — focusable programmatically (e.g. via roving focus), skipped in tab order Both are valid for elements that manage focus via aria-activedescendant; see the W3C APG entry on "Managing focus in composites using aria-activedescendant": https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_focus_activedescendant Matches the check upstream in eslint-plugin-jsx-a11y (aria-activedescendant-has-tabindex.js: `if (tabIndex >= -1) return;`). lit-a11y's aria-activedescendant-has-tabindex has the same semantics. Rule doc updated to describe the new accepted range. Tests moved the three tabindex="-1" cases from invalid to valid.
1 parent 24882a3 commit 82e7652

3 files changed

Lines changed: 13 additions & 15 deletions

File tree

docs/rules/template-require-aria-activedescendant-tabindex.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
<!-- end auto-generated rule header -->
88

9-
This rule requires all non-interactive HTML elements using the `aria-activedescendant` attribute to declare a `tabindex` of zero.
9+
This rule requires non-interactive HTML elements using the `aria-activedescendant` attribute to declare a `tabindex` of `0` or `-1`.
1010

1111
The `aria-activedescendant` attribute identifies the active descendant element of a composite widget, textbox, group, or application with document focus. This attribute is placed on the container element of the input control, and its value is set to the ID of the active child element. This allows screen readers to communicate information about the currently active element as if it has focus, while actual focus of the DOM remains on the container element.
1212

13-
Elements with `aria-activedescendant` must have a `tabindex` of zero in order to support keyboard navigation. Besides interactive elements, which are inherently keyboard-focusable, elements using the `aria-activedescendant` attribute must declare a `tabIndex` of zero with the `tabIndex` attribute.
13+
Elements with `aria-activedescendant` must be focusable to support keyboard navigation. `tabindex="0"` puts the element in the natural tab order; `tabindex="-1"` makes it focusable programmatically (e.g. via roving focus) but skips it in the tab order. Both are valid patterns for composite widgets — see the [W3C APG — Managing focus in composites using aria-activedescendant](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_focus_activedescendant).
1414

1515
## Examples
1616

@@ -19,8 +19,8 @@ This rule **forbids** the following:
1919
```gjs
2020
<template>
2121
<div aria-activedescendant='some-id'></div>
22-
<div aria-activedescendant='some-id' tabindex='-1'></div>
23-
<input aria-activedescendant={{some-id}} tabindex='-1' />
22+
<div aria-activedescendant='some-id' tabindex='-2'></div>
23+
<input aria-activedescendant={{some-id}} tabindex='-100' />
2424
</template>
2525
```
2626

@@ -32,9 +32,11 @@ This rule **allows** the following:
3232
<CustomComponent aria-activedescendant={{some-id}} />
3333
<CustomComponent aria-activedescendant={{some-id}} tabindex={{0}} />
3434
<div aria-activedescendant='some-id' tabindex='0'></div>
35+
<div aria-activedescendant='some-id' tabindex='-1'></div>
3536
<input />
3637
<input aria-activedescendant={{some-id}} />
3738
<input aria-activedescendant={{some-id}} tabindex={{0}} />
39+
<input aria-activedescendant={{some-id}} tabindex={{-1}} />
3840
</template>
3941
```
4042

lib/rules/template-require-aria-activedescendant-tabindex.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ module.exports = {
9191

9292
const tabindexValue = getTabindexNumericValue(tabindexAttr);
9393

94-
if (!Number.isFinite(tabindexValue) || tabindexValue < 0) {
94+
if (!Number.isFinite(tabindexValue) || tabindexValue < -1) {
9595
context.report({
9696
node,
9797
messageId: 'missingTabindex',

tests/lib/rules/template-require-aria-activedescendant-tabindex.js

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ const validHbs = [
1313
'<CustomComponent aria-activedescendant="choice1" />',
1414
'<CustomComponent aria-activedescendant="option1" tabIndex="-1" />',
1515
'<CustomComponent aria-activedescendant={{foo}} tabindex={{bar}} />',
16+
// tabindex="-1" is focusable-but-not-tabbable — the canonical pattern for
17+
// composite widgets that manage focus via roving focus / aria-activedescendant.
18+
// See W3C APG — https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_focus_activedescendant
19+
'<div aria-activedescendant="option0" tabindex="-1"></div>',
20+
'<div aria-activedescendant={{foo}} tabindex={{-1}}></div>',
21+
'<button aria-activedescendant="x" tabindex="-1"></button>',
1622
];
1723

1824
const invalidHbs = [
@@ -26,11 +32,6 @@ const invalidHbs = [
2632
output: '<div aria-activedescendant={{bar}} tabindex="0" />',
2733
errors: [{ message: ERROR_MESSAGE }],
2834
},
29-
{
30-
code: '<div aria-activedescendant={{foo}} tabindex={{-1}}></div>',
31-
output: '<div aria-activedescendant={{foo}} tabindex="0"></div>',
32-
errors: [{ message: ERROR_MESSAGE }],
33-
},
3435
{
3536
code: '<div aria-activedescendant="fixme" tabindex=-100></div>',
3637
output: '<div aria-activedescendant="fixme" tabindex="0"></div>',
@@ -41,11 +42,6 @@ const invalidHbs = [
4142
output: '<a aria-activedescendant="x" tabindex="0"></a>',
4243
errors: [{ message: ERROR_MESSAGE }],
4344
},
44-
{
45-
code: '<button aria-activedescendant="x" tabindex="-1"></button>',
46-
output: '<button aria-activedescendant="x" tabindex="0"></button>',
47-
errors: [{ message: ERROR_MESSAGE }],
48-
},
4945
];
5046

5147
function wrapTemplate(entry) {

0 commit comments

Comments
 (0)