Skip to content

Commit 4d6432d

Browse files
authored
Add few new rules (#60)
* [new] - Implement heading-has-content rule * [fix] - Handle undefined case in heading-has-content * [new] - Implement lang rule Enforces valid language & country code for `lang` attribute on `<html>` element * [new] - Implement html-has-lang rule * [fix] - Name fix on html-has-lang test * [new] - Implement scope rule Enforce `scope` prop is only used on `<th>` elements. * [new] - Implement no-marquee rule. * [docs] - Add new rules to README * [fix] - Don't test custom components in rule `scope` * [fix] - Only test for low-level html DOM element in html-has-lang and lang rules.
1 parent 5e04ad9 commit 4d6432d

18 files changed

+1026
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,22 @@ Then configure the rules you want to use under the rules section.
7474
- [aria-proptypes](docs/rules/aria-proptypes.md): Enforce ARIA state and property values are valid.
7575
- [aria-role](docs/rules/aria-role.md): Enforce that elements with ARIA roles must use a valid, non-abstract ARIA role.
7676
- [aria-unsupported-elements](docs/rules/aria-unsupported-elements.md): Enforce that elements that do not support ARIA roles, states, and properties do not have those attributes.
77+
- [heading-has-content](docs/rules/heading-has-content.md): Enforce heading (`h1`, `h2`, etc) elements contain accessible content.
7778
- [href-no-hash](docs/rules/href-no-hash.md): Enforce an anchor element's `href` prop value is not just `#`.
79+
- [html-has-lang](docs/rules/html-has-lang.md): Enforce `<html>` element has `lang` prop.
7880
- [img-has-alt](docs/rules/img-has-alt.md): Enforce that `<img>` JSX elements use the `alt` prop.
7981
- [img-redundant-alt](docs/rules/img-redundant-alt.md): Enforce `<img>` alt prop does not contain the word "image", "picture", or "photo".
8082
- [label-has-for](docs/rules/label-has-for.md): Enforce that `<label>` elements have the `htmlFor` prop.
83+
- [lang](docs/rules/lang.md): Enforce lang attribute has a valid value.
8184
- [mouse-events-have-key-events](docs/rules/mouse-events-have-key-events.md): Enforce that `onMouseOver`/`onMouseOut` are accompanied by `onFocus`/`onBlur` for keyboard-only users.
8285
- [no-access-key](docs/rules/no-access-key.md): Enforce that the `accessKey` prop is not used on any element to avoid complications with keyboard commands used by a screenreader.
86+
- [no-marquee](docs/rules/no-marquee.md): Enforce `<marquee>` elements are not used.
8387
- [no-onchange](docs/rules/no-onchange.md): Enforce that `onBlur` is used instead of `onChange`.
8488
- [onclick-has-focus](docs/rules/onclick-has-focus.md): Enforce that elements with `onClick` handlers must be focusable.
8589
- [onclick-has-role](docs/rules/onclick-has-role.md): Enforce that non-interactive, visible elements (such as `<div>`) that have click handlers use the role attribute.
8690
- [role-has-required-aria-props](docs/rules/role-has-required-aria-props.md): Enforce that elements with ARIA roles must have all required attributes for that role.
8791
- [role-supports-aria-props](docs/rules/role-supports-aria-props.md): Enforce that elements with explicit or implicit roles defined contain only `aria-*` properties supported by that `role`.
92+
- [scope](docs/rules/scope.md): Enforce `scope` prop is only used on `<th>` elements.
8893
- [tabindex-no-positive](docs/rules/tabindex-no-positive.md): Enforce `tabIndex` value is not greater than zero.
8994

9095
## License

docs/rules/heading-has-content.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# heading-has-content
2+
3+
Enforce that heading elements (`h1`, `h2`, etc.) have content and that the content is accessible to screen readers. Accessible means that it is not hidden using the `aria-hidden` prop. Refer to the references to learn about why this is important.
4+
5+
#### References
6+
1. [Deque University](https://dequeuniversity.com/rules/axe/1.1/empty-heading)
7+
8+
## Rule details
9+
10+
This rule takes one optional argument of type string or array of strings. These strings determine which JSX elements should be checked including `h1`, `h2`, `h3`, `h4`, `h5`, and `h6` by default. This is a good use case when you have a wrapper component that simply renders a heading element (like in React):
11+
12+
```js
13+
// Header.js
14+
const Header = props => {
15+
return (
16+
<h1 {...props}>{ props.children }</h1>
17+
);
18+
}
19+
20+
...
21+
22+
// CreateAccount.js (for example)
23+
...
24+
return (
25+
<Header>Create Account</Header>
26+
);
27+
```
28+
29+
To tell this plugin to also check your `Header` element, specify this in your `.eslintrc` file:
30+
31+
```json
32+
{
33+
"rules": {
34+
"jsx-a11y/heading-has-content": [ 2, "Header" ], // OR
35+
"jsx-a11y/heading-has-content": [ 2, [ "HeaderOne", "HeaderTwo" ] ]
36+
}
37+
}
38+
```
39+
40+
41+
#### Bad
42+
```jsx
43+
function Foo(props) {
44+
return <label {...props} />
45+
}
46+
```
47+
48+
### Succeed
49+
```jsx
50+
<h1>Heading Content!</h1>
51+
<h1><TextWrapper /><h1>
52+
```
53+
54+
### Fail
55+
```jsx
56+
<h1 />
57+
<h1><TextWrapper aria-hidden />
58+
```

docs/rules/html-has-lang.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# html-has-lang
2+
3+
<html> elements must have the lang prop.
4+
5+
#### References
6+
1. [Deque University](https://dequeuniversity.com/rules/axe/1.1/html-lang)
7+
8+
## Rule details
9+
10+
This rule takes no arguments.
11+
12+
### Succeed
13+
```jsx
14+
<html lang="en">
15+
<html lang="en-US">
16+
<html lang={language}>
17+
```
18+
19+
### Fail
20+
21+
```jsx
22+
<html>
23+
```

docs/rules/lang.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# lang
2+
3+
The `lang` prop on the `<html>` element must have a valid value based on ISO country and language codes.
4+
5+
#### References
6+
1. [Deque University](https://dequeuniversity.com/rules/axe/1.1/valid-lang)
7+
2. [ISO Language Codes](http://www.w3schools.com/tags/ref_language_codes.asp)
8+
3. [ISO Country Codes](http://www.w3schools.com/tags/ref_country_codes.asp)
9+
10+
## Rule details
11+
12+
This rule takes no arguments.
13+
14+
### Succeed
15+
```jsx
16+
<html lang="en">
17+
<html lang="en-US">
18+
```
19+
20+
### Fail
21+
22+
```jsx
23+
<html>
24+
<html lang="foo">
25+
```

docs/rules/no-marquee.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# no-marquee
2+
3+
Enforces that no `<marquee>` elements are used.
4+
5+
#### References
6+
1. [Deque University](https://dequeuniversity.com/rules/axe/1.1/marquee)
7+
8+
## Rule details
9+
10+
This rule takes no arguments. You may have custom JSX components named `Marquee`, as this rule only checks for the DOM level `marquee`.
11+
12+
### Succeed
13+
```jsx
14+
<div />
15+
<Marquee />
16+
```
17+
18+
### Fail
19+
```jsx
20+
<marquee />
21+
```

docs/rules/scope.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# scope
2+
3+
The `scope` scope should be used only on `<th>` elements.
4+
5+
#### References
6+
1. [Deque University](https://dequeuniversity.com/rules/axe/1.1/scope)
7+
8+
## Rule details
9+
10+
This rule takes no arguments.
11+
12+
### Succeed
13+
```jsx
14+
<th scope="col" />
15+
<th scope={scope} />
16+
```
17+
18+
### Fail
19+
20+
```jsx
21+
<div scope />
22+
```

src/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,22 @@ module.exports = {
66
'aria-proptypes': require('./rules/aria-proptypes'),
77
'aria-role': require('./rules/aria-role'),
88
'aria-unsupported-elements': require('./rules/aria-unsupported-elements'),
9+
'heading-has-content': require('./rules/heading-has-content'),
910
'href-no-hash': require('./rules/href-no-hash'),
11+
'html-has-lang': require('./rules/html-has-lang'),
1012
'img-has-alt': require('./rules/img-has-alt'),
1113
'img-redundant-alt': require('./rules/img-redundant-alt'),
1214
'label-has-for': require('./rules/label-has-for'),
15+
lang: require('./rules/lang'),
1316
'mouse-events-have-key-events': require('./rules/mouse-events-have-key-events'),
1417
'no-access-key': require('./rules/no-access-key'),
18+
'no-marquee': require('./rules/no-marquee'),
1519
'no-onchange': require('./rules/no-onchange'),
1620
'onclick-has-focus': require('./rules/onclick-has-focus'),
1721
'onclick-has-role': require('./rules/onclick-has-role'),
1822
'role-has-required-aria-props': require('./rules/role-has-required-aria-props'),
1923
'role-supports-aria-props': require('./rules/role-supports-aria-props'),
24+
scope: require('./rules/scope'),
2025
'tabindex-no-positive': require('./rules/tabindex-no-positive'),
2126
},
2227
configs: {
@@ -31,17 +36,21 @@ module.exports = {
3136
'jsx-a11y/aria-proptypes': 2,
3237
'jsx-a11y/aria-role': 2,
3338
'jsx-a11y/aria-unsupported-elements': 2,
39+
'jsx-a11y/heading-has-content': 2,
3440
'jsx-a11y/href-no-hash': 2,
41+
'jsx-a11y/html-has-lang': 2,
3542
'jsx-a11y/img-has-alt': 2,
3643
'jsx-a11y/img-redundant-alt': 2,
3744
'jsx-a11y/label-has-for': 2,
3845
'jsx-a11y/mouse-events-have-key-events': 2,
3946
'jsx-a11y/no-access-key': 2,
47+
'jsx-a11y/no-marquee': 2,
4048
'jsx-a11y/no-onchange': 2,
4149
'jsx-a11y/onclick-has-focus': 2,
4250
'jsx-a11y/onclick-has-role': 2,
4351
'jsx-a11y/role-has-required-aria-props': 2,
4452
'jsx-a11y/role-supports-aria-props': 2,
53+
'jsx-a11y/scope': 2,
4554
'jsx-a11y/tabindex-no-positive': 2,
4655
},
4756
},

src/rules/heading-has-content.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @fileoverview Enforce heading (h1, h2, etc) elements contain accessible content.
3+
* @author Ethan Cohen
4+
*/
5+
6+
// ----------------------------------------------------------------------------
7+
// Rule Definition
8+
// ----------------------------------------------------------------------------
9+
10+
import { elementType } from 'jsx-ast-utils';
11+
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
12+
13+
const errorMessage =
14+
'Headings must have content and the content must be accessible by a screen reader.';
15+
16+
const headings = [
17+
'h1',
18+
'h2',
19+
'h3',
20+
'h4',
21+
'h5',
22+
'h6',
23+
];
24+
25+
module.exports = context => ({
26+
JSXOpeningElement: node => {
27+
const typeCheck = headings.concat(context.options[0]);
28+
const nodeType = elementType(node);
29+
30+
// Only check 'h*' elements and custom types.
31+
if (typeCheck.indexOf(nodeType) === -1) {
32+
return;
33+
}
34+
35+
const isAccessible = node.parent.children.some(child => {
36+
switch (child.type) {
37+
case 'Literal':
38+
return Boolean(child.value);
39+
case 'JSXElement':
40+
return !isHiddenFromScreenReader(
41+
elementType(child.openingElement),
42+
child.openingElement.attributes
43+
);
44+
case 'JSXExpressionContainer':
45+
return child.expression.type === 'Identifier' && child.expression.name !== 'undefined';
46+
default:
47+
return false;
48+
}
49+
});
50+
51+
if (isAccessible) {
52+
return;
53+
}
54+
55+
context.report({
56+
node,
57+
message: errorMessage,
58+
});
59+
},
60+
});
61+
62+
module.exports.schema = [
63+
{
64+
oneOf: [
65+
{ type: 'string' },
66+
{
67+
type: 'array',
68+
items: {
69+
type: 'string',
70+
},
71+
minItems: 1,
72+
uniqueItems: true,
73+
},
74+
],
75+
},
76+
];

src/rules/html-has-lang.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* @fileoverview Enforce html element has lang prop.
3+
* @author Ethan Cohen
4+
*/
5+
6+
// ----------------------------------------------------------------------------
7+
// Rule Definition
8+
// ----------------------------------------------------------------------------
9+
10+
import { elementType, getProp, getPropValue } from 'jsx-ast-utils';
11+
12+
const errorMessage = '<html> elements must have the lang prop.';
13+
14+
module.exports = context => ({
15+
JSXOpeningElement: node => {
16+
const type = elementType(node);
17+
18+
if (type && type !== 'html') {
19+
return;
20+
}
21+
22+
const lang = getPropValue(getProp(node.attributes, 'lang'));
23+
24+
if (lang) {
25+
return;
26+
}
27+
28+
context.report({
29+
node,
30+
message: errorMessage,
31+
});
32+
},
33+
});
34+
35+
module.exports.schema = [
36+
{ type: 'object' },
37+
];

src/rules/lang.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* @fileoverview Enforce lang attribute has a valid value.
3+
* @author Ethan Cohen
4+
*/
5+
6+
// ----------------------------------------------------------------------------
7+
// Rule Definition
8+
// ----------------------------------------------------------------------------
9+
10+
import { propName, elementType, getLiteralPropValue } from 'jsx-ast-utils';
11+
import ISO_CODES from '../util/attributes/ISO';
12+
13+
const errorMessage =
14+
'lang attribute must have a valid value.';
15+
16+
module.exports = context => ({
17+
JSXAttribute: node => {
18+
const name = propName(node);
19+
if (name && name.toUpperCase() !== 'LANG') {
20+
return;
21+
}
22+
23+
const { parent } = node;
24+
const type = elementType(parent);
25+
if (type && type !== 'html') {
26+
return;
27+
}
28+
29+
const value = getLiteralPropValue(node);
30+
31+
// Don't check identifiers
32+
if (value === null) {
33+
return;
34+
} else if (value === undefined) {
35+
context.report({
36+
node,
37+
message: errorMessage,
38+
});
39+
40+
return;
41+
}
42+
43+
const hyphen = value.indexOf('-');
44+
const lang = hyphen > -1 ? value.substring(0, hyphen) : value;
45+
const country = hyphen > -1 ? value.substring(3) : undefined;
46+
47+
if (ISO_CODES.languages.indexOf(lang) > -1
48+
&& (country === undefined || ISO_CODES.countries.indexOf(country) > -1)) {
49+
return;
50+
}
51+
52+
context.report({
53+
node,
54+
message: errorMessage,
55+
});
56+
},
57+
});
58+
59+
module.exports.schema = [
60+
{ type: 'object' },
61+
];

0 commit comments

Comments
 (0)