Skip to content

Commit 3b4e492

Browse files
author
Callie Callaway
committed
Allow exception for implicit navigation role in no-redundant-roles rule
1 parent 1b29f50 commit 3b4e492

File tree

4 files changed

+93
-30
lines changed

4 files changed

+93
-30
lines changed

__tests__/src/rules/no-redundant-roles-test.js

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
// -----------------------------------------------------------------------------
1111

1212
import { RuleTester } from 'eslint';
13+
import { configs } from '../../../src/index';
1314
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
1415
import rule from '../../../src/rules/no-redundant-roles';
16+
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
1517

1618
// -----------------------------------------------------------------------------
1719
// Tests
@@ -24,16 +26,41 @@ const expectedError = (element, implicitRole) => ({
2426
type: 'JSXOpeningElement',
2527
});
2628

27-
ruleTester.run('no-redundant-roles', rule, {
29+
const ruleName = 'jsx-a11y/no-redundant-roles';
30+
31+
const alwaysValid = [
32+
{ code: '<div />;' },
33+
{ code: '<button role="main" />' },
34+
{ code: '<MyComponent role="button" />' },
35+
{ code: '<button role={`${foo}button`} />' },
36+
];
37+
38+
const neverValid = [
39+
{ code: '<button role="button" />', errors: [expectedError('button', 'button')] },
40+
{ code: '<body role="DOCUMENT" />', errors: [expectedError('body', 'document')] },
41+
{ code: '<button role={`${undefined}button`} />', errors: [expectedError('button', 'button')] },
42+
];
43+
44+
const recommendedOptions = (configs.recommended.rules[ruleName][1] || {});
45+
46+
ruleTester.run(`${ruleName}:recommended`, rule, {
2847
valid: [
29-
{ code: '<div />;' },
30-
{ code: '<button role="main" />' },
31-
{ code: '<MyComponent role="button" />' },
32-
{ code: '<button role={`${foo}button`} />' },
33-
].map(parserOptionsMapper),
48+
...alwaysValid,
49+
{ code: '<nav role="navigation" />' },
50+
]
51+
.map(ruleOptionsMapperFactory(recommendedOptions))
52+
.map(parserOptionsMapper),
53+
invalid: neverValid
54+
.map(ruleOptionsMapperFactory(recommendedOptions))
55+
.map(parserOptionsMapper),
56+
});
57+
58+
ruleTester.run(`${ruleName}:strict`, rule, {
59+
valid: alwaysValid
60+
.map(parserOptionsMapper),
3461
invalid: [
35-
{ code: '<button role="button" />', errors: [expectedError('button', 'button')] },
36-
{ code: '<body role="DOCUMENT" />', errors: [expectedError('body', 'document')] },
37-
{ code: '<button role={`${undefined}button`} />', errors: [expectedError('button', 'button')] },
38-
].map(parserOptionsMapper),
62+
...neverValid,
63+
{ code: '<nav role="navigation" />', errors: [expectedError('nav', 'navigation')] },
64+
]
65+
.map(parserOptionsMapper),
3966
});

docs/rules/no-redundant-roles.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,23 @@ Some HTML elements have native semantics that are implemented by the browser. Th
77

88
## Rule details
99

10-
This rule takes no arguments.
10+
The recommended options for this rule allow an implicit role of `navigation` to be applied to a `nav` element as is [advised by w3](https://www.w3.org/WAI/GL/wiki/Using_HTML5_nav_element#Example:The_.3Cnav.3E_element). The options are provided as an object keyed by HTML element name; the value is an array of implicit ARIA roles that are allowed on the specified element.
11+
12+
```
13+
{
14+
'jsx-a11y/no-redundant-roles': [
15+
'error',
16+
{
17+
nav: ['navigation'],
18+
},
19+
}
20+
```
21+
22+
Under the recommended options, the following code is valid. It would be invalid under the strict rules.
23+
24+
```
25+
<nav role="navigation" />
26+
```
1127

1228
### Succeed
1329
```jsx

src/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,12 @@ module.exports = {
137137
},
138138
],
139139
'jsx-a11y/no-onchange': 'error',
140-
'jsx-a11y/no-redundant-roles': 'error',
140+
'jsx-a11y/no-redundant-roles': [
141+
'error',
142+
{
143+
nav: ['navigation'],
144+
},
145+
],
141146
'jsx-a11y/no-static-element-interactions': [
142147
'error',
143148
{

src/rules/no-redundant-roles.js

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
* @fileoverview Enforce explicit role property is not the
33
* same as implicit/default role property on element.
44
* @author Ethan Cohen <@evcohen>
5+
* @flow
56
*/
67

78
// ----------------------------------------------------------------------------
89
// Rule Definition
910
// ----------------------------------------------------------------------------
1011

1112
import { elementType } from 'jsx-ast-utils';
13+
import includes from 'array-includes';
14+
import type { JSXOpeningElement } from 'ast-types-flow';
15+
import type { ESLintContext } from '../../flow/eslint';
1216
import { generateObjSchema } from '../util/schemas';
1317
import getExplicitRole from '../util/getExplicitRole';
1418
import getImplicitRole from '../util/getImplicitRole';
@@ -26,22 +30,33 @@ module.exports = {
2630
schema: [schema],
2731
},
2832

29-
create: context => ({
30-
JSXOpeningElement: (node) => {
31-
const type = elementType(node);
32-
const implicitRole = getImplicitRole(type, node.attributes);
33-
const explicitRole = getExplicitRole(type, node.attributes);
34-
35-
if (!implicitRole || !explicitRole) {
36-
return;
37-
}
38-
39-
if (implicitRole === explicitRole) {
40-
context.report({
41-
node,
42-
message: errorMessage(type, implicitRole.toLowerCase()),
43-
});
44-
}
45-
},
46-
}),
33+
create: (context: ESLintContext) => {
34+
const { options } = context;
35+
return {
36+
JSXOpeningElement: (node: JSXOpeningElement) => {
37+
const type = elementType(node);
38+
const implicitRole = getImplicitRole(type, node.attributes);
39+
const explicitRole = getExplicitRole(type, node.attributes);
40+
41+
if (!implicitRole || !explicitRole) {
42+
return;
43+
}
44+
45+
if (implicitRole === explicitRole) {
46+
const allowedRoles = (options[0] || {});
47+
if (
48+
Object.prototype.hasOwnProperty.call(allowedRoles, type) &&
49+
includes(allowedRoles[type], implicitRole)
50+
) {
51+
return;
52+
}
53+
54+
context.report({
55+
node,
56+
message: errorMessage(type, implicitRole.toLowerCase()),
57+
});
58+
}
59+
},
60+
};
61+
},
4762
};

0 commit comments

Comments
 (0)