Skip to content

Commit ddb5532

Browse files
author
Ethan Cohen
committed
[new] - Implement no-unsupported-elements-use-aria rule
This rule enforces that elements that do not support roles and/or aria-* properties do not contain those attributes. Fixes #14
1 parent e05562e commit ddb5532

File tree

7 files changed

+294
-21
lines changed

7 files changed

+294
-21
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Then configure the rules you want to use under the rules section.
7878
- [valid-aria-role](docs/rules/valid-aria-role.md): Enforce that elements with ARIA roles must use a valid, non-abstract ARIA role.
7979
- [valid-aria-proptype](docs/rules/valid-aria-proptype.md): Enforce ARIA state and property values are valid.
8080
- [role-requires-aria](docs/rules/role-requires-aria.md): Enforce that elements with ARIA roles must have all required attributes for that role.
81+
- [no-unsupported-elements-use-aria](docs/rules/no-unsupported-elements-use-aria.md): Enforce that elements that do not support ARIA roles, states and properties do not have those attributes.
8182

8283
## Contributing
8384
Feel free to contribute! I am currently using [Google Chrome's Audit Rules](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules) to map out as rules for this plugin.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# no-unsupported-elements-use-aria
2+
3+
Certain reserved DOM elements do not support ARIA roles, states and properties. This is often because they are not visible, for example `meta`, `html`, `script`, `style`. This rule enforces that these DOM elements do not contain the role and/or aria-* props.
4+
5+
#### References
6+
1. [AX_ARIA_12](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_12)
7+
8+
## Rule details
9+
10+
This rule takes no arguments.
11+
12+
### Succeed
13+
```jsx
14+
<!-- Good: the meta element should not be given any ARIA attributes -->
15+
<meta charset="UTF-8">
16+
```
17+
18+
### Fail
19+
```jsx
20+
<!-- Bad: the meta element should not be given any ARIA attributes -->
21+
<meta charset="UTF-8" aria-hidden="false">
22+
```
23+

src/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ module.exports = {
1313
'valid-aria-role': require('./rules/valid-aria-role'),
1414
'valid-aria-proptypes': require('./rules/valid-aria-proptypes'),
1515
'no-invalid-aria': require('./rules/no-invalid-aria'),
16-
'role-requires-aria': require('./rules/role-requires-aria')
16+
'role-requires-aria': require('./rules/role-requires-aria'),
17+
'no-unsupported-elements-use-aria': require('./rules/no-unsupported-elements-use-aria')
1718
},
1819
configs: {
1920
recommended: {
@@ -34,7 +35,8 @@ module.exports = {
3435
"jsx-a11y/valid-aria-role": 2,
3536
"jsx-a11y/valid-aria-proptypes": 2,
3637
"jsx-a11y/no-invalid-aria": 2,
37-
"jsx-a11y/role-requires-aria": 2
38+
"jsx-a11y/role-requires-aria": 2,
39+
"jsx-a11y/no-unsupported-elements-use-aria": 2
3840
}
3941
}
4042
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* @fileoverview Enforce that elements that do not support ARIA roles, states and properties do not have those attributes.
3+
* @author Ethan Cohen
4+
*/
5+
'use strict';
6+
7+
// ----------------------------------------------------------------------------
8+
// Rule Definition
9+
// ----------------------------------------------------------------------------
10+
11+
import DOM from '../util/attributes/DOM';
12+
import ARIA from '../util/attributes/ARIA';
13+
import hasAttribute from '../util/hasAttribute';
14+
import getNodeType from '../util/getNodeType';
15+
16+
const errorMessage = 'This element does not support ARIA roles, states and properties.';
17+
18+
module.exports = context => ({
19+
JSXOpeningElement: node => {
20+
const nodeType = getNodeType(node);
21+
const nodeAttrs = DOM[nodeType.toUpperCase()];
22+
const isReservedNodeType = nodeAttrs && nodeAttrs.reserved || false;
23+
24+
// If it's not reserved, then it can have ARIA-* roles, states, and properties
25+
if (isReservedNodeType === false) {
26+
return;
27+
}
28+
29+
// Check if it has role attribute;
30+
const hasRole = hasAttribute(node.attributes, 'role');
31+
const hasAria = Object.keys(ARIA).some(prop => hasAttribute(node.attributes, prop.toLowerCase()));
32+
33+
if (hasRole || hasAria) {
34+
context.report({
35+
node,
36+
message: errorMessage
37+
});
38+
}
39+
}
40+
});
41+
42+
module.exports.schema = [
43+
{ type: 'object' }
44+
];

src/util/attributes/DOM.json

Lines changed: 145 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,145 @@
1-
[
2-
"a", "abbr", "address", "area", "article",
3-
"aside", "audio", "b", "base", "bdi", "bdo", "big",
4-
"blockquote", "body", "br", "button", "canvas", "caption",
5-
"cite", "code", "col", "colgroup", "data", "datalist",
6-
"dd", "del", "details", "dfn", "dialog", "div", "dl", "dt",
7-
"em", "embed", "fieldset", "figcaption", "figure", "footer",
8-
"form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header",
9-
"hgroup", "hr", "html", "i", "iframe", "img", "input", "ins",
10-
"kbd", "keygen", "label", "legend", "li", "link", "main", "map",
11-
"mark", "menu", "menuitem", "meta", "meter", "nav", "noscript",
12-
"object", "ol", "optgroup", "option", "output", "p", "param",
13-
"picture", "pre", "progress", "q", "rp", "rt", "ruby", "s",
14-
"samp", "script", "section", "select", "small", "source", "span",
15-
"strong", "style", "sub", "summary", "sup", "table", "tbody",
16-
"td", "textarea", "tfoot", "th", "thead", "time", "title", "tr",
17-
"track", "u", "ul", "var", "video", "wbr"
18-
]
1+
{
2+
"A": {},
3+
"ABBR": {},
4+
"ADDRESS": {},
5+
"AREA": {},
6+
"ARTICLE": {},
7+
"ASIDE": {},
8+
"AUDIO": {},
9+
"B": {},
10+
"BASE": {
11+
"reserved": true
12+
},
13+
"BDI": {},
14+
"BDO": {},
15+
"BIG": {},
16+
"BLOCKQUOTE": {},
17+
"BODY": {},
18+
"BR": {},
19+
"BUTTON": {},
20+
"CANVAS": {},
21+
"CAPTION": {},
22+
"CITE": {},
23+
"CODE": {},
24+
"COL": {
25+
"reserved": true
26+
},
27+
"COLGROUP": {
28+
"reserved": true
29+
},
30+
"DATA": {},
31+
"DATALIST": {},
32+
"DD": {},
33+
"DEL": {},
34+
"DETAILS": {},
35+
"DFN": {},
36+
"DIALOG": {},
37+
"DIV": {},
38+
"DL": {},
39+
"DT": {},
40+
"EM": {},
41+
"EMBED": {},
42+
"FIELDSET": {},
43+
"FIGCAPTION": {},
44+
"FIGURE": {},
45+
"FOOTER": {},
46+
"FORM": {},
47+
"H1": {},
48+
"H2": {},
49+
"H3": {},
50+
"H4": {},
51+
"H5": {},
52+
"H6": {},
53+
"HEAD": {
54+
"reserved": true
55+
},
56+
"HEADER": {},
57+
"HGROUP": {},
58+
"HR": {},
59+
"HTML": {
60+
"reserved": true
61+
},
62+
"I": {},
63+
"IFRAME": {},
64+
"IMG": {},
65+
"INPUT": {},
66+
"INS": {},
67+
"KBD": {},
68+
"KEYGEN": {},
69+
"LABEL": {},
70+
"LEGEND": {},
71+
"LI": {},
72+
"LINK": {
73+
"reserved": true
74+
},
75+
"MAIN": {},
76+
"MAP": {},
77+
"MARK": {},
78+
"MENU": {},
79+
"MENUITEM": {},
80+
"META": {
81+
"reserved": true
82+
},
83+
"METER": {},
84+
"NAV": {},
85+
"NOSCRIPT": {
86+
"reserved": true
87+
},
88+
"OBJECT": {},
89+
"OL": {},
90+
"OPTGROUP": {},
91+
"OPTION": {},
92+
"OUTPUT": {},
93+
"P": {},
94+
"PARAM": {
95+
"reserved": true
96+
},
97+
"PICTURE": {
98+
"reserved": true
99+
},
100+
"PRE": {},
101+
"PROGRESS": {},
102+
"Q": {},
103+
"RP": {},
104+
"RT": {},
105+
"RUBY": {},
106+
"S": {},
107+
"SAMP": {},
108+
"SCRIPT": {
109+
"reserved": true
110+
},
111+
"SECTION": {},
112+
"SELECT": {},
113+
"SMALL": {},
114+
"SOURCE": {
115+
"reserved": true
116+
},
117+
"SPAN": {},
118+
"STRONG": {},
119+
"STYLE": {
120+
"reserved": true
121+
},
122+
"SUB": {},
123+
"SUMMARY": {},
124+
"SUP": {},
125+
"TABLE": {},
126+
"TBODY": {},
127+
"TD": {},
128+
"TEXTAREA": {},
129+
"TFOOT": {},
130+
"TH": {},
131+
"THEAD": {},
132+
"TIME": {},
133+
"TITLE": {
134+
"reserved": true
135+
},
136+
"TR": {},
137+
"TRACK": {
138+
"reserved": true
139+
},
140+
"U": {},
141+
"UL": {},
142+
"VAR": {},
143+
"VIDEO": {},
144+
"WBR": {}
145+
}

src/util/isInteractiveElement.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const interactiveMap = {
2929
const isInteractiveElement = (tagName, attributes) => {
3030
// Do not test higher level JSX components, as we do not know what
3131
// low-level DOM element this maps to.
32-
if (DOMElements.indexOf(tagName) === -1) {
32+
if (Object.keys(DOMElements).indexOf(tagName.toUpperCase()) === -1) {
3333
return true;
3434
}
3535

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @fileoverview Enforce that elements that do not support ARIA roles, states and properties do not have those attributes.
3+
* @author Ethan Cohen
4+
*/
5+
6+
'use strict';
7+
8+
// -----------------------------------------------------------------------------
9+
// Requirements
10+
// -----------------------------------------------------------------------------
11+
12+
import rule from '../../../src/rules/no-unsupported-elements-use-aria';
13+
import { RuleTester } from 'eslint';
14+
15+
const parserOptions = {
16+
ecmaVersion: 6,
17+
ecmaFeatures: {
18+
jsx: true
19+
}
20+
};
21+
22+
// -----------------------------------------------------------------------------
23+
// Tests
24+
// -----------------------------------------------------------------------------
25+
26+
const ruleTester = new RuleTester();
27+
28+
import DOM from '../../../src/util/attributes/DOM';
29+
30+
const errorMessage = {
31+
message: 'This element does not support ARIA roles, states and properties.',
32+
type: 'JSXOpeningElement'
33+
};
34+
35+
// Generate valid test cases
36+
const roleValidityTests = Object.keys(DOM).map(element => {
37+
const isReserved = DOM[element].reserved || false;
38+
const role = isReserved ? '' : 'role';
39+
40+
return {
41+
code: `<${element.toLowerCase()} ${role} />`,
42+
parserOptions
43+
};
44+
});
45+
46+
const ariaValidityTests = Object.keys(DOM).map(element => {
47+
const isReserved = DOM[element].reserved || false;
48+
const aria = isReserved ? '' : 'aria-hidden';
49+
50+
return {
51+
code: `<${element.toLowerCase()} ${aria} />`,
52+
parserOptions
53+
};
54+
});
55+
56+
// Generate invalid test cases.
57+
const invalidRoleValidityTests = Object.keys(DOM)
58+
.filter(element => Boolean(DOM[element].reserved))
59+
.map(reservedElem => ({
60+
code: `<${reservedElem.toLowerCase()} role />`,
61+
errors: [ errorMessage ],
62+
parserOptions
63+
}));
64+
65+
const invalidAriaValidityTests = Object.keys(DOM)
66+
.filter(element => Boolean(DOM[element].reserved))
67+
.map(reservedElem => ({
68+
code: `<${reservedElem.toLowerCase()} aria-hidden />`,
69+
errors: [ errorMessage ],
70+
parserOptions
71+
}));
72+
73+
ruleTester.run('no-unsupported-elements-use-aria', rule, {
74+
valid: roleValidityTests.concat(ariaValidityTests),
75+
invalid: invalidRoleValidityTests.concat(invalidAriaValidityTests)
76+
});

0 commit comments

Comments
 (0)