Skip to content

Commit 35370e0

Browse files
committed
Adding tag and role config overrides to no-noninteractive-tabindex rule.
1 parent aca0eab commit 35370e0

File tree

3 files changed

+82
-36
lines changed

3 files changed

+82
-36
lines changed

__tests__/src/rules/no-noninteractive-tabindex-test.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@
99
// -----------------------------------------------------------------------------
1010

1111
import { RuleTester } from 'eslint';
12+
import { configs } from '../../../src/index';
1213
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
1314
import rule from '../../../src/rules/no-noninteractive-tabindex';
15+
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
1416

1517
// -----------------------------------------------------------------------------
1618
// Tests
1719
// -----------------------------------------------------------------------------
1820

1921
const ruleTester = new RuleTester();
2022

23+
const ruleName = 'no-noninteractive-tabindex';
24+
2125
const expectedError = {
2226
message: '`tabIndex` should only be declared on interactive elements.',
2327
type: 'JSXAttribute',
@@ -42,11 +46,30 @@ const neverValid = [
4246
{ code: '<article tabIndex={0} />', errors: [expectedError] },
4347
];
4448

45-
ruleTester.run('no-noninteractive-tabindex', rule, {
49+
const recommendedOptions = (
50+
configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {}
51+
);
52+
53+
ruleTester.run(`${ruleName}:recommended`, rule, {
54+
valid: [
55+
...alwaysValid,
56+
{ code: '<div role="tabpanel" tabIndex="0" />' },
57+
]
58+
.map(ruleOptionsMapperFactory(recommendedOptions))
59+
.map(parserOptionsMapper),
60+
invalid: [
61+
...neverValid,
62+
]
63+
.map(ruleOptionsMapperFactory(recommendedOptions))
64+
.map(parserOptionsMapper),
65+
});
66+
67+
ruleTester.run(`${ruleName}:strict`, rule, {
4668
valid: [
4769
...alwaysValid,
4870
].map(parserOptionsMapper),
4971
invalid: [
5072
...neverValid,
73+
{ code: '<div role="tabpanel" tabIndex="0" />', errors: [expectedError] },
5174
].map(parserOptionsMapper),
5275
});

src/index.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,13 @@ module.exports = {
101101
td: ['gridcell'],
102102
},
103103
],
104-
'jsx-a11y/no-noninteractive-tabindex': 'error',
104+
'jsx-a11y/no-noninteractive-tabindex': [
105+
'error',
106+
{
107+
tags: [],
108+
roles: ['tabpanel'],
109+
}
110+
],
105111
'jsx-a11y/no-onchange': 'error',
106112
'jsx-a11y/no-redundant-roles': 'error',
107113
'jsx-a11y/no-static-element-interactions': 'warn',
@@ -142,6 +148,7 @@ module.exports = {
142148
'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
143149
'jsx-a11y/no-noninteractive-element-interactions': 'error',
144150
'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error',
151+
'jsx-a11y/no-noninteractive-tabindex': 'error',
145152
'jsx-a11y/no-onchange': 'error',
146153
'jsx-a11y/no-redundant-roles': 'error',
147154
'jsx-a11y/no-static-element-interactions': 'warn',

src/rules/no-noninteractive-tabindex.js

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -32,41 +32,57 @@ module.exports = {
3232
schema: [schema],
3333
},
3434

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'),
35+
create: (context: ESLintContext) => {
36+
const options = context.options;
37+
return {
38+
JSXOpeningElement: (
39+
node: JSXOpeningElement,
40+
) => {
41+
const type = elementType(node);
42+
const attributes = node.attributes;
43+
const tabIndexProp = getProp(attributes, 'tabIndex');
44+
const tabIndex = getLiteralPropValue(tabIndexProp);
45+
// Early return;
46+
if (typeof tabIndex === 'undefined') {
47+
return;
48+
}
49+
const role = getLiteralPropValue(
50+
getProp(node.attributes, 'role'),
4851
);
4952

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
53+
54+
if (!dom.has(type)) {
55+
// Do not test higher level JSX components, as we do not know what
56+
// low-level DOM element this maps to.
57+
return;
58+
}
59+
// Allow for configuration overrides.
60+
const {
61+
tags,
62+
roles,
63+
} = (options[0] || {});
64+
if (
65+
(tags && tags.includes(type))
66+
|| (roles && roles.includes(role))
67+
) {
68+
return;
69+
}
70+
if (
71+
isInteractiveElement(type, attributes)
72+
|| isInteractiveRole(type, attributes)
6473
) {
65-
context.report({
66-
node: attribute,
67-
message: errorMessage,
68-
});
69-
}
70-
},
71-
}),
74+
return;
75+
}
76+
if (
77+
!isNaN(Number.parseInt(tabIndex, 10))
78+
&& tabIndex >= 0
79+
) {
80+
context.report({
81+
node: tabIndexProp,
82+
message: errorMessage,
83+
});
84+
}
85+
},
86+
};
87+
},
7288
};

0 commit comments

Comments
 (0)