Skip to content

Commit 4758cbf

Browse files
authored
Merge branch 'master' into n-chardon-fix-issue510
2 parents a0cc110 + a2f2c54 commit 4758cbf

File tree

63 files changed

+397
-163
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+397
-163
lines changed

README.md

+7-5
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
@@ -169,7 +171,7 @@ Rule | Recommended | Strict
169171
The following rules have extra options when in *recommended* mode:
170172

171173
#### no-interactive-element-to-noninteractive-role
172-
```
174+
```js
173175
'jsx-a11y/no-interactive-element-to-noninteractive-role': [
174176
'error',
175177
{
@@ -179,7 +181,7 @@ The following rules have extra options when in *recommended* mode:
179181
```
180182

181183
#### no-noninteractive-element-interactions
182-
```
184+
```js
183185
'jsx-a11y/no-noninteractive-element-interactions': [
184186
'error',
185187
{
@@ -196,7 +198,7 @@ The following rules have extra options when in *recommended* mode:
196198
```
197199

198200
#### no-noninteractive-element-to-interactive-role
199-
```
201+
```js
200202
'jsx-a11y/no-noninteractive-element-to-interactive-role': [
201203
'error',
202204
{
@@ -226,7 +228,7 @@ The following rules have extra options when in *recommended* mode:
226228
```
227229

228230
#### no-noninteractive-tabindex
229-
```
231+
```js
230232
'jsx-a11y/no-noninteractive-tabindex': [
231233
'error',
232234
{
@@ -237,7 +239,7 @@ The following rules have extra options when in *recommended* mode:
237239
```
238240

239241
#### no-static-element-interactions
240-
```
242+
```js
241243
'jsx-a11y/no-noninteractive-element-interactions': [
242244
'error',
243245
{

__mocks__/genInteractives.js

+11-11
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,9 @@ Object.keys(interactiveElementsMap)
119119
.concat(Object.keys(nonInteractiveElementsMap))
120120
.forEach((name: string) => delete indeterminantInteractiveElementsMap[name]);
121121

122-
const abstractRoles = roleNames.filter(role => roles.get(role).abstract);
122+
const abstractRoles = roleNames.filter((role) => roles.get(role).abstract);
123123

124-
const nonAbstractRoles = roleNames.filter(role => !roles.get(role).abstract);
124+
const nonAbstractRoles = roleNames.filter((role) => !roles.get(role).abstract);
125125

126126
const interactiveRoles = []
127127
.concat(
@@ -130,21 +130,21 @@ const interactiveRoles = []
130130
// aria-activedescendant, thus in practice we treat it as a widget.
131131
'toolbar',
132132
)
133-
.filter(role => !roles.get(role).abstract)
134-
.filter(role => roles.get(role).superClass.some(klasses => includes(klasses, 'widget')));
133+
.filter((role) => !roles.get(role).abstract)
134+
.filter((role) => roles.get(role).superClass.some((klasses) => includes(klasses, 'widget')));
135135

136136
const nonInteractiveRoles = roleNames
137-
.filter(role => !roles.get(role).abstract)
138-
.filter(role => !roles.get(role).superClass.some(klasses => includes(klasses, 'widget')))
137+
.filter((role) => !roles.get(role).abstract)
138+
.filter((role) => !roles.get(role).superClass.some((klasses) => includes(klasses, 'widget')))
139139
// 'toolbar' does not descend from widget, but it does support
140140
// aria-activedescendant, thus in practice we treat it as a widget.
141-
.filter(role => !includes(['toolbar'], role));
141+
.filter((role) => !includes(['toolbar'], role));
142142

143143
export function genElementSymbol(openingElement: Object) {
144144
return (
145145
openingElement.name.name + (openingElement.attributes.length > 0
146146
? `${openingElement.attributes
147-
.map(attr => `[${attr.name.name}="${attr.value.value}"]`)
147+
.map((attr) => `[${attr.name.name}="${attr.value.value}"]`)
148148
.join('')}`
149149
: ''
150150
)
@@ -187,15 +187,15 @@ export function genNonInteractiveRoleElements() {
187187
...nonInteractiveRoles,
188188
'article button',
189189
'fakerole article button',
190-
].map(value => JSXElementMock('div', [JSXAttributeMock('role', value)]));
190+
].map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
191191
}
192192

193193
export function genAbstractRoleElements() {
194-
return abstractRoles.map(value => JSXElementMock('div', [JSXAttributeMock('role', value)]));
194+
return abstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
195195
}
196196

197197
export function genNonAbstractRoleElements() {
198-
return nonAbstractRoles.map(value => JSXElementMock('div', [JSXAttributeMock('role', value)]));
198+
return nonAbstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
199199
}
200200

201201
export function genIndeterminantInteractiveElements(): Array<TJSXElementMock> {

__tests__/__util__/axeMapping.js

+6
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+
}

__tests__/index-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import path from 'path';
77
import plugin from '../src';
88

99
const rules = fs.readdirSync(path.resolve(__dirname, '../src/rules/'))
10-
.map(f => path.basename(f, '.js'));
10+
.map((f) => path.basename(f, '.js'));
1111

1212
describe('all rule files should be exported by the plugin', () => {
1313
rules.forEach((ruleName) => {

__tests__/src/rules/alt-text-test.js

+23-2
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,20 @@ import rule from '../../../src/rules/alt-text';
1818

1919
const ruleTester = new RuleTester();
2020

21-
const missingPropError = type => ({
21+
const missingPropError = (type) => ({
2222
message: `${type} elements must have an alt prop, either with meaningful text, or an empty string for decorative images.`,
2323
type: 'JSXOpeningElement',
2424
});
2525

26-
const altValueError = type => ({
26+
const altValueError = (type) => ({
2727
message: `Invalid alt value for ${type}. \
2828
Use alt="" for presentational images.`,
2929
type: 'JSXOpeningElement',
3030
});
3131

32+
const ariaLabelValueError = 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.';
33+
const ariaLabelledbyValueError = 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.';
34+
3235
const preferAltError = () => ({
3336
message: 'Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.',
3437
type: 'JSXOpeningElement',
@@ -83,6 +86,8 @@ ruleTester.run('alt-text', rule, {
8386
{ code: '<img alt={error ? "not working": "working"} />' },
8487
{ code: '<img alt={undefined ? "working": "not working"} />' },
8588
{ code: '<img alt={plugin.name + " Logo"} />' },
89+
{ code: '<img aria-label="foo" />' },
90+
{ code: '<img aria-labelledby="id1" />' },
8691

8792
// DEFAULT <object> TESTS
8893
{ code: '<object aria-label="foo" />' },
@@ -168,25 +173,41 @@ ruleTester.run('alt-text', rule, {
168173
{ code: '<img alt role="presentation" />;', errors: [altValueError('img')] },
169174
{ code: '<img role="presentation" />;', errors: [preferAltError()] },
170175
{ code: '<img role="none" />;', errors: [preferAltError()] },
176+
{ code: '<img aria-label={undefined} />', errors: [ariaLabelValueError] },
177+
{ code: '<img aria-labelledby={undefined} />', errors: [ariaLabelledbyValueError] },
178+
{ code: '<img aria-label="" />', errors: [ariaLabelValueError] },
179+
{ code: '<img aria-labelledby="" />', errors: [ariaLabelledbyValueError] },
171180

172181
// DEFAULT ELEMENT 'object' TESTS
173182
{ code: '<object />', errors: [objectError] },
174183
{ code: '<object><div aria-hidden /></object>', errors: [objectError] },
175184
{ code: '<object title={undefined} />', errors: [objectError] },
185+
{ code: '<object aria-label="" />', errors: [objectError] },
186+
{ code: '<object aria-labelledby="" />', errors: [objectError] },
187+
{ code: '<object aria-label={undefined} />', errors: [objectError] },
188+
{ code: '<object aria-labelledby={undefined} />', errors: [objectError] },
176189

177190
// DEFAULT ELEMENT 'area' TESTS
178191
{ code: '<area />', errors: [areaError] },
179192
{ code: '<area alt />', errors: [areaError] },
180193
{ code: '<area alt={undefined} />', errors: [areaError] },
181194
{ code: '<area src="xyz" />', errors: [areaError] },
182195
{ code: '<area {...this.props} />', errors: [areaError] },
196+
{ code: '<area aria-label="" />', errors: [areaError] },
197+
{ code: '<area aria-label={undefined} />', errors: [areaError] },
198+
{ code: '<area aria-labelledby="" />', errors: [areaError] },
199+
{ code: '<area aria-labelledby={undefined} />', errors: [areaError] },
183200

184201
// DEFAULT ELEMENT 'input type="image"' TESTS
185202
{ code: '<input type="image" />', errors: [inputImageError] },
186203
{ code: '<input type="image" alt />', errors: [inputImageError] },
187204
{ code: '<input type="image" alt={undefined} />', errors: [inputImageError] },
188205
{ code: '<input type="image">Foo</input>', errors: [inputImageError] },
189206
{ code: '<input type="image" {...this.props} />', errors: [inputImageError] },
207+
{ code: '<input type="image" aria-label="" />', errors: [inputImageError] },
208+
{ code: '<input type="image" aria-label={undefined} />', errors: [inputImageError] },
209+
{ code: '<input type="image" aria-labelledby="" />', errors: [inputImageError] },
210+
{ code: '<input type="image" aria-labelledby={undefined} />', errors: [inputImageError] },
190211

191212
// CUSTOM ELEMENT TESTS FOR ARRAY OPTION TESTS
192213
{

__tests__/src/rules/aria-props-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const errorMessage = (name) => {
3939
};
4040

4141
// Create basic test cases using all valid role types.
42-
const basicValidityTests = ariaAttributes.map(prop => ({
42+
const basicValidityTests = ariaAttributes.map((prop) => ({
4343
code: `<div ${prop.toLowerCase()}="foobar" />`,
4444
}));
4545

__tests__/src/rules/aria-role-test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ const errorMessage = {
2626

2727
const roleKeys = [...roles.keys()];
2828

29-
const validRoles = roleKeys.filter(role => roles.get(role).abstract === false);
30-
const invalidRoles = roleKeys.filter(role => roles.get(role).abstract === true);
29+
const validRoles = roleKeys.filter((role) => roles.get(role).abstract === false);
30+
const invalidRoles = roleKeys.filter((role) => roles.get(role).abstract === true);
3131

32-
const createTests = roleNames => roleNames.map(role => ({
32+
const createTests = (roleNames) => roleNames.map((role) => ({
3333
code: `<div role="${role.toLowerCase()}" />`,
3434
}));
3535

3636
const validTests = createTests(validRoles);
3737
const invalidTests = createTests(invalidRoles).map((test) => {
38-
const invalidTest = Object.assign({}, test);
38+
const invalidTest = { ...test };
3939
invalidTest.errors = [errorMessage];
4040
return invalidTest;
4141
});

__tests__/src/rules/aria-unsupported-elements-test.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import rule from '../../../src/rules/aria-unsupported-elements';
2020

2121
const ruleTester = new RuleTester();
2222

23-
const errorMessage = invalidProp => ({
23+
const errorMessage = (invalidProp) => ({
2424
message: `This element does not support ARIA roles, states and properties. \
2525
Try removing the prop '${invalidProp}'.`,
2626
type: 'JSXOpeningElement',
@@ -51,15 +51,15 @@ const ariaValidityTests = domElements.map((element) => {
5151

5252
// Generate invalid test cases.
5353
const invalidRoleValidityTests = domElements
54-
.filter(element => Boolean(dom.get(element).reserved))
55-
.map(reservedElem => ({
54+
.filter((element) => Boolean(dom.get(element).reserved))
55+
.map((reservedElem) => ({
5656
code: `<${reservedElem} role {...props} />`,
5757
errors: [errorMessage('role')],
5858
}));
5959

6060
const invalidAriaValidityTests = domElements
61-
.filter(element => Boolean(dom.get(element).reserved))
62-
.map(reservedElem => ({
61+
.filter((element) => Boolean(dom.get(element).reserved))
62+
.map((reservedElem) => ({
6363
code: `<${reservedElem} aria-hidden aria-role="none" {...props} />`,
6464
errors: [errorMessage('aria-hidden')],
6565
}));
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+
});

0 commit comments

Comments
 (0)