Skip to content

Commit 6a80cc3

Browse files
committed
no-redundant-roles rule
1 parent e818f7f commit 6a80cc3

12 files changed

+523
-51
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ I'm currently working on getting parity between this project and `eslint-plugin-
6767
- [x] no-autofocus
6868
- [x] no-distracting-elements
6969
- [ ] no-onchange
70-
- [ ] no-redundant-roles
70+
- [x] no-redundant-roles
7171
- [ ] role-has-required-aria-props
7272
- [x] tabindex-no-positive
7373

docs/rules/no-redundant-roles.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# no-redundant-roles
2+
3+
Some HTML elements have native semantics that are implemented by the browser. This includes default/implicit ARIA roles. Setting an ARIA role that matches its default/implicit role is redundant since it is already set by the browser.
4+
5+
_References:_
6+
7+
1. [W3](https://www.w3.org/TR/html5/dom.html#aria-role-attribute)
8+
9+
## Rule details
10+
11+
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.
12+
13+
```json
14+
{
15+
"jsx-a11y/no-redundant-roles": ["error", {
16+
"nav": ["navigation"]
17+
}
18+
}
19+
```
20+
21+
### Succeed
22+
23+
```vue
24+
<div />
25+
<button role="presentation" />
26+
```
27+
28+
### Fail
29+
30+
<!-- eslint-ignore -->
31+
32+
```vue
33+
<button role="button" /> <img role="img" src="foo.jpg" />
34+
```

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"devDependencies": {
2020
"eslint": "^6.8.0",
21+
"eslint-plugin-vue-a11y": "^0.0.31",
2122
"husky": "^4.2.5",
2223
"jest": "^25.4.0",
2324
"prettier": "^2.0.5",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const rule = require("../no-redundant-roles");
2+
const makeRuleTester = require("./makeRuleTester");
3+
4+
makeRuleTester("no-redundant-roles", rule, {
5+
valid: ["<a role='link' />", "<div role='link' />"],
6+
invalid: [
7+
{
8+
code: '<template><img role="img" src="foo.jpg" /></template>',
9+
errors: [{ message: rule.makeMessage("img", "img") }]
10+
},
11+
{
12+
code: '<template><a role="link" href="#" /></template>',
13+
errors: [{ message: rule.makeMessage("a", "link") }]
14+
},
15+
{
16+
code: '<template><button role="button" /></template>',
17+
errors: [{ message: rule.makeMessage("button", "button") }]
18+
}
19+
]
20+
});

src/rules/aria-role.js

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const { dom, roles } = require("aria-query");
22
const {
33
defineTemplateBodyVisitor,
4+
getAttributeValue,
45
getElementType,
56
isAttribute,
67
makeDocsURL
@@ -9,24 +10,6 @@ const {
910
const message =
1011
"Elements with ARIA roles must use a valid, non-abstract ARIA role.";
1112

12-
const getAttributeValue = (node) => {
13-
const { key, value } = node;
14-
15-
if (!value) {
16-
return null;
17-
}
18-
19-
if (!node.directive) {
20-
return value.value;
21-
}
22-
23-
if (key.name.name === "bind" && value.expression) {
24-
return value.expression.value;
25-
}
26-
27-
return null;
28-
};
29-
3013
module.exports = {
3114
meta: {
3215
docs: {

src/rules/no-redundant-roles.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
const { elementRoles } = require("aria-query");
2+
const {
3+
defineTemplateBodyVisitor,
4+
getElementAttributeValue,
5+
getElementType,
6+
makeDocsURL
7+
} = require("../utils");
8+
9+
const exceptions = { nav: ["navigation"] };
10+
const makeMessage = (type, role) => `\
11+
The element ${type} has an implicit role of ${role}. Defining this \
12+
explicitly is redundant and should be avoided.`;
13+
14+
const hasRoleAttributes = (node, attributes) =>
15+
attributes.every((attribute) => {
16+
const value = getElementAttributeValue(node, attribute.name);
17+
18+
if (attribute.value) {
19+
return value === attribute.value;
20+
}
21+
22+
if (attribute.constraints) {
23+
switch (attribute.constraints[0]) {
24+
case "set":
25+
return value;
26+
case "undefined":
27+
return !value;
28+
default:
29+
return null;
30+
}
31+
}
32+
33+
return value;
34+
});
35+
36+
const getImplicitRoleSet = (node) => {
37+
const elementType = getElementType(node);
38+
39+
for (const [key, value] of elementRoles) {
40+
if (key.name === elementType) {
41+
if (!key.attributes || hasRoleAttributes(node, key.attributes)) {
42+
return value;
43+
}
44+
}
45+
}
46+
return null;
47+
};
48+
49+
module.exports = {
50+
getImplicitRoleSet,
51+
meta: {
52+
docs: {
53+
url: makeDocsURL("no-redundant-roles")
54+
},
55+
schema: [
56+
{
57+
type: "object",
58+
additionalProperties: {
59+
type: "array",
60+
items: {
61+
type: "string"
62+
},
63+
uniqueItems: true
64+
}
65+
}
66+
]
67+
},
68+
create(context) {
69+
return defineTemplateBodyVisitor(context, {
70+
VElement(node) {
71+
const type = getElementType(node);
72+
const implicitRoleSet = getImplicitRoleSet(node);
73+
const explicitRole = getElementAttributeValue(node, "role");
74+
75+
if (!implicitRoleSet || !explicitRole) {
76+
return;
77+
}
78+
79+
const permittedRoles = context.options[0] || {};
80+
if (
81+
(permittedRoles[type] || [])
82+
.concat(exceptions[type] || [])
83+
.includes(explicitRole)
84+
) {
85+
return;
86+
}
87+
88+
if (implicitRoleSet.has(explicitRole)) {
89+
context.report({ node, message: makeMessage(type, explicitRole) });
90+
}
91+
}
92+
});
93+
},
94+
makeMessage
95+
};

src/utils.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
const defineTemplateBodyVisitor = require("./utils/defineTemplateBodyVisitor");
2+
const getAttributeValue = require("./utils/getAttributeValue");
3+
const getElementAttribute = require("./utils/getElementAttribute");
4+
const getElementAttributeValue = require("./utils/getElementAttributeValue");
25
const hasContent = require("./utils/hasContent");
36
const isAttribute = require("./utils/isAttribute");
47
const makeDocsURL = require("./utils/makeDocsURL");
@@ -44,6 +47,9 @@ const isAttributeWithValue = (node, name) => {
4447

4548
module.exports = {
4649
defineTemplateBodyVisitor,
50+
getAttributeValue,
51+
getElementAttribute,
52+
getElementAttributeValue,
4753
getElementType,
4854
getLiteralAttributeValue,
4955
hasContent,

src/utils/getAttributeValue.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const getAttributeValue = (node) => {
2+
const { key, value } = node;
3+
4+
if (!value) {
5+
return null;
6+
}
7+
8+
if (!node.directive) {
9+
return value.value;
10+
}
11+
12+
if (key.name.name === "bind" && value.expression) {
13+
return value.type === "Literal" ? value.expression.value : value.expression;
14+
}
15+
16+
return null;
17+
};
18+
19+
module.exports = getAttributeValue;

src/utils/getElementAttribute.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const getElementAttribute = (node, name) => {
2+
for (const attribute of node.startTag.attributes) {
3+
const { key } = attribute;
4+
5+
if (
6+
(!attribute.directive && key.name === name) ||
7+
(attribute.directive &&
8+
key.name.name === "bind" &&
9+
key.argument.name === name)
10+
) {
11+
return attribute;
12+
}
13+
}
14+
15+
return null;
16+
};
17+
18+
module.exports = getElementAttribute;

src/utils/getElementAttributeValue.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const getAttributeValue = require("./getAttributeValue");
2+
const getElementAttribute = require("./getElementAttribute");
3+
4+
const getElementAttributeValue = (node, name) => {
5+
const attribute = getElementAttribute(node, name);
6+
return attribute && getAttributeValue(attribute);
7+
};
8+
9+
module.exports = getElementAttributeValue;

src/utils/hasContent.js

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,7 @@
1-
const getAttributeValue = (node, name) => {
2-
for (const attribute of node.startTag.attributes) {
3-
const { key, value } = attribute;
4-
5-
if (!value) {
6-
continue;
7-
}
8-
9-
if (!attribute.directive && key.name === name) {
10-
return value.value;
11-
}
12-
13-
if (
14-
attribute.directive &&
15-
key.name.name === "bind" &&
16-
key.argument.name === name &&
17-
value.expression
18-
) {
19-
return value.expression.value;
20-
}
21-
}
22-
23-
return null;
24-
};
1+
const getElementAttributeValue = require("./getElementAttributeValue");
252

263
const isHiddenFromScreenReader = (node) => {
27-
const ariaHidden = getAttributeValue(node, "aria-hidden");
4+
const ariaHidden = getElementAttributeValue(node, "aria-hidden");
285
return ariaHidden && ariaHidden.toString() === "true";
296
};
307

0 commit comments

Comments
 (0)