Skip to content

Commit 53ca4e0

Browse files
Merge pull request #2564 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-positive-tabindex
Extract rule: template-no-positive-tabindex
2 parents 87cbd47 + 25ff021 commit 53ca4e0

4 files changed

Lines changed: 356 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ rules in templates can be disabled with eslint directives with mustache or html
197197
| [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | | | |
198198
| [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | | | |
199199
| [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | | | |
200+
| [template-no-positive-tabindex](docs/rules/template-no-positive-tabindex.md) | disallow positive tabindex values | | | |
200201
| [template-no-redundant-role](docs/rules/template-no-redundant-role.md) | disallow redundant role attributes | | 🔧 | |
201202
| [template-no-unsupported-role-attributes](docs/rules/template-no-unsupported-role-attributes.md) | disallow ARIA attributes that are not supported by the element role | | 🔧 | |
202203
| [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") | | | |
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# ember/template-no-positive-tabindex
2+
3+
<!-- end auto-generated rule header -->
4+
5+
## `<* tabindex>`
6+
7+
[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) explains the motivation of this rule nicely:
8+
9+
> Avoid using tabindex values greater than 0. Doing so makes it difficult for people who rely on assistive technology to navigate and operate page content. Instead, write the document with the elements in a logical sequence.
10+
11+
This rule prevents usage of any `tabindex` values other than `0` and `-1`. It does allow for dynamic values (choosing which value to show based on some condition / helper / etc), but only if that inline `if` condition has static `0`/`-1` as the value.
12+
13+
This rule takes no arguments.
14+
15+
## Examples
16+
17+
This rule **allows** the following:
18+
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>
25+
```
26+
27+
This rule **forbids** the following:
28+
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>
35+
```
36+
37+
## References
38+
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)
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
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.
5+
*/
6+
function getTabindexViolation(attrValue) {
7+
if (!attrValue) {
8+
return null;
9+
}
10+
11+
// Handle simple text values like tabindex="0" or tabindex="-1"
12+
if (attrValue.type === 'GlimmerTextNode') {
13+
const value = Number.parseInt(attrValue.chars, 10);
14+
if (Number.isNaN(value)) {
15+
return 'mustBeNegativeNumeric';
16+
}
17+
return value > 0 ? 'positive' : null;
18+
}
19+
20+
// Handle mustache statements like tabindex={{-1}} or tabindex={{someProperty}}
21+
if (attrValue.type === 'GlimmerMustacheStatement') {
22+
const path = attrValue.path;
23+
24+
if (path.type === 'GlimmerNumberLiteral') {
25+
return Number.parseInt(path.original, 10) > 0 ? 'positive' : null;
26+
}
27+
if (path.type === 'GlimmerStringLiteral') {
28+
const value = Number.parseInt(path.original, 10);
29+
if (Number.isNaN(value)) {
30+
return 'mustBeNegativeNumeric';
31+
}
32+
return value > 0 ? 'positive' : null;
33+
}
34+
35+
// Handle conditional expressions like {{if this.show -1 0}}
36+
if (
37+
path.type === 'GlimmerPathExpression' &&
38+
(path.original === 'if' || path.original === 'unless')
39+
) {
40+
return getConditionalTabindexViolation(attrValue.params);
41+
}
42+
43+
// Any other dynamic value (variable, boolean, etc.) is not verifiably safe
44+
return 'mustBeNegativeNumeric';
45+
}
46+
47+
// Handle concat statements like tabindex="{{-1}}" or tabindex="{{false}}"
48+
if (attrValue.type === 'GlimmerConcatStatement') {
49+
const parts = attrValue.parts || [];
50+
if (parts.length > 0 && parts[0].type === 'GlimmerMustacheStatement') {
51+
return getTabindexViolation(parts[0]);
52+
}
53+
return 'mustBeNegativeNumeric';
54+
}
55+
56+
return 'mustBeNegativeNumeric';
57+
}
58+
59+
/**
60+
* Check that all branches of a conditional (if/unless) expression are safe.
61+
*/
62+
function getConditionalTabindexViolation(params) {
63+
if (!params) {
64+
return 'mustBeNegativeNumeric';
65+
}
66+
67+
// Check the value branches (params[1] and optionally params[2])
68+
for (let i = 1; i < params.length && i < 3; i++) {
69+
const param = params[i];
70+
if (param.type === 'GlimmerNumberLiteral') {
71+
if (Number.parseInt(param.original, 10) > 0) {
72+
return 'positive';
73+
}
74+
} else if (param.type === 'GlimmerStringLiteral') {
75+
const val = Number.parseInt(param.original, 10);
76+
if (Number.isNaN(val)) {
77+
return 'mustBeNegativeNumeric';
78+
}
79+
if (val > 0) {
80+
return 'positive';
81+
}
82+
} else {
83+
// Dynamic value in branch — not verifiably safe
84+
return 'mustBeNegativeNumeric';
85+
}
86+
}
87+
88+
return null;
89+
}
90+
91+
/** @type {import('eslint').Rule.RuleModule} */
92+
module.exports = {
93+
meta: {
94+
type: 'suggestion',
95+
docs: {
96+
description: 'disallow positive tabindex values',
97+
category: 'Accessibility',
98+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-positive-tabindex.md',
99+
templateMode: 'both',
100+
},
101+
fixable: null,
102+
schema: [],
103+
messages: {
104+
positive: 'Avoid positive integer values for tabindex.',
105+
mustBeNegativeNumeric: 'Tabindex values must be negative numeric.',
106+
},
107+
originallyFrom: {
108+
name: 'ember-template-lint',
109+
rule: 'lib/rules/no-positive-tabindex.js',
110+
docs: 'docs/rule/no-positive-tabindex.md',
111+
tests: 'test/unit/rules/no-positive-tabindex-test.js',
112+
},
113+
},
114+
115+
create(context) {
116+
return {
117+
GlimmerElementNode(node) {
118+
const tabindexAttr = node.attributes?.find((attr) => attr.name === 'tabindex');
119+
120+
if (!tabindexAttr || !tabindexAttr.value) {
121+
return;
122+
}
123+
124+
const violation = getTabindexViolation(tabindexAttr.value);
125+
if (violation) {
126+
context.report({
127+
node: tabindexAttr,
128+
messageId: violation,
129+
});
130+
}
131+
},
132+
};
133+
},
134+
};
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
const rule = require('../../../lib/rules/template-no-positive-tabindex');
6+
const RuleTester = require('eslint').RuleTester;
7+
8+
//------------------------------------------------------------------------------
9+
// Tests
10+
//------------------------------------------------------------------------------
11+
12+
const ruleTester = new RuleTester({
13+
parser: require.resolve('ember-eslint-parser'),
14+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
15+
});
16+
17+
ruleTester.run('template-no-positive-tabindex', rule, {
18+
valid: [
19+
'<template><button tabindex="0"></button></template>',
20+
'<template><button tabindex="-1"></button></template>',
21+
'<template><button tabindex={{-1}}>baz</button></template>',
22+
'<template><button tabindex={{"-1"}}>baz</button></template>',
23+
'<template><button tabindex="{{-1}}">baz</button></template>',
24+
'<template><button tabindex="{{"-1"}}">baz</button></template>',
25+
'<template><button tabindex="{{if this.show -1}}">baz</button></template>',
26+
'<template><button tabindex="{{if this.show "-1" "0"}}">baz</button></template>',
27+
'<template><button tabindex="{{if (not this.show) "-1" "0"}}">baz</button></template>',
28+
'<template><button tabindex={{if this.show -1}}>baz</button></template>',
29+
'<template><button tabindex={{if this.show "-1" "0"}}>baz</button></template>',
30+
'<template><button tabindex={{if (not this.show) "-1" "0"}}>baz</button></template>',
31+
],
32+
33+
invalid: [
34+
{
35+
code: '<template><button tabindex={{someProperty}}></button></template>',
36+
output: null,
37+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
38+
},
39+
{
40+
code: '<template><button tabindex="1"></button></template>',
41+
output: null,
42+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
43+
},
44+
{
45+
code: '<template><button tabindex="text"></button></template>',
46+
output: null,
47+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
48+
},
49+
{
50+
code: '<template><button tabindex={{true}}></button></template>',
51+
output: null,
52+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
53+
},
54+
{
55+
code: '<template><button tabindex="{{false}}"></button></template>',
56+
output: null,
57+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
58+
},
59+
{
60+
code: '<template><button tabindex="{{5}}"></button></template>',
61+
output: null,
62+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
63+
},
64+
{
65+
code: '<template><button tabindex="{{if a 1 -1}}"></button></template>',
66+
output: null,
67+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
68+
},
69+
{
70+
code: '<template><button tabindex="{{if a -1 1}}"></button></template>',
71+
output: null,
72+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
73+
},
74+
{
75+
code: '<template><button tabindex="{{if a 1}}"></button></template>',
76+
output: null,
77+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
78+
},
79+
{
80+
code: '<template><button tabindex="{{if (not a) 1}}"></button></template>',
81+
output: null,
82+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
83+
},
84+
{
85+
code: '<template><button tabindex="{{unless a 1}}"></button></template>',
86+
output: null,
87+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
88+
},
89+
{
90+
code: '<template><button tabindex="{{unless a -1 1}}"></button></template>',
91+
output: null,
92+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
93+
},
94+
],
95+
});
96+
97+
const hbsRuleTester = new RuleTester({
98+
parser: require.resolve('ember-eslint-parser/hbs'),
99+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
100+
});
101+
102+
hbsRuleTester.run('template-no-positive-tabindex', rule, {
103+
valid: [
104+
'<button tabindex="0"></button>',
105+
'<button tabindex="-1"></button>',
106+
'<button tabindex={{-1}}>baz</button>',
107+
'<button tabindex={{"-1"}}>baz</button>',
108+
'<button tabindex="{{-1}}">baz</button>',
109+
'<button tabindex="{{"-1"}}">baz</button>',
110+
'<button tabindex="{{if this.show -1}}">baz</button>',
111+
'<button tabindex="{{if this.show "-1" "0"}}">baz</button>',
112+
'<button tabindex="{{if (not this.show) "-1" "0"}}">baz</button>',
113+
'<button tabindex={{if this.show -1}}>baz</button>',
114+
'<button tabindex={{if this.show "-1" "0"}}>baz</button>',
115+
'<button tabindex={{if (not this.show) "-1" "0"}}>baz</button>',
116+
],
117+
invalid: [
118+
{
119+
code: '<button tabindex={{someProperty}}></button>',
120+
output: null,
121+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
122+
},
123+
{
124+
code: '<button tabindex="1"></button>',
125+
output: null,
126+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
127+
},
128+
{
129+
code: '<button tabindex="text"></button>',
130+
output: null,
131+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
132+
},
133+
{
134+
code: '<button tabindex={{true}}></button>',
135+
output: null,
136+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
137+
},
138+
{
139+
code: '<button tabindex="{{false}}"></button>',
140+
output: null,
141+
errors: [{ message: 'Tabindex values must be negative numeric.' }],
142+
},
143+
{
144+
code: '<button tabindex="{{5}}"></button>',
145+
output: null,
146+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
147+
},
148+
{
149+
code: '<button tabindex="{{if a 1 -1}}"></button>',
150+
output: null,
151+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
152+
},
153+
{
154+
code: '<button tabindex="{{if a -1 1}}"></button>',
155+
output: null,
156+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
157+
},
158+
{
159+
code: '<button tabindex="{{if a 1}}"></button>',
160+
output: null,
161+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
162+
},
163+
{
164+
code: '<button tabindex="{{if (not a) 1}}"></button>',
165+
output: null,
166+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
167+
},
168+
{
169+
code: '<button tabindex="{{unless a 1}}"></button>',
170+
output: null,
171+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
172+
},
173+
{
174+
code: '<button tabindex="{{unless a -1 1}}"></button>',
175+
output: null,
176+
errors: [{ message: 'Avoid positive integer values for tabindex.' }],
177+
},
178+
],
179+
});

0 commit comments

Comments
 (0)