Skip to content

Commit efd7d4e

Browse files
committed
Adding the rule no-noninteractive-tabindex
1 parent 7ca7805 commit efd7d4e

File tree

4 files changed

+147
-1
lines changed

4 files changed

+147
-1
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/* eslint-env jest */
2+
/**
3+
* @fileoverview Disallow tabindex on static and noninteractive elements
4+
* @author jessebeach
5+
*/
6+
7+
// -----------------------------------------------------------------------------
8+
// Requirements
9+
// -----------------------------------------------------------------------------
10+
11+
import { RuleTester } from 'eslint';
12+
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
13+
import rule from '../../../src/rules/no-noninteractive-tabindex';
14+
15+
// -----------------------------------------------------------------------------
16+
// Tests
17+
// -----------------------------------------------------------------------------
18+
19+
const ruleTester = new RuleTester();
20+
21+
const expectedError = {
22+
message: 'TabIndex should only be declared on interactive elements.',
23+
type: 'JSXAttribute',
24+
};
25+
26+
const alwaysValid = [
27+
{ code: '<MyButton tabIndex={0} />' },
28+
{ code: '<button />' },
29+
{ code: '<button tabIndex="0" />' },
30+
{ code: '<button tabIndex={0} />' },
31+
{ code: '<div />' },
32+
{ code: '<div tabIndex="-1" />' },
33+
{ code: '<div role="button" tabIndex="0" />' },
34+
{ code: '<div role="article" tabIndex="-1" />' },
35+
{ code: '<article tabIndex="-1" />' },
36+
];
37+
38+
const neverValid = [
39+
{ code: '<div tabIndex="0" />', errors: [expectedError] },
40+
{ code: '<div role="article" tabIndex="0" />', errors: [expectedError] },
41+
{ code: '<article tabIndex="0" />', errors: [expectedError] },
42+
{ code: '<article tabIndex={0} />', errors: [expectedError] },
43+
];
44+
45+
ruleTester.run('no-noninteractive-tabindex', rule, {
46+
valid: [
47+
...alwaysValid,
48+
].map(parserOptionsMapper),
49+
invalid: [
50+
...neverValid,
51+
].map(parserOptionsMapper),
52+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# no-noninteractive-tabindex
2+
3+
Write a useful explanation here!
4+
5+
### References
6+
7+
1.
8+
9+
## Rule details
10+
11+
This rule takes no arguments.
12+
13+
### Succeed
14+
```jsx
15+
<div />
16+
```
17+
18+
### Fail
19+
```jsx
20+
21+
```

src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ module.exports = {
2727
'no-interactive-element-to-noninteractive-role': require('./rules/no-interactive-element-to-noninteractive-role'),
2828
'no-noninteractive-element-interactions': require('./rules/no-noninteractive-element-interactions'),
2929
'no-noninteractive-element-to-interactive-role': require('./rules/no-noninteractive-element-to-interactive-role'),
30+
'no-noninteractive-tabindex': require('./rules/no-noninteractive-tabindex'),
3031
'no-onchange': require('./rules/no-onchange'),
3132
'no-redundant-roles': require('./rules/no-redundant-roles'),
3233
'no-static-element-interactions': require('./rules/no-static-element-interactions'),
@@ -100,7 +101,7 @@ module.exports = {
100101
td: ['gridcell'],
101102
},
102103
],
103-
104+
'jsx-a11y/no-noninteractive-tabindex': 'error',
104105
'jsx-a11y/no-onchange': 'error',
105106
'jsx-a11y/no-redundant-roles': 'error',
106107
'jsx-a11y/no-static-element-interactions': 'warn',
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* @fileoverview Disallow tabindex on static and noninteractive elements
3+
* @author jessebeach
4+
* @flow
5+
*/
6+
7+
// ----------------------------------------------------------------------------
8+
// Rule Definition
9+
// ----------------------------------------------------------------------------
10+
11+
import {
12+
dom,
13+
} from 'aria-query';
14+
import {
15+
elementType,
16+
getProp,
17+
getLiteralPropValue,
18+
propName,
19+
} from 'jsx-ast-utils';
20+
import isInteractiveElement from '../util/isInteractiveElement';
21+
import isInteractiveRole from '../util/isInteractiveRole';
22+
import { generateObjSchema } from '../util/schemas';
23+
24+
const errorMessage =
25+
'TabIndex should only be declared on interactive elements.';
26+
27+
const schema = generateObjSchema();
28+
29+
module.exports = {
30+
meta: {
31+
docs: {},
32+
schema: [schema],
33+
},
34+
35+
create: (context: ESLintContext) => ({
36+
JSXAttribute: (
37+
attribute: ESLintJSXAttribute,
38+
) => {
39+
const attributeName = propName(attribute);
40+
if (attributeName !== 'tabIndex') {
41+
return;
42+
}
43+
const node = attribute.parent;
44+
const attributes = node.attributes;
45+
const type = elementType(node);
46+
const tabIndex = getLiteralPropValue(
47+
getProp(node.attributes, 'tabIndex'),
48+
);
49+
50+
if (!dom.has(type)) {
51+
// Do not test higher level JSX components, as we do not know what
52+
// low-level DOM element this maps to.
53+
return;
54+
}
55+
if (
56+
isInteractiveElement(type, attributes)
57+
|| isInteractiveRole(type, attributes)
58+
) {
59+
return;
60+
}
61+
if (
62+
!isNaN(Number.parseInt(tabIndex, 10))
63+
&& tabIndex >= 0
64+
) {
65+
context.report({
66+
node: attribute,
67+
message: errorMessage,
68+
});
69+
}
70+
},
71+
}),
72+
};

0 commit comments

Comments
 (0)