Skip to content

Commit de76184

Browse files
authored
Merge pull request #624 from WilcoFiers/autocomplete-rule
Add new autocomplete-valid rule using axe-core
2 parents 82f598e + 3aea217 commit de76184

File tree

7 files changed

+174
-0
lines changed

7 files changed

+174
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Add `plugin:jsx-a11y/recommended` or `plugin:jsx-a11y/strict` in `extends`:
103103
- [aria-proptypes](docs/rules/aria-proptypes.md): Enforce ARIA state and property values are valid.
104104
- [aria-role](docs/rules/aria-role.md): Enforce that elements with ARIA roles must use a valid, non-abstract ARIA role.
105105
- [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.
106+
- [autocomplete-valid](docs/rules/autocomplete-valid.md): Enforce that autocomplete attributes are used correctly.
106107
- [click-events-have-key-events](docs/rules/click-events-have-key-events.md): Enforce a clickable non-interactive element has at least one keyboard event listener.
107108
- [heading-has-content](docs/rules/heading-has-content.md): Enforce heading (`h1`, `h2`, etc) elements contain accessible content.
108109
- [html-has-lang](docs/rules/html-has-lang.md): Enforce `<html>` element has `lang` prop.
@@ -141,6 +142,7 @@ Rule | Recommended | Strict
141142
[aria-proptypes](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-proptypes.md) | error | error
142143
[aria-role](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-role.md) | error | error
143144
[aria-unsupported-elements](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-unsupported-elements.md) | error | error
145+
[autocomplete-valid](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/autocomplete-valid.md) | error | error
144146
[click-events-have-key-events](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/click-events-have-key-events.md) | error | error
145147
[heading-has-content](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/heading-has-content.md) | error | error
146148
[html-has-lang](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/html-has-lang.md) | error | error

__tests__/__util__/axeMapping.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* eslint-disable import/prefer-default-export, no-underscore-dangle */
2+
import * as axe from 'axe-core';
3+
4+
export function axeFailMessage(checkId, data) {
5+
return axe._audit.data.checks[checkId].messages.fail(data);
6+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/* eslint-env jest */
2+
/**
3+
* @fileoverview Ensure autocomplete attribute is correct.
4+
* @author Wilco Fiers
5+
*/
6+
7+
// -----------------------------------------------------------------------------
8+
// Requirements
9+
// -----------------------------------------------------------------------------
10+
11+
import { RuleTester } from 'eslint';
12+
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
13+
import { axeFailMessage } from '../../__util__/axeMapping';
14+
import rule from '../../../src/rules/autocomplete-valid';
15+
16+
// -----------------------------------------------------------------------------
17+
// Tests
18+
// -----------------------------------------------------------------------------
19+
20+
const ruleTester = new RuleTester();
21+
22+
const invalidAutocomplete = [{
23+
message: axeFailMessage('autocomplete-valid'),
24+
type: 'JSXOpeningElement',
25+
}];
26+
27+
const inappropriateAutocomplete = [{
28+
message: axeFailMessage('autocomplete-appropriate'),
29+
type: 'JSXOpeningElement',
30+
}];
31+
32+
ruleTester.run('autocomplete-valid', rule, {
33+
valid: [
34+
// INAPPLICABLE
35+
{ code: '<input type="text" />;' },
36+
// // PASSED AUTOCOMPLETE
37+
{ code: '<input type="text" autocomplete="name" />;' },
38+
{ code: '<input type="text" autocomplete="" />;' },
39+
{ code: '<input type="text" autocomplete="off" />;' },
40+
{ code: '<input type="text" autocomplete="on" />;' },
41+
{ code: '<input type="text" autocomplete="billing family-name" />;' },
42+
{ code: '<input type="text" autocomplete="section-blue shipping street-address" />;' },
43+
{ code: '<input type="text" autocomplete="section-somewhere shipping work email" />;' },
44+
{ code: '<input type="text" autocomplete />;' },
45+
{ code: '<input type="text" autocomplete={autocompl} />;' },
46+
{ code: '<input type="text" autocomplete={autocompl || "name"} />;' },
47+
{ code: '<input type="text" autocomplete={autocompl || "foo"} />;' },
48+
{ code: '<Foo autocomplete="bar"></Foo>;' },
49+
].map(parserOptionsMapper),
50+
invalid: [
51+
// FAILED "autocomplete-valid"
52+
{ code: '<input type="text" autocomplete="foo" />;', errors: invalidAutocomplete },
53+
{ code: '<input type="text" autocomplete="name invalid" />;', errors: invalidAutocomplete },
54+
{ code: '<input type="text" autocomplete="invalid name" />;', errors: invalidAutocomplete },
55+
{ code: '<input type="text" autocomplete="home url" />;', errors: invalidAutocomplete },
56+
{ code: '<Bar autocomplete="baz"></Bar>;', errors: invalidAutocomplete, options: [{ inputComponents: ['Bar'] }] },
57+
58+
// FAILED "autocomplete-appropriate"
59+
{ code: '<input type="date" autocomplete="email" />;', errors: inappropriateAutocomplete },
60+
{ code: '<input type="number" autocomplete="url" />;', errors: inappropriateAutocomplete },
61+
{ code: '<input type="month" autocomplete="tel" />;', errors: inappropriateAutocomplete },
62+
{ code: '<Foo type="month" autocomplete="tel"></Foo>;', errors: inappropriateAutocomplete, options: [{ inputComponents: ['Foo'] }] },
63+
].map(parserOptionsMapper),
64+
});

docs/rules/autocomplete-valid.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# autocomplete-valid
2+
3+
Ensure the autocomplete attribute is correct and suitable for the form field it is used with.
4+
5+
#### References
6+
1. [axe-core, autocomplete-valid](https://dequeuniversity.com/rules/axe/3.2/autocomplete-valid)
7+
2. [HTML 5.2, Autocomplete requirements](https://www.w3.org/TR/html52/sec-forms.html#autofilling-form-controls-the-autocomplete-attribute)
8+
9+
## Rule details
10+
11+
This rule takes one optional object argument of type object:
12+
13+
```
14+
{
15+
"rules": {
16+
"jsx-a11y/autocomplete-valid": [ 2, {
17+
"inputComponents": ["Input", "FormField"]
18+
}],
19+
}
20+
}
21+
```
22+
23+
### Succeed
24+
```jsx
25+
<!-- Good: the autocomplete attribute is used according to the HTML specification -->
26+
<input type="text" autocomplete="name" />
27+
28+
<!-- Good: MyInput is not listed in inputComponents -->
29+
<MyInput autocomplete="incorrect" />
30+
```
31+
32+
### Fail
33+
```jsx
34+
<!-- Bad: the autocomplete attribute has an invalid value -->
35+
<input type="text" autocomplete="incorrect" />
36+
37+
<!-- Bad: the autocomplete attribute is on an inappropriate input element -->
38+
<input type="email" autocomplete="url" />
39+
40+
<!-- Bad: MyInput is listed in inputComponents -->
41+
<MyInput autocomplete="incorrect" />
42+
```

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"aria-query": "^3.0.0",
6363
"array-includes": "^3.0.3",
6464
"ast-types-flow": "^0.0.7",
65+
"axe-core": "^3.4.0",
6566
"axobject-query": "^2.0.2",
6667
"damerau-levenshtein": "^1.0.4",
6768
"emoji-regex": "^7.0.2",

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ module.exports = {
5858
'jsx-a11y/aria-proptypes': 'error',
5959
'jsx-a11y/aria-role': 'error',
6060
'jsx-a11y/aria-unsupported-elements': 'error',
61+
'jsx-a11y/autocomplete-valid': 'error',
6162
'jsx-a11y/click-events-have-key-events': 'error',
6263
'jsx-a11y/control-has-associated-label': ['off',
6364
{
@@ -214,6 +215,7 @@ module.exports = {
214215
'jsx-a11y/aria-proptypes': 'error',
215216
'jsx-a11y/aria-role': 'error',
216217
'jsx-a11y/aria-unsupported-elements': 'error',
218+
'jsx-a11y/autocomplete-valid': 'error',
217219
'jsx-a11y/click-events-have-key-events': 'error',
218220
'jsx-a11y/control-has-associated-label': ['off', {
219221
ignoreElements: [

src/rules/autocomplete-valid.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @fileoverview Ensure autocomplete attribute is correct.
3+
* @author Wilco Fiers
4+
*/
5+
6+
// ----------------------------------------------------------------------------
7+
// Rule Definition
8+
// ----------------------------------------------------------------------------
9+
import { runVirtualRule } from 'axe-core';
10+
import { elementType, getLiteralPropValue, getProp } from 'jsx-ast-utils';
11+
import { generateObjSchema, arraySchema } from '../util/schemas';
12+
13+
const schema = generateObjSchema({
14+
inputComponents: arraySchema,
15+
});
16+
17+
module.exports = {
18+
meta: {
19+
docs: {
20+
url: 'https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules/autocomplete-valid.md',
21+
},
22+
schema: [schema],
23+
},
24+
25+
create: context => ({
26+
JSXOpeningElement: (node) => {
27+
const options = context.options[0] || {};
28+
const { inputComponents = [] } = options;
29+
const inputTypes = ['input', ...inputComponents];
30+
31+
const elType = elementType(node);
32+
const autocomplete = getLiteralPropValue(getProp(node.attributes, 'autocomplete'));
33+
34+
if (typeof autocomplete !== 'string' || !inputTypes.includes(elType)) {
35+
return;
36+
}
37+
38+
const { violations } = runVirtualRule('autocomplete-valid', {
39+
nodeName: 'input',
40+
attributes: {
41+
autocomplete,
42+
// Which autocomplete is valid depends on the input type
43+
type: getLiteralPropValue(getProp(node.attributes, 'type')),
44+
},
45+
});
46+
47+
if (violations.length === 0) {
48+
return;
49+
}
50+
// Since we only test one rule, with one node, return the message from first (and only) instance of each
51+
context.report({
52+
node,
53+
message: violations[0].nodes[0].all[0].message,
54+
});
55+
},
56+
}),
57+
};

0 commit comments

Comments
 (0)