Skip to content

Commit 9b9745c

Browse files
authored
Merge pull request #386 from calinoracation/no-redundant-roles-exception
Allow exception for implicit navigation role in no-redundant-roles rule
2 parents 1b29f50 + dbdc50e commit 9b9745c

7 files changed

+104
-41
lines changed

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

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import { RuleTester } from 'eslint';
1313
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
1414
import rule from '../../../src/rules/no-redundant-roles';
15+
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
1516

1617
// -----------------------------------------------------------------------------
1718
// Tests
@@ -24,16 +25,41 @@ const expectedError = (element, implicitRole) => ({
2425
type: 'JSXOpeningElement',
2526
});
2627

27-
ruleTester.run('no-redundant-roles', rule, {
28+
const ruleName = 'jsx-a11y/no-redundant-roles';
29+
30+
const alwaysValid = [
31+
{ code: '<div />;' },
32+
{ code: '<button role="main" />' },
33+
{ code: '<MyComponent role="button" />' },
34+
{ code: '<button role={`${foo}button`} />' },
35+
];
36+
37+
const neverValid = [
38+
{ code: '<button role="button" />', errors: [expectedError('button', 'button')] },
39+
{ code: '<body role="DOCUMENT" />', errors: [expectedError('body', 'document')] },
40+
{ code: '<button role={`${undefined}button`} />', errors: [expectedError('button', 'button')] },
41+
];
42+
43+
ruleTester.run(`${ruleName}:recommended`, rule, {
2844
valid: [
29-
{ code: '<div />;' },
30-
{ code: '<button role="main" />' },
31-
{ code: '<MyComponent role="button" />' },
32-
{ code: '<button role={`${foo}button`} />' },
33-
].map(parserOptionsMapper),
45+
...alwaysValid,
46+
{ code: '<nav role="navigation" />' },
47+
]
48+
.map(parserOptionsMapper),
49+
invalid: neverValid
50+
.map(parserOptionsMapper),
51+
});
52+
53+
const noNavExceptionsOptions = { nav: [] };
54+
55+
ruleTester.run(`${ruleName}:recommended`, rule, {
56+
valid: alwaysValid
57+
.map(ruleOptionsMapperFactory(noNavExceptionsOptions))
58+
.map(parserOptionsMapper),
3459
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),
60+
...neverValid,
61+
{ code: '<nav role="navigation" />', errors: [expectedError('nav', 'navigation')] },
62+
]
63+
.map(ruleOptionsMapperFactory(noNavExceptionsOptions))
64+
.map(parserOptionsMapper),
3965
});

docs/rules/no-redundant-roles.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,17 @@ 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 default 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+
```
1121

1222
### Succeed
1323
```jsx

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"axobject-query": "^1.0.1",
6767
"damerau-levenshtein": "^1.0.0",
6868
"emoji-regex": "^6.1.0",
69+
"has": "^1.0.1",
6970
"jsx-ast-utils": "^2.0.0"
7071
},
7172
"peerDependencies": {

src/rules/no-interactive-element-to-noninteractive-role.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from 'jsx-ast-utils';
2121
import type { JSXIdentifier } from 'ast-types-flow';
2222
import includes from 'array-includes';
23+
import has from 'has';
2324
import type { ESLintContext } from '../../flow/eslint';
2425
import type { ESLintJSXAttribute } from '../../flow/eslint-jsx';
2526
import isInteractiveElement from '../util/isInteractiveElement';
@@ -69,10 +70,7 @@ module.exports = {
6970
// Allow overrides from rule configuration for specific elements and
7071
// roles.
7172
const allowedRoles = (options[0] || {});
72-
if (
73-
Object.prototype.hasOwnProperty.call(allowedRoles, type)
74-
&& includes(allowedRoles[type], role)
75-
) {
73+
if (has(allowedRoles, type) && includes(allowedRoles[type], role)) {
7674
return;
7775
}
7876
if (

src/rules/no-noninteractive-element-interactions.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from 'jsx-ast-utils';
2121
import type { JSXOpeningElement } from 'ast-types-flow';
2222
import includes from 'array-includes';
23+
import has from 'has';
2324
import type { ESLintContext } from '../../flow/eslint';
2425
import { arraySchema, generateObjSchema } from '../util/schemas';
2526
import isAbstractRole from '../util/isAbstractRole';
@@ -61,7 +62,7 @@ module.exports = {
6162
const config = (options[0] || {});
6263
const interactiveProps = config.handlers || defaultInteractiveProps;
6364
// Allow overrides from rule configuration for specific elements and roles.
64-
if (Object.prototype.hasOwnProperty.call(config, type)) {
65+
if (has(config, type)) {
6566
attributes = attributes.filter(attr => !includes(config[type], propName(attr)));
6667
}
6768

src/rules/no-noninteractive-element-to-interactive-role.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
JSXIdentifier,
2121
} from 'ast-types-flow';
2222
import includes from 'array-includes';
23+
import has from 'has';
2324
import type { ESLintContext } from '../../flow/eslint';
2425
import type { ESLintJSXAttribute } from '../../flow/eslint-jsx';
2526
import getExplicitRole from '../util/getExplicitRole';
@@ -69,10 +70,7 @@ module.exports = {
6970
// Allow overrides from rule configuration for specific elements and
7071
// roles.
7172
const allowedRoles = (options[0] || {});
72-
if (
73-
Object.prototype.hasOwnProperty.call(allowedRoles, type)
74-
&& includes(allowedRoles[type], role)
75-
) {
73+
if (has(allowedRoles, type) && includes(allowedRoles[type], role)) {
7674
return;
7775
}
7876
if (

src/rules/no-redundant-roles.js

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,75 @@
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';
12-
import { generateObjSchema } from '../util/schemas';
13+
import includes from 'array-includes';
14+
import has from 'has';
15+
import type { JSXOpeningElement } from 'ast-types-flow';
16+
import type { ESLintContext } from '../../flow/eslint';
1317
import getExplicitRole from '../util/getExplicitRole';
1418
import getImplicitRole from '../util/getImplicitRole';
1519

1620
const errorMessage = (element, implicitRole) =>
1721
`The element ${element} has an implicit role of ${implicitRole}. Defining this explicitly is redundant and should be avoided.`;
1822

19-
const schema = generateObjSchema();
23+
const DEFAULT_ROLE_EXCEPTIONS = { nav: ['navigation'] };
2024

2125
module.exports = {
2226
meta: {
2327
docs: {
2428
url: 'https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules/no-redundant-roles.md',
2529
},
26-
schema: [schema],
30+
schema: [{
31+
type: 'object',
32+
additionalProperties: {
33+
type: 'array',
34+
items: {
35+
type: 'string',
36+
},
37+
uniqueItems: true,
38+
},
39+
}],
2740
},
2841

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-
}),
42+
create: (context: ESLintContext) => {
43+
const { options } = context;
44+
return {
45+
JSXOpeningElement: (node: JSXOpeningElement) => {
46+
const type = elementType(node);
47+
const implicitRole = getImplicitRole(type, node.attributes);
48+
const explicitRole = getExplicitRole(type, node.attributes);
49+
50+
if (!implicitRole || !explicitRole) {
51+
return;
52+
}
53+
54+
if (implicitRole === explicitRole) {
55+
const allowedRedundantRoles = (options[0] || {});
56+
let redundantRolesForElement;
57+
58+
if (has(allowedRedundantRoles, type)) {
59+
redundantRolesForElement = allowedRedundantRoles[type];
60+
} else {
61+
redundantRolesForElement = DEFAULT_ROLE_EXCEPTIONS[type] || [];
62+
}
63+
64+
if (includes(redundantRolesForElement, implicitRole)) {
65+
return;
66+
}
67+
68+
context.report({
69+
node,
70+
message: errorMessage(type, implicitRole.toLowerCase()),
71+
});
72+
}
73+
},
74+
};
75+
},
4776
};

0 commit comments

Comments
 (0)