Skip to content

Commit

Permalink
Merge branch 'master' into n-chardon-fix-issue510
Browse files Browse the repository at this point in the history
  • Loading branch information
jessebeach committed Nov 28, 2019
2 parents a0cc110 + a2f2c54 commit 4758cbf
Show file tree
Hide file tree
Showing 63 changed files with 397 additions and 163 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Add `plugin:jsx-a11y/recommended` or `plugin:jsx-a11y/strict` in `extends`:
- [aria-proptypes](docs/rules/aria-proptypes.md): Enforce ARIA state and property values are valid.
- [aria-role](docs/rules/aria-role.md): Enforce that elements with ARIA roles must use a valid, non-abstract ARIA role.
- [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.
- [autocomplete-valid](docs/rules/autocomplete-valid.md): Enforce that autocomplete attributes are used correctly.
- [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.
- [heading-has-content](docs/rules/heading-has-content.md): Enforce heading (`h1`, `h2`, etc) elements contain accessible content.
- [html-has-lang](docs/rules/html-has-lang.md): Enforce `<html>` element has `lang` prop.
Expand Down Expand Up @@ -141,6 +142,7 @@ Rule | Recommended | Strict
[aria-proptypes](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-proptypes.md) | error | error
[aria-role](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-role.md) | error | error
[aria-unsupported-elements](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-unsupported-elements.md) | error | error
[autocomplete-valid](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/autocomplete-valid.md) | error | error
[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
[heading-has-content](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/heading-has-content.md) | error | error
[html-has-lang](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/html-has-lang.md) | error | error
Expand Down Expand Up @@ -169,7 +171,7 @@ Rule | Recommended | Strict
The following rules have extra options when in *recommended* mode:

#### no-interactive-element-to-noninteractive-role
```
```js
'jsx-a11y/no-interactive-element-to-noninteractive-role': [
'error',
{
Expand All @@ -179,7 +181,7 @@ The following rules have extra options when in *recommended* mode:
```

#### no-noninteractive-element-interactions
```
```js
'jsx-a11y/no-noninteractive-element-interactions': [
'error',
{
Expand All @@ -196,7 +198,7 @@ The following rules have extra options when in *recommended* mode:
```

#### no-noninteractive-element-to-interactive-role
```
```js
'jsx-a11y/no-noninteractive-element-to-interactive-role': [
'error',
{
Expand Down Expand Up @@ -226,7 +228,7 @@ The following rules have extra options when in *recommended* mode:
```

#### no-noninteractive-tabindex
```
```js
'jsx-a11y/no-noninteractive-tabindex': [
'error',
{
Expand All @@ -237,7 +239,7 @@ The following rules have extra options when in *recommended* mode:
```

#### no-static-element-interactions
```
```js
'jsx-a11y/no-noninteractive-element-interactions': [
'error',
{
Expand Down
22 changes: 11 additions & 11 deletions __mocks__/genInteractives.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ Object.keys(interactiveElementsMap)
.concat(Object.keys(nonInteractiveElementsMap))
.forEach((name: string) => delete indeterminantInteractiveElementsMap[name]);

const abstractRoles = roleNames.filter(role => roles.get(role).abstract);
const abstractRoles = roleNames.filter((role) => roles.get(role).abstract);

const nonAbstractRoles = roleNames.filter(role => !roles.get(role).abstract);
const nonAbstractRoles = roleNames.filter((role) => !roles.get(role).abstract);

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

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

export function genElementSymbol(openingElement: Object) {
return (
openingElement.name.name + (openingElement.attributes.length > 0
? `${openingElement.attributes
.map(attr => `[${attr.name.name}="${attr.value.value}"]`)
.map((attr) => `[${attr.name.name}="${attr.value.value}"]`)
.join('')}`
: ''
)
Expand Down Expand Up @@ -187,15 +187,15 @@ export function genNonInteractiveRoleElements() {
...nonInteractiveRoles,
'article button',
'fakerole article button',
].map(value => JSXElementMock('div', [JSXAttributeMock('role', value)]));
].map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
}

export function genAbstractRoleElements() {
return abstractRoles.map(value => JSXElementMock('div', [JSXAttributeMock('role', value)]));
return abstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
}

export function genNonAbstractRoleElements() {
return nonAbstractRoles.map(value => JSXElementMock('div', [JSXAttributeMock('role', value)]));
return nonAbstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
}

export function genIndeterminantInteractiveElements(): Array<TJSXElementMock> {
Expand Down
6 changes: 6 additions & 0 deletions __tests__/__util__/axeMapping.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* eslint-disable import/prefer-default-export, no-underscore-dangle */
import * as axe from 'axe-core';

export function axeFailMessage(checkId, data) {
return axe._audit.data.checks[checkId].messages.fail(data);
}
2 changes: 1 addition & 1 deletion __tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import path from 'path';
import plugin from '../src';

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

describe('all rule files should be exported by the plugin', () => {
rules.forEach((ruleName) => {
Expand Down
25 changes: 23 additions & 2 deletions __tests__/src/rules/alt-text-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,20 @@ import rule from '../../../src/rules/alt-text';

const ruleTester = new RuleTester();

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

const altValueError = type => ({
const altValueError = (type) => ({
message: `Invalid alt value for ${type}. \
Use alt="" for presentational images.`,
type: 'JSXOpeningElement',
});

const ariaLabelValueError = 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.';
const ariaLabelledbyValueError = 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.';

const preferAltError = () => ({
message: 'Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.',
type: 'JSXOpeningElement',
Expand Down Expand Up @@ -83,6 +86,8 @@ ruleTester.run('alt-text', rule, {
{ code: '<img alt={error ? "not working": "working"} />' },
{ code: '<img alt={undefined ? "working": "not working"} />' },
{ code: '<img alt={plugin.name + " Logo"} />' },
{ code: '<img aria-label="foo" />' },
{ code: '<img aria-labelledby="id1" />' },

// DEFAULT <object> TESTS
{ code: '<object aria-label="foo" />' },
Expand Down Expand Up @@ -168,25 +173,41 @@ ruleTester.run('alt-text', rule, {
{ code: '<img alt role="presentation" />;', errors: [altValueError('img')] },
{ code: '<img role="presentation" />;', errors: [preferAltError()] },
{ code: '<img role="none" />;', errors: [preferAltError()] },
{ code: '<img aria-label={undefined} />', errors: [ariaLabelValueError] },
{ code: '<img aria-labelledby={undefined} />', errors: [ariaLabelledbyValueError] },
{ code: '<img aria-label="" />', errors: [ariaLabelValueError] },
{ code: '<img aria-labelledby="" />', errors: [ariaLabelledbyValueError] },

// DEFAULT ELEMENT 'object' TESTS
{ code: '<object />', errors: [objectError] },
{ code: '<object><div aria-hidden /></object>', errors: [objectError] },
{ code: '<object title={undefined} />', errors: [objectError] },
{ code: '<object aria-label="" />', errors: [objectError] },
{ code: '<object aria-labelledby="" />', errors: [objectError] },
{ code: '<object aria-label={undefined} />', errors: [objectError] },
{ code: '<object aria-labelledby={undefined} />', errors: [objectError] },

// DEFAULT ELEMENT 'area' TESTS
{ code: '<area />', errors: [areaError] },
{ code: '<area alt />', errors: [areaError] },
{ code: '<area alt={undefined} />', errors: [areaError] },
{ code: '<area src="xyz" />', errors: [areaError] },
{ code: '<area {...this.props} />', errors: [areaError] },
{ code: '<area aria-label="" />', errors: [areaError] },
{ code: '<area aria-label={undefined} />', errors: [areaError] },
{ code: '<area aria-labelledby="" />', errors: [areaError] },
{ code: '<area aria-labelledby={undefined} />', errors: [areaError] },

// DEFAULT ELEMENT 'input type="image"' TESTS
{ code: '<input type="image" />', errors: [inputImageError] },
{ code: '<input type="image" alt />', errors: [inputImageError] },
{ code: '<input type="image" alt={undefined} />', errors: [inputImageError] },
{ code: '<input type="image">Foo</input>', errors: [inputImageError] },
{ code: '<input type="image" {...this.props} />', errors: [inputImageError] },
{ code: '<input type="image" aria-label="" />', errors: [inputImageError] },
{ code: '<input type="image" aria-label={undefined} />', errors: [inputImageError] },
{ code: '<input type="image" aria-labelledby="" />', errors: [inputImageError] },
{ code: '<input type="image" aria-labelledby={undefined} />', errors: [inputImageError] },

// CUSTOM ELEMENT TESTS FOR ARRAY OPTION TESTS
{
Expand Down
2 changes: 1 addition & 1 deletion __tests__/src/rules/aria-props-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const errorMessage = (name) => {
};

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

Expand Down
8 changes: 4 additions & 4 deletions __tests__/src/rules/aria-role-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,16 @@ const errorMessage = {

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

const validRoles = roleKeys.filter(role => roles.get(role).abstract === false);
const invalidRoles = roleKeys.filter(role => roles.get(role).abstract === true);
const validRoles = roleKeys.filter((role) => roles.get(role).abstract === false);
const invalidRoles = roleKeys.filter((role) => roles.get(role).abstract === true);

const createTests = roleNames => roleNames.map(role => ({
const createTests = (roleNames) => roleNames.map((role) => ({
code: `<div role="${role.toLowerCase()}" />`,
}));

const validTests = createTests(validRoles);
const invalidTests = createTests(invalidRoles).map((test) => {
const invalidTest = Object.assign({}, test);
const invalidTest = { ...test };
invalidTest.errors = [errorMessage];
return invalidTest;
});
Expand Down
10 changes: 5 additions & 5 deletions __tests__/src/rules/aria-unsupported-elements-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import rule from '../../../src/rules/aria-unsupported-elements';

const ruleTester = new RuleTester();

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

// Generate invalid test cases.
const invalidRoleValidityTests = domElements
.filter(element => Boolean(dom.get(element).reserved))
.map(reservedElem => ({
.filter((element) => Boolean(dom.get(element).reserved))
.map((reservedElem) => ({
code: `<${reservedElem} role {...props} />`,
errors: [errorMessage('role')],
}));

const invalidAriaValidityTests = domElements
.filter(element => Boolean(dom.get(element).reserved))
.map(reservedElem => ({
.filter((element) => Boolean(dom.get(element).reserved))
.map((reservedElem) => ({
code: `<${reservedElem} aria-hidden aria-role="none" {...props} />`,
errors: [errorMessage('aria-hidden')],
}));
Expand Down
64 changes: 64 additions & 0 deletions __tests__/src/rules/autocomplete-valid-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* eslint-env jest */
/**
* @fileoverview Ensure autocomplete attribute is correct.
* @author Wilco Fiers
*/

// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------

import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import { axeFailMessage } from '../../__util__/axeMapping';
import rule from '../../../src/rules/autocomplete-valid';

// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------

const ruleTester = new RuleTester();

const invalidAutocomplete = [{
message: axeFailMessage('autocomplete-valid'),
type: 'JSXOpeningElement',
}];

const inappropriateAutocomplete = [{
message: axeFailMessage('autocomplete-appropriate'),
type: 'JSXOpeningElement',
}];

ruleTester.run('autocomplete-valid', rule, {
valid: [
// INAPPLICABLE
{ code: '<input type="text" />;' },
// // PASSED AUTOCOMPLETE
{ code: '<input type="text" autocomplete="name" />;' },
{ code: '<input type="text" autocomplete="" />;' },
{ code: '<input type="text" autocomplete="off" />;' },
{ code: '<input type="text" autocomplete="on" />;' },
{ code: '<input type="text" autocomplete="billing family-name" />;' },
{ code: '<input type="text" autocomplete="section-blue shipping street-address" />;' },
{ code: '<input type="text" autocomplete="section-somewhere shipping work email" />;' },
{ code: '<input type="text" autocomplete />;' },
{ code: '<input type="text" autocomplete={autocompl} />;' },
{ code: '<input type="text" autocomplete={autocompl || "name"} />;' },
{ code: '<input type="text" autocomplete={autocompl || "foo"} />;' },
{ code: '<Foo autocomplete="bar"></Foo>;' },
].map(parserOptionsMapper),
invalid: [
// FAILED "autocomplete-valid"
{ code: '<input type="text" autocomplete="foo" />;', errors: invalidAutocomplete },
{ code: '<input type="text" autocomplete="name invalid" />;', errors: invalidAutocomplete },
{ code: '<input type="text" autocomplete="invalid name" />;', errors: invalidAutocomplete },
{ code: '<input type="text" autocomplete="home url" />;', errors: invalidAutocomplete },
{ code: '<Bar autocomplete="baz"></Bar>;', errors: invalidAutocomplete, options: [{ inputComponents: ['Bar'] }] },

// FAILED "autocomplete-appropriate"
{ code: '<input type="date" autocomplete="email" />;', errors: inappropriateAutocomplete },
{ code: '<input type="number" autocomplete="url" />;', errors: inappropriateAutocomplete },
{ code: '<input type="month" autocomplete="tel" />;', errors: inappropriateAutocomplete },
{ code: '<Foo type="month" autocomplete="tel"></Foo>;', errors: inappropriateAutocomplete, options: [{ inputComponents: ['Foo'] }] },
].map(parserOptionsMapper),
});

0 comments on commit 4758cbf

Please sign in to comment.