Skip to content

Commit edd95d5

Browse files
committed
Implement valid-aria-jroptype rule.
Fixes #13 cc @lencioni Thinking we can do better with error messaging in the token/tokenlist case but will hold off for now.
1 parent 9ca8691 commit edd95d5

File tree

6 files changed

+435
-2
lines changed

6 files changed

+435
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Then configure the rules you want to use under the rules section.
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 #.
7777
- [valid-aria-role](docs/rules/valid-aria-role.md): Enforce that elements with ARIA roles must use a valid, non-abstract ARIA role.
78+
- [valid-aria-proptype](docs/rules/valid-aria-proptype.md): Enforce ARIA state and property values are valid.
7879

7980
## Contributing
8081
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.

docs/rules/valid-aria-proptype.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# valid-aria-proptype
2+
3+
ARIA state and property values must be valid.
4+
5+
#### References
6+
1. [Spec](https://www.w3.org/TR/wai-aria/states_and_properties)
7+
2. [AX_ARIA_04](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_04)
8+
9+
## Rule details
10+
11+
This rule takes no arguments.
12+
13+
### Succeed
14+
```jsx
15+
<!-- Good: the aria-hidden state is of type true/false -->
16+
<span aria-hidden="true">foo</span>
17+
```
18+
19+
### Fail
20+
```jsx
21+
<!-- Bad: the aria-hidden state is of type true/false -->
22+
<span aria-hidden="yes">foo</span>
23+
```
24+

src/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ module.exports = {
1010
'no-access-key': require('./rules/no-access-key'),
1111
'label-uses-for': require('./rules/label-uses-for'),
1212
'no-hash-href': require('./rules/no-hash-href'),
13-
'valid-aria-role': require('./rules/valid-aria-role')
13+
'valid-aria-role': require('./rules/valid-aria-role'),
14+
'valid-aria-proptypes': require('./rules/valid-aria-proptypes')
1415
},
1516
configs: {
1617
recommended: {
@@ -28,7 +29,8 @@ module.exports = {
2829
"jsx-a11y/no-access-key": 2,
2930
"jsx-a11y/label-uses-for": 2,
3031
"jsx-a11y/no-hash-href": 2,
31-
"jsx-a11y/valid-aria-role": 2
32+
"jsx-a11y/valid-aria-role": 2,
33+
"jsx-a11y/valid-aria-proptypes": 2
3234
}
3335
}
3436
}

src/rules/valid-aria-proptypes.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @fileoverview Enforce ARIA state and property values are valid.
3+
* @author Ethan Cohen
4+
*/
5+
'use strict';
6+
7+
// ----------------------------------------------------------------------------
8+
// Rule Definition
9+
// ----------------------------------------------------------------------------
10+
11+
import ariaAttributes from '../util/ariaAttributes';
12+
import { getLiteralAttributeValue } from '../util/getAttributeValue';
13+
14+
const errorMessage = (name, type) => `${name} must be of type ${type}.`;
15+
16+
const validityCheck = (value, expectedType, tokens) => {
17+
switch (expectedType) {
18+
case 'boolean':
19+
return typeof value === 'boolean';
20+
case 'string':
21+
return typeof value === 'string';
22+
case 'tristate':
23+
return typeof value === 'boolean' || value == 'mixed';
24+
case 'integer':
25+
case 'number':
26+
// Booleans resolve to 0/1 values so hard check that it's not first.
27+
return typeof value !== 'boolean' && isNaN(Number(value)) === false;
28+
case 'token':
29+
return typeof value === 'string' && tokens.some(token => value.toLowerCase() == token);
30+
case 'tokenlist':
31+
return typeof value === 'string' && value.split(' ').every(token => tokens.indexOf(token.toLowerCase()) > -1);
32+
default:
33+
return false;
34+
}
35+
};
36+
37+
module.exports = context => ({
38+
JSXAttribute: attribute => {
39+
const name = attribute.name.name;
40+
const normalizedName = name.toUpperCase();
41+
42+
// Not an aria-* state or property.
43+
if (normalizedName.indexOf('ARIA-') === -1) {
44+
return;
45+
}
46+
47+
const value = getLiteralAttributeValue(attribute);
48+
49+
// We only want to check literal prop values, so just pass if it's null.
50+
if (value === null) {
51+
return;
52+
}
53+
54+
// These are the attributes of the property/state to check against.
55+
const attributes = ariaAttributes[normalizedName];
56+
const permittedType = attributes.value;
57+
const allowUndefined = attributes.allowUndefined || false;
58+
const tokens = attributes.tokens || [];
59+
60+
const isValid = validityCheck(value, permittedType, tokens) || (allowUndefined && value === undefined);
61+
62+
if (isValid) {
63+
return;
64+
}
65+
66+
context.report({
67+
node: attribute,
68+
message: errorMessage(name, permittedType)
69+
});
70+
}
71+
});
72+
73+
module.exports.schema = [
74+
{ type: 'object' }
75+
];

src/util/ariaAttributes.json

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
{
2+
"ARIA-ACTIVEDESCENDANT": {
3+
"type": "property",
4+
"value": "string"
5+
},
6+
"ARIA-ATOMIC": {
7+
"type": "property",
8+
"value": "boolean"
9+
},
10+
"ARIA-AUTOCOMPLETE": {
11+
"type": "property",
12+
"value": "token",
13+
"tokens": [ "inline", "list", "both", "none" ]
14+
},
15+
"ARIA-BUSY": {
16+
"type": "state",
17+
"value": "boolean"
18+
},
19+
"ARIA-CHECKED": {
20+
"type": "state",
21+
"value": "tristate"
22+
},
23+
"ARIA-CONTROLS": {
24+
"type": "property",
25+
"value": "string"
26+
},
27+
"ARIA-DESCRIBEDBY": {
28+
"type": "property",
29+
"value": "string"
30+
},
31+
"ARIA-DISABLED": {
32+
"type": "state",
33+
"value": "boolean"
34+
},
35+
"ARIA-DROPEFFECT": {
36+
"type": "property",
37+
"value": "tokenlist",
38+
"tokens": [ "copy", "move", "link", "execute", "popup", "none" ]
39+
},
40+
"ARIA-EXPANDED": {
41+
"type": "state",
42+
"value": "boolean",
43+
"allowUndefined": true
44+
},
45+
"ARIA-FLOWTO": {
46+
"type": "property",
47+
"value": "string"
48+
},
49+
"ARIA-GRABBED": {
50+
"type": "state",
51+
"value": "boolean",
52+
"allowUndefined": true
53+
},
54+
"ARIA-HASPOPUP": {
55+
"type": "property",
56+
"value": "boolean"
57+
},
58+
"ARIA-HIDDEN": {
59+
"type": "state",
60+
"value": "boolean"
61+
},
62+
"ARIA-INVALID": {
63+
"type": "state",
64+
"value": "token",
65+
"tokens": [ "grammar", "false", "spelling", "true" ]
66+
},
67+
"ARIA-LABEL": {
68+
"type": "property",
69+
"value": "string"
70+
},
71+
"ARIA-LABELLEDBY": {
72+
"type": "property",
73+
"value": "string"
74+
},
75+
"ARIA-LEVEL": {
76+
"type": "property",
77+
"value": "integer"
78+
},
79+
"ARIA-LIVE": {
80+
"type": "property",
81+
"value": "token",
82+
"tokens": [ "off", "polite", "assertive" ]
83+
},
84+
"ARIA-MULTILINE": {
85+
"type": "property",
86+
"value": "boolean"
87+
},
88+
"ARIA-MULTISELECTABLE": {
89+
"type": "property",
90+
"value": "boolean"
91+
},
92+
"ARIA-ORIENTATION": {
93+
"type": "property",
94+
"value": "token",
95+
"tokens": [ "vertical", "horizontal" ]
96+
},
97+
"ARIA-OWNS": {
98+
"type": "property",
99+
"value": "string"
100+
},
101+
"ARIA-POSINSET": {
102+
"type": "property",
103+
"value": "integer"
104+
},
105+
"ARIA-PRESSED": {
106+
"type": "state",
107+
"value": "tristate"
108+
},
109+
"ARIA-READONLY": {
110+
"type": "property",
111+
"value": "boolean"
112+
},
113+
"ARIA-RELEVANT": {
114+
"type": "property",
115+
"value": "tokenlist",
116+
"tokens": [ "additions", "removals", "text", "all" ]
117+
},
118+
"ARIA-REQUIRED": {
119+
"type": "property",
120+
"value": "boolean"
121+
},
122+
"ARIA-SELECTED": {
123+
"type": "state",
124+
"value": "boolean",
125+
"allowUndefined": true
126+
},
127+
"ARIA-SETSIZE": {
128+
"type": "property",
129+
"value": "integer"
130+
},
131+
"ARIA-SORT": {
132+
"type": "property",
133+
"value": "token",
134+
"tokens": [ "ascending", "descending", "none", "other" ]
135+
},
136+
"ARIA-VALUEMAX": {
137+
"type": "property",
138+
"value": "number"
139+
},
140+
"ARIA-VALUEMIN": {
141+
"type": "property",
142+
"value": "number"
143+
},
144+
"ARIA-VALUENOW": {
145+
"type": "property",
146+
"value": "number"
147+
},
148+
"ARIA-VALUETEXT": {
149+
"type": "property",
150+
"value": "string"
151+
}
152+
}
153+

0 commit comments

Comments
 (0)