Skip to content

Commit 00ae7e4

Browse files
committed
Implement valid-aria-role rule.
1 parent ad897ae commit 00ae7e4

File tree

7 files changed

+163
-3
lines changed

7 files changed

+163
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Then configure the rules you want to use under the rules section.
7474
- [label-uses-for](docs/rules/label-uses-for.md): Enforce that label elements have the htmlFor attribute
7575
- [redundant-alt](docs/rules/redundant-alt.md): Enforce img alt attribute does not contain the word image, picture, or photo.
7676
- [no-hash-href](docs/rules/no-hash-href.md): Enforce an anchor element's href prop value is not just #.
77+
- [valid-aria-role](docs/rules/valid-aria-role.md): Enforce that elements with ARIA roles must use a valid, non-abstract ARIA role.
7778

7879
## License
7980

docs/rules/valid-aria-role.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# valid-aria-role
2+
3+
Elements with ARIA roles must use a valid, non-abstract ARIA role. A reference to all role defintions can be found at [WAI-ARIA](https://www.w3.org/TR/wai-aria/roles#role_definitions) site.
4+
5+
## Rule details
6+
7+
This rule takes no arguments.
8+
9+
### Succeed
10+
```jsx
11+
<div role="button"></div> <!-- Good: "button" is a valid ARIA role -->
12+
<div role={role}></div> <!-- Good: role is a variable & cannot be determined until runtime. -->
13+
<div></div> <!-- Good: No ARIA role -->
14+
```
15+
16+
### Fail
17+
18+
```jsx
19+
<div role="datepicker"></div> <!-- Bad: "datepicker" is not an ARIA role -->
20+
<div role="range"></div> <!-- Bad: "range" is an _abstract_ ARIA role -->
21+
<div role=""></div> <!-- Bad: An empty ARIA role is not allowed -->
22+
```

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "eslint-plugin-jsx-a11y",
3-
"version": "0.4.3",
3+
"version": "0.5.0",
44
"description": "A static analysis linter of jsx and their accessibility with screen readers.",
55
"keywords": [
66
"eslint",

src/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ module.exports = {
99
'use-onblur-not-onchange': require('./rules/use-onblur-not-onchange'),
1010
'no-access-key': require('./rules/no-access-key'),
1111
'label-uses-for': require('./rules/label-uses-for'),
12-
'no-hash-href': require('./rules/no-hash-href')
12+
'no-hash-href': require('./rules/no-hash-href'),
13+
'valid-aria-role': require('./rules/valid-aria-role')
1314
},
1415
configs: {
1516
recommended: {
@@ -26,7 +27,8 @@ module.exports = {
2627
"jsx-a11y/use-onblur-not-onchange": 2,
2728
"jsx-a11y/no-access-key": 2,
2829
"jsx-a11y/label-uses-for": 2,
29-
"jsx-a11y/no-hash-href": 2
30+
"jsx-a11y/no-hash-href": 2,
31+
"jsx-a11y/valid-aria-role": 2
3032
}
3133
}
3234
}

src/rules/valid-aria-role.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @fileoverview Enforce aria role attribute is valid.
3+
* @author Ethan Cohen
4+
*/
5+
'use strict';
6+
7+
// ----------------------------------------------------------------------------
8+
// Rule Definition
9+
// ----------------------------------------------------------------------------
10+
11+
import validRoleTypes from '../util/validRoleTypes';
12+
import getAttributeValue from '../util/getAttributeValue';
13+
14+
const errorMessage = 'Elements with ARIA roles must use a valid, non-abstract ARIA role.';
15+
16+
module.exports = context => ({
17+
JSXAttribute: attribute => {
18+
const normalizedName = attribute.name.name.toUpperCase();
19+
const normalizedType = attribute.value.type.toUpperCase();
20+
21+
if (normalizedName !== 'ROLE') {
22+
return;
23+
}
24+
25+
// Only check literals, as we cannot enforce variables representing role types.
26+
// Check expression containers to determine null or undefined values.
27+
if (normalizedType === 'JSXEXPRESSIONCONTAINER') {
28+
const expressionValue = getAttributeValue(attribute);
29+
const isUndefinedOrNull = expressionValue === undefined || expressionValue === null;
30+
31+
if (isUndefinedOrNull) {
32+
context.report({
33+
node: attribute,
34+
message: errorMessage
35+
});
36+
}
37+
38+
return;
39+
} else if (normalizedType !== 'LITERAL') {
40+
return;
41+
}
42+
43+
// If value is a literal.
44+
const normalizedValue = attribute.value.value.toUpperCase();
45+
const isValid = validRoleTypes.indexOf(normalizedValue) > -1;
46+
47+
if (isValid === false) {
48+
context.report({
49+
node: attribute,
50+
message: errorMessage
51+
});
52+
}
53+
}
54+
});
55+
56+
module.exports.schema = [
57+
{ type: 'object' }
58+
];

src/util/validRoleTypes.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Taken from https://www.w3.org/TR/wai-aria/roles#role_definitions
2+
3+
export default [
4+
'ALERT', 'ALERTDIALOG', 'APPLICATION', 'ARTICLE',
5+
'BANNER', 'BUTTON', 'CHECKBOX', 'COLUMNHEADER',
6+
'COMBOBOX', 'COMPLEMENTARY',
7+
'CONTENTINFO', 'DEFINITION', 'DIALOG', 'DIRECTORY',
8+
'DOCUMENT', 'FORM', 'GRID', 'GRIDCELL',
9+
'GROUP', 'HEADING', 'IMG', 'LINK', 'LIST', 'LISTBOX',
10+
'LISTITEM', 'LOG', 'MAIN', 'MARQUEE',
11+
'MATH', 'MENU', 'MENUBAR', 'MENUITEM',
12+
'MENUITEMCHECKBOX', 'MENUITEMRADIO', 'NAVIGATION', 'NOTE',
13+
'OPTION', 'PRESENTATION', 'PROGRESSBAR', 'RADIO',
14+
'RADIOGROUP', 'REGION', 'ROW', 'ROWGROUP', 'ROWHEADER',
15+
'SCROLLBAR', 'SEARCH', 'SEPARATOR', 'SLIDER',
16+
'SPINBUTTON', 'STATUS', 'TAB', 'TABLIST', 'TABPANEL',
17+
'TEXTBOX', 'TIMER', 'TOOLBAR', 'TOOLTIP',
18+
'TREE', 'TREEGRID', 'TREEITEM'
19+
];

tests/src/rules/valid-aria-role.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @fileoverview Enforce aria role attribute is valid.
3+
* @author Ethan Cohen
4+
*/
5+
6+
'use strict';
7+
8+
// -----------------------------------------------------------------------------
9+
// Requirements
10+
// -----------------------------------------------------------------------------
11+
12+
import rule from '../../../src/rules/valid-aria-role';
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+
const errorMessage = {
29+
message: 'Elements with ARIA roles must use a valid, non-abstract ARIA role.',
30+
type: 'JSXAttribute'
31+
};
32+
33+
import validRoleTypes from '../../../src/util/validRoleTypes';
34+
35+
// Create basic test cases using all valid role types.
36+
const basicValidityTests = validRoleTypes.map(role => ({
37+
code: `<div role="${role.toLowerCase()}" />`,
38+
parserOptions
39+
}));
40+
41+
ruleTester.run('valid-aria-role', rule, {
42+
valid: [
43+
// Variables should pass, as we are only testing literals.
44+
{ code: '<div />', parserOptions },
45+
{ code: '<div></div>', parserOptions },
46+
{ code: '<div role={role} />', parserOptions },
47+
{ code: '<div role={role || "button"} />', parserOptions },
48+
{ code: '<div role={role || "foobar"} />', parserOptions }
49+
].concat(basicValidityTests),
50+
invalid: [
51+
{ code: '<div role="foobar" />', errors: [ errorMessage ], parserOptions },
52+
{ code: '<div role="datepicker"></div>', errors: [ errorMessage ], parserOptions },
53+
{ code: '<div role="range"></div>', errors: [ errorMessage ], parserOptions },
54+
{ code: '<div role=""></div>', errors: [ errorMessage ], parserOptions },
55+
{ code: '<div role={undefined}></div>', errors: [ errorMessage ], parserOptions },
56+
{ code: '<div role={null}></div>', errors: [ errorMessage ], parserOptions }
57+
]
58+
});

0 commit comments

Comments
 (0)