Skip to content

Commit a4910ff

Browse files
committed
Merge branch 'develop' of https://github.com/evcohen/eslint-plugin-jsx-a11y into develop
2 parents 451b666 + ddb5532 commit a4910ff

23 files changed

+971
-244
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,11 @@ 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+
- [no-invalid-aria](docs/rules/no-invalid-aria.md): Enforce all aria-* properties are valid.
7778
- [valid-aria-role](docs/rules/valid-aria-role.md): Enforce that elements with ARIA roles must use a valid, non-abstract ARIA role.
7879
- [valid-aria-proptype](docs/rules/valid-aria-proptype.md): Enforce ARIA state and property values are valid.
80+
- [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.
7982

8083
## Contributing
8184
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.

TODO.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
# TODO
2-
1. Allow `alt=""` if `role="presentation"` on img-uses-alt rule.

docs/rules/no-invalid-aria.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# no-invalid-aria
2+
3+
Elements cannot use an invalid ARIA attribute. This will fail if it finds an `aria-*` property that is not listed in [WAI-ARIA States and Properties spec](https://www.w3.org/TR/wai-aria/states_and_properties#state_prop_def).
4+
5+
## Rule details
6+
7+
This rule takes no arguments.
8+
9+
### Succeed
10+
```jsx
11+
<!-- Good: Labeled using correctly spelled aria-labelledby -->
12+
<div id="address_label">Enter your address</div>
13+
<input aria-labelledby="address_label">
14+
```
15+
16+
### Fail
17+
18+
```jsx
19+
<!-- Bad: Labeled using incorrectly spelled aria-labeledby -->
20+
<div id="address_label">Enter your address</div>
21+
<input aria-labeledby="address_label">
22+
```
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+

docs/rules/role-requires-aria.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# role-requires-aria
2+
3+
Elements with ARIA roles must have all required attributes for that role.
4+
5+
#### References
6+
1. [Spec](https://www.w3.org/TR/wai-aria/roles)
7+
2. [AX_ARIA_03](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_03)
8+
9+
## Rule details
10+
11+
This rule takes no arguments.
12+
13+
### Succeed
14+
```jsx
15+
<!-- Good: the checkbox role requires the aria-checked state -->
16+
<span role="checkbox" aria-checked="false" aria-labelledby="foo" tabindex="0"></span>
17+
```
18+
19+
### Fail
20+
21+
```jsx
22+
<!-- Bad: the checkbox role requires the aria-checked state -->
23+
<span role="checkbox" aria-labelledby="foo" tabindex="0"></span>
24+
```

src/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ module.exports = {
1111
'label-uses-for': require('./rules/label-uses-for'),
1212
'no-hash-href': require('./rules/no-hash-href'),
1313
'valid-aria-role': require('./rules/valid-aria-role'),
14-
'valid-aria-proptypes': require('./rules/valid-aria-proptypes')
14+
'valid-aria-proptypes': require('./rules/valid-aria-proptypes'),
15+
'no-invalid-aria': require('./rules/no-invalid-aria'),
16+
'role-requires-aria': require('./rules/role-requires-aria'),
17+
'no-unsupported-elements-use-aria': require('./rules/no-unsupported-elements-use-aria')
1518
},
1619
configs: {
1720
recommended: {
@@ -30,7 +33,10 @@ module.exports = {
3033
"jsx-a11y/label-uses-for": 2,
3134
"jsx-a11y/no-hash-href": 2,
3235
"jsx-a11y/valid-aria-role": 2,
33-
"jsx-a11y/valid-aria-proptypes": 2
36+
"jsx-a11y/valid-aria-proptypes": 2,
37+
"jsx-a11y/no-invalid-aria": 2,
38+
"jsx-a11y/role-requires-aria": 2,
39+
"jsx-a11y/no-unsupported-elements-use-aria": 2
3440
}
3541
}
3642
}

src/rules/no-invalid-aria.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @fileoverview Enforce all aria-* properties are valid.
3+
* @author Ethan Cohen
4+
*/
5+
'use strict';
6+
7+
// ----------------------------------------------------------------------------
8+
// Rule Definition
9+
// ----------------------------------------------------------------------------
10+
11+
import ariaAttributes from '../util/attributes/ARIA';
12+
13+
const errorMessage = name => `${name}: This attribute is an invalid ARIA attribute.`;
14+
15+
module.exports = context => ({
16+
JSXAttribute: attribute => {
17+
const name = attribute.name.name;
18+
const normalizedName = name.toUpperCase();
19+
20+
// `aria` needs to be prefix of property.
21+
if (normalizedName.indexOf('ARIA-') !== 0) {
22+
return;
23+
}
24+
25+
const isValid = Object.keys(ariaAttributes).indexOf(normalizedName) > -1;
26+
27+
if (isValid === false) {
28+
context.report({
29+
node: attribute,
30+
message: errorMessage(name)
31+
});
32+
}
33+
}
34+
});
35+
36+
module.exports.schema = [
37+
{ type: 'object' }
38+
];
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/rules/role-requires-aria.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @fileoverview Enforce that elements with ARIA roles must have all required attributes for that role.
3+
* @author Ethan Cohen
4+
*/
5+
'use strict';
6+
7+
// ----------------------------------------------------------------------------
8+
// Rule Definition
9+
// ----------------------------------------------------------------------------
10+
11+
import validRoleTypes from '../util/attributes/role';
12+
import { getLiteralAttributeValue } from '../util/getAttributeValue';
13+
import hasAttribute from '../util/hasAttribute';
14+
15+
const errorMessage = (role, requiredProps) =>
16+
`Elements with the ARIA role "${role}" must have the following ` +
17+
`attributes defined: ${requiredProps.toString().toLowerCase()}`;
18+
19+
module.exports = context => ({
20+
JSXAttribute: attribute => {
21+
const normalizedName = attribute.name.name.toUpperCase();
22+
if (normalizedName !== 'ROLE') {
23+
return;
24+
}
25+
26+
const value = getLiteralAttributeValue(attribute);
27+
28+
// If value is undefined, then the role attribute will be dropped in the DOM.
29+
// If value is null, then getLiteralAttributeValue is telling us that the value isn't in the form of a literal.
30+
if (value === undefined || value === null) {
31+
return;
32+
}
33+
34+
const normalizedValues = `${value}`.toUpperCase().split(" ");
35+
const validRoles = normalizedValues.filter(value => Object.keys(validRoleTypes).indexOf(value) > -1);
36+
37+
validRoles.forEach(role => {
38+
const { requiredProps } = validRoleTypes[role];
39+
40+
if (requiredProps.length > 0) {
41+
const hasRequiredProps = requiredProps.every(prop => hasAttribute(attribute.parent.attributes, prop));
42+
43+
if (hasRequiredProps === false) {
44+
context.report({
45+
node: attribute,
46+
message: errorMessage(role.toLowerCase(), requiredProps)
47+
});
48+
}
49+
}
50+
});
51+
52+
}
53+
});
54+
55+
module.exports.schema = [
56+
{ type: 'object' }
57+
];

src/rules/valid-aria-proptypes.js

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,27 @@
88
// Rule Definition
99
// ----------------------------------------------------------------------------
1010

11-
import ariaAttributes from '../util/ariaAttributes';
11+
import ariaAttributes from '../util/attributes/ARIA';
1212
import { getLiteralAttributeValue } from '../util/getAttributeValue';
1313

14-
const errorMessage = (name, type) => `${name} must be of type ${type}.`;
14+
const errorMessage = (name, type, permittedValues) => {
15+
switch (type) {
16+
case 'tristate':
17+
return `The value for ${name} must be a boolean or the string "mixed".`;
18+
case 'token':
19+
return `The value for ${name} must be a single token from the following: ${permittedValues}.`;
20+
case 'tokenlist':
21+
return `The value for ${name} must be a list of one or more tokens from the following: ${permittedValues}.`;
22+
case 'boolean':
23+
case 'string':
24+
case 'integer':
25+
case 'number':
26+
default:
27+
return `The value for ${name} must be a ${type}.`;
28+
}
29+
};
1530

16-
const validityCheck = (value, expectedType, tokens) => {
31+
const validityCheck = (value, expectedType, permittedValues) => {
1732
switch (expectedType) {
1833
case 'boolean':
1934
return typeof value === 'boolean';
@@ -26,9 +41,9 @@ const validityCheck = (value, expectedType, tokens) => {
2641
// Booleans resolve to 0/1 values so hard check that it's not first.
2742
return typeof value !== 'boolean' && isNaN(Number(value)) === false;
2843
case 'token':
29-
return typeof value === 'string' && tokens.some(token => value.toLowerCase() == token);
44+
return typeof value === 'string' && permittedValues.indexOf(value.toLowerCase()) > -1;
3045
case 'tokenlist':
31-
return typeof value === 'string' && value.split(' ').every(token => tokens.indexOf(token.toLowerCase()) > -1);
46+
return typeof value === 'string' && value.split(' ').every(token => permittedValues.indexOf(token.toLowerCase()) > -1);
3247
default:
3348
return false;
3449
}
@@ -39,8 +54,8 @@ module.exports = context => ({
3954
const name = attribute.name.name;
4055
const normalizedName = name.toUpperCase();
4156

42-
// Not an aria-* state or property.
43-
if (normalizedName.indexOf('ARIA-') === -1) {
57+
// Not a valid aria-* state or property.
58+
if (normalizedName.indexOf('ARIA-') !== 0 || ariaAttributes[normalizedName] === undefined) {
4459
return;
4560
}
4661

@@ -53,19 +68,19 @@ module.exports = context => ({
5368

5469
// These are the attributes of the property/state to check against.
5570
const attributes = ariaAttributes[normalizedName];
56-
const permittedType = attributes.value;
71+
const permittedType = attributes.type;
5772
const allowUndefined = attributes.allowUndefined || false;
58-
const tokens = attributes.tokens || [];
73+
const permittedValues = attributes.values || [];
5974

60-
const isValid = validityCheck(value, permittedType, tokens) || (allowUndefined && value === undefined);
75+
const isValid = validityCheck(value, permittedType, permittedValues) || (allowUndefined && value === undefined);
6176

6277
if (isValid) {
6378
return;
6479
}
6580

6681
context.report({
6782
node: attribute,
68-
message: errorMessage(name, permittedType)
83+
message: errorMessage(name, permittedType, permittedValues)
6984
});
7085
}
7186
});

0 commit comments

Comments
 (0)