Skip to content

Commit 25ff021

Browse files
committed
Sync with ember-template-lint
1 parent 38a6b74 commit 25ff021

3 files changed

Lines changed: 60 additions & 124 deletions

File tree

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

Lines changed: 18 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,6 @@
22

33
<!-- end auto-generated rule header -->
44

5-
Disallows positive `tabindex` values.
6-
7-
Positive `tabindex` values disrupt the natural tab order of the page, making keyboard navigation confusing for users. This is especially problematic for users who rely on keyboard navigation, such as those with motor disabilities.
8-
9-
## Rule Details
10-
11-
This rule disallows positive integer values for the `tabindex` attribute. Only `0` (for naturally focusable elements) and `-1` (for programmatically focusable elements) are allowed.
12-
135
## `<* tabindex>`
146

157
[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) explains the motivation of this rule nicely:
@@ -22,46 +14,29 @@ This rule takes no arguments.
2214

2315
## Examples
2416

25-
Examples of **incorrect** code for this rule:
26-
27-
```gjs
28-
<template>
29-
<div tabindex="1">Content</div>
30-
</template>
31-
```
32-
33-
```gjs
34-
<template>
35-
<button tabindex="2">Click</button>
36-
</template>
37-
```
38-
39-
Examples of **correct** code for this rule:
17+
This rule **allows** the following:
4018

41-
```gjs
42-
<template>
43-
<div tabindex="0">Content</div>
44-
</template>
19+
```hbs
20+
<span tabindex='0'>foo</span>
21+
<span tabindex='-1'>bar</span>
22+
<span tabindex={{0}}>baz</span>
23+
<button tabindex={{if this.isHidden '-1'}}>baz</button>
24+
<div role='tab' tabindex={{if this.isHidden '-1' '0'}}>baz</div>
4525
```
4626

47-
```gjs
48-
<template>
49-
<div tabindex="-1">Content</div>
50-
</template>
51-
```
27+
This rule **forbids** the following:
5228

53-
```gjs
54-
<template>
55-
<button>Click</button>
56-
</template>
29+
```hbs
30+
<span tabindex='5'>foo</span>
31+
<span tabindex='3'>bar</span>
32+
<span tabindex={{dynamicValue}}>zoo</span>
33+
<span tabindex='1'>baz</span>
34+
<span tabindex='2'>never really sure what goes after baz</span>
5735
```
5836

59-
## When Not To Use It
60-
61-
This rule should generally always be enabled for accessibility. However, if you have a specific use case where positive tabindex values are necessary and well-tested, you may disable it.
62-
6337
## References
6438

65-
- [eslint-plugin-ember template-no-positive-tabindex](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-positive-tabindex.md)
66-
- [MDN tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex)
67-
- [WebAIM: Keyboard Accessibility](https://webaim.org/techniques/keyboard/tabindex)
39+
1. [AX_FOCUS_03](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_focus_03)
40+
1. [w3.org/TR/wai-aria-practices/#kbd_general_between](https://www.w3.org/TR/wai-aria-practices/#kbd_general_between)
41+
1. [w3.org/TR/2009/WD-wai-aria-practices-20090224/#focus_tabindex](https://www.w3.org/TR/2009/WD-wai-aria-practices-20090224/#focus_tabindex)
42+
1. [MDN: tabindex documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex)

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

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,91 @@
11
/**
2-
* Check if a tabindex value is statically verifiable as safe (0 or negative).
3-
* Returns true only if we can confirm the value is not positive.
4-
* Dynamic, non-numeric, or boolean values are considered unsafe.
2+
* Check a tabindex attribute value and return the violation type, if any.
3+
* Returns null if safe, 'positive' if the value is a positive integer,
4+
* or 'mustBeNegativeNumeric' if the value is non-numeric/dynamic/boolean.
55
*/
6-
function isTabindexSafe(attrValue) {
6+
function getTabindexViolation(attrValue) {
77
if (!attrValue) {
8-
return true;
8+
return null;
99
}
1010

1111
// Handle simple text values like tabindex="0" or tabindex="-1"
1212
if (attrValue.type === 'GlimmerTextNode') {
1313
const value = Number.parseInt(attrValue.chars, 10);
14-
return !Number.isNaN(value) && value <= 0;
14+
if (Number.isNaN(value)) {
15+
return 'mustBeNegativeNumeric';
16+
}
17+
return value > 0 ? 'positive' : null;
1518
}
1619

1720
// Handle mustache statements like tabindex={{-1}} or tabindex={{someProperty}}
1821
if (attrValue.type === 'GlimmerMustacheStatement') {
1922
const path = attrValue.path;
2023

2124
if (path.type === 'GlimmerNumberLiteral') {
22-
return Number.parseInt(path.original, 10) <= 0;
25+
return Number.parseInt(path.original, 10) > 0 ? 'positive' : null;
2326
}
2427
if (path.type === 'GlimmerStringLiteral') {
2528
const value = Number.parseInt(path.original, 10);
26-
return !Number.isNaN(value) && value <= 0;
29+
if (Number.isNaN(value)) {
30+
return 'mustBeNegativeNumeric';
31+
}
32+
return value > 0 ? 'positive' : null;
2733
}
2834

2935
// Handle conditional expressions like {{if this.show -1 0}}
3036
if (
3137
path.type === 'GlimmerPathExpression' &&
3238
(path.original === 'if' || path.original === 'unless')
3339
) {
34-
return isConditionalTabindexSafe(attrValue.params);
40+
return getConditionalTabindexViolation(attrValue.params);
3541
}
3642

3743
// Any other dynamic value (variable, boolean, etc.) is not verifiably safe
38-
return false;
44+
return 'mustBeNegativeNumeric';
3945
}
4046

4147
// Handle concat statements like tabindex="{{-1}}" or tabindex="{{false}}"
4248
if (attrValue.type === 'GlimmerConcatStatement') {
4349
const parts = attrValue.parts || [];
4450
if (parts.length > 0 && parts[0].type === 'GlimmerMustacheStatement') {
45-
return isTabindexSafe(parts[0]);
51+
return getTabindexViolation(parts[0]);
4652
}
47-
return false;
53+
return 'mustBeNegativeNumeric';
4854
}
4955

50-
return false;
56+
return 'mustBeNegativeNumeric';
5157
}
5258

5359
/**
5460
* Check that all branches of a conditional (if/unless) expression are safe.
5561
*/
56-
function isConditionalTabindexSafe(params) {
62+
function getConditionalTabindexViolation(params) {
5763
if (!params) {
58-
return false;
64+
return 'mustBeNegativeNumeric';
5965
}
6066

6167
// Check the value branches (params[1] and optionally params[2])
6268
for (let i = 1; i < params.length && i < 3; i++) {
6369
const param = params[i];
6470
if (param.type === 'GlimmerNumberLiteral') {
6571
if (Number.parseInt(param.original, 10) > 0) {
66-
return false;
72+
return 'positive';
6773
}
6874
} else if (param.type === 'GlimmerStringLiteral') {
6975
const val = Number.parseInt(param.original, 10);
70-
if (Number.isNaN(val) || val > 0) {
71-
return false;
76+
if (Number.isNaN(val)) {
77+
return 'mustBeNegativeNumeric';
78+
}
79+
if (val > 0) {
80+
return 'positive';
7281
}
7382
} else {
7483
// Dynamic value in branch — not verifiably safe
75-
return false;
84+
return 'mustBeNegativeNumeric';
7685
}
7786
}
7887

79-
return true;
88+
return null;
8089
}
8190

8291
/** @type {import('eslint').Rule.RuleModule} */
@@ -93,6 +102,7 @@ module.exports = {
93102
schema: [],
94103
messages: {
95104
positive: 'Avoid positive integer values for tabindex.',
105+
mustBeNegativeNumeric: 'Tabindex values must be negative numeric.',
96106
},
97107
originallyFrom: {
98108
name: 'ember-template-lint',
@@ -111,10 +121,11 @@ module.exports = {
111121
return;
112122
}
113123

114-
if (!isTabindexSafe(tabindexAttr.value)) {
124+
const violation = getTabindexViolation(tabindexAttr.value);
125+
if (violation) {
115126
context.report({
116127
node: tabindexAttr,
117-
messageId: 'positive',
128+
messageId: violation,
118129
});
119130
}
120131
},

tests/lib/rules/template-no-positive-tabindex.js

Lines changed: 9 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,6 @@ const ruleTester = new RuleTester({
1616

1717
ruleTester.run('template-no-positive-tabindex', rule, {
1818
valid: [
19-
`<template>
20-
<div tabindex="0">Content</div>
21-
</template>`,
22-
`<template>
23-
<div tabindex="-1">Content</div>
24-
</template>`,
25-
`<template>
26-
<button>Click</button>
27-
</template>`,
28-
`<template>
29-
<div>No tabindex</div>
30-
</template>`,
31-
3219
'<template><button tabindex="0"></button></template>',
3320
'<template><button tabindex="-1"></button></template>',
3421
'<template><button tabindex={{-1}}>baz</button></template>',
@@ -44,47 +31,10 @@ ruleTester.run('template-no-positive-tabindex', rule, {
4431
],
4532

4633
invalid: [
47-
{
48-
code: `<template>
49-
<div tabindex="1">Content</div>
50-
</template>`,
51-
output: null,
52-
errors: [
53-
{
54-
message: 'Avoid positive integer values for tabindex.',
55-
type: 'GlimmerAttrNode',
56-
},
57-
],
58-
},
59-
{
60-
code: `<template>
61-
<div tabindex="5">Content</div>
62-
</template>`,
63-
output: null,
64-
errors: [
65-
{
66-
message: 'Avoid positive integer values for tabindex.',
67-
type: 'GlimmerAttrNode',
68-
},
69-
],
70-
},
71-
{
72-
code: `<template>
73-
<button tabindex="2">Click</button>
74-
</template>`,
75-
output: null,
76-
errors: [
77-
{
78-
message: 'Avoid positive integer values for tabindex.',
79-
type: 'GlimmerAttrNode',
80-
},
81-
],
82-
},
83-
8434
{
8535
code: '<template><button tabindex={{someProperty}}></button></template>',
8636
output: null,
87-
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
37+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
8838
},
8939
{
9040
code: '<template><button tabindex="1"></button></template>',
@@ -94,17 +44,17 @@ ruleTester.run('template-no-positive-tabindex', rule, {
9444
{
9545
code: '<template><button tabindex="text"></button></template>',
9646
output: null,
97-
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
47+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
9848
},
9949
{
10050
code: '<template><button tabindex={{true}}></button></template>',
10151
output: null,
102-
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
52+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
10353
},
10454
{
10555
code: '<template><button tabindex="{{false}}"></button></template>',
10656
output: null,
107-
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
57+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
10858
},
10959
{
11060
code: '<template><button tabindex="{{5}}"></button></template>',
@@ -149,7 +99,7 @@ const hbsRuleTester = new RuleTester({
14999
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
150100
});
151101

152-
hbsRuleTester.run('template-no-positive-tabindex (hbs)', rule, {
102+
hbsRuleTester.run('template-no-positive-tabindex', rule, {
153103
valid: [
154104
'<button tabindex="0"></button>',
155105
'<button tabindex="-1"></button>',
@@ -168,7 +118,7 @@ hbsRuleTester.run('template-no-positive-tabindex (hbs)', rule, {
168118
{
169119
code: '<button tabindex={{someProperty}}></button>',
170120
output: null,
171-
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
121+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
172122
},
173123
{
174124
code: '<button tabindex="1"></button>',
@@ -178,17 +128,17 @@ hbsRuleTester.run('template-no-positive-tabindex (hbs)', rule, {
178128
{
179129
code: '<button tabindex="text"></button>',
180130
output: null,
181-
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
131+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
182132
},
183133
{
184134
code: '<button tabindex={{true}}></button>',
185135
output: null,
186-
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
136+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
187137
},
188138
{
189139
code: '<button tabindex="{{false}}"></button>',
190140
output: null,
191-
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
141+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
192142
},
193143
{
194144
code: '<button tabindex="{{5}}"></button>',

0 commit comments

Comments
 (0)