Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New] Introduce a plugin-wide setting for custom components. #844

Merged
merged 1 commit into from Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Expand Up @@ -89,6 +89,24 @@ Add `plugin:jsx-a11y/recommended` or `plugin:jsx-a11y/strict` in `extends`:
}
```

To enable your custom components to be checked as DOM elements, you can set global settings in your
configuration file by mapping each custom component name to a DOM element type.

```json
{
"settings": {
"jsx-a11y": {
"components": {
"CityInput": "input",
"CustomButton": "button",
"MyButton": "button",
"RoundButton": "button"
}
}
}
}
```

## Supported Rules

<!-- AUTO-GENERATED-CONTENT:START (LIST) -->
Expand Down
2 changes: 2 additions & 0 deletions __tests__/__util__/parserOptionsMapper.js
Expand Up @@ -11,6 +11,7 @@ export default function parserOptionsMapper({
errors,
options = [],
parserOptions = {},
settings,
}) {
return {
code,
Expand All @@ -20,5 +21,6 @@ export default function parserOptionsMapper({
...defaultParserOptions,
...parserOptions,
},
settings,
};
}
6 changes: 4 additions & 2 deletions __tests__/__util__/ruleOptionsMapperFactory.js
Expand Up @@ -6,7 +6,8 @@ type ESLintTestRunnerTestCase = {
code: string,
errors: ?Array<{ message: string, type: string }>,
options: ?Array<mixed>,
parserOptions: ?Array<mixed>
parserOptions: ?Array<mixed>,
settings?: {[string]: mixed},
};

type RuleOptionsMapperFactoryType = (
Expand All @@ -15,7 +16,7 @@ type RuleOptionsMapperFactoryType = (

export default function ruleOptionsMapperFactory(ruleOptions: Array<mixed> = []): RuleOptionsMapperFactoryType {
// eslint-disable-next-line
return ({ code, errors, options, parserOptions }: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => {
return ({ code, errors, options, parserOptions, settings }: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => {
return {
code,
errors,
Expand All @@ -25,6 +26,7 @@ export default function ruleOptionsMapperFactory(ruleOptions: Array<mixed> = [])
...item,
}], [{}]),
parserOptions,
settings,
};
};
}
6 changes: 6 additions & 0 deletions __tests__/src/rules/accessible-emoji-test.js
Expand Up @@ -37,6 +37,11 @@ ruleTester.run('accessible-emoji', rule, {
{ code: '<span aria-hidden="true">🐼</span>' },
{ code: '<span aria-hidden>🐼</span>' },
{ code: '<div aria-hidden="true">🐼</div>' },
{ code: '<input type="hidden">🐼</input>' },
{
code: '<CustomInput type="hidden">🐼</CustomInput>',
settings: { 'jsx-a11y': { components: { CustomInput: 'input' } } },
pcorpet marked this conversation as resolved.
Show resolved Hide resolved
},
].map(parserOptionsMapper),
invalid: [
{ code: '<span>🐼</span>', errors: [expectedError] },
Expand All @@ -46,5 +51,6 @@ ruleTester.run('accessible-emoji', rule, {
{ code: '<i role="img" aria-labelledby="id1">🐼</i>', errors: [expectedError] },
{ code: '<Foo>🐼</Foo>', errors: [expectedError] },
{ code: '<span aria-hidden="false">🐼</span>', errors: [expectedError] },
{ code: '<CustomInput type="hidden">🐼</CustomInput>', errors: [expectedError] },
].map(parserOptionsMapper),
});
10 changes: 10 additions & 0 deletions __tests__/src/rules/alt-text-test.js
Expand Up @@ -42,6 +42,14 @@ const areaError = 'Each area of an image map must have a text alternative throug

const inputImageError = '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.';

const componentsSettings = {
'jsx-a11y': {
components: {
Input: 'input',
},
},
};

const array = [{
img: ['Thumbnail', 'Image'],
object: ['Object'],
Expand Down Expand Up @@ -112,6 +120,7 @@ ruleTester.run('alt-text', rule, {
{ code: '<input type="image" alt="This is descriptive!" />' },
{ code: '<input type="image" alt={altText} />' },
{ code: '<InputImage />' },
{ code: '<Input type="image" alt="" />', settings: componentsSettings },

// CUSTOM ELEMENT TESTS FOR ARRAY OPTION TESTS
{ code: '<Thumbnail alt="foo" />;', options: array },
Expand Down Expand Up @@ -263,5 +272,6 @@ ruleTester.run('alt-text', rule, {
{ code: '<InputImage alt={undefined} />', errors: [inputImageError], options: array },
{ code: '<InputImage>Foo</InputImage>', errors: [inputImageError], options: array },
{ code: '<InputImage {...this.props} />', errors: [inputImageError], options: array },
{ code: '<Input type="image" />', errors: [inputImageError], settings: componentsSettings },
].map(parserOptionsMapper),
});
10 changes: 10 additions & 0 deletions __tests__/src/rules/anchor-has-content-test.js
Expand Up @@ -31,10 +31,20 @@ ruleTester.run('anchor-has-content', rule, {
{ code: '<a>{foo.bar}</a>' },
{ code: '<a dangerouslySetInnerHTML={{ __html: "foo" }} />' },
{ code: '<a children={children} />' },
{ code: '<Link />' },
{
code: '<Link>foo</Link>',
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
].map(parserOptionsMapper),
invalid: [
{ code: '<a />', errors: [expectedError] },
{ code: '<a><Bar aria-hidden /></a>', errors: [expectedError] },
{ code: '<a>{undefined}</a>', errors: [expectedError] },
{
code: '<Link />',
errors: [expectedError],
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
].map(parserOptionsMapper),
});
17 changes: 16 additions & 1 deletion __tests__/src/rules/anchor-is-valid-test.js
Expand Up @@ -78,10 +78,19 @@ const componentsAndSpecialLinkAndNoHrefAspect = [{
aspects: ['noHref'],
}];

const componentsSettings = {
'jsx-a11y': {
components: {
Anchor: 'a',
Link: 'a',
},
},
};

ruleTester.run('anchor-is-valid', rule, {
valid: [
// DEFAULT ELEMENT 'a' TESTS
{ code: '<Anchor />;' },
{ code: '<Anchor />' },
{ code: '<a {...props} />' },
{ code: '<a href="foo" />' },
{ code: '<a href={foo} />' },
Expand Down Expand Up @@ -119,6 +128,7 @@ ruleTester.run('anchor-is-valid', rule, {
{ code: '<Link href={`#foo`}/>', options: components },
{ code: '<Link href={"foo"}/>', options: components },
{ code: '<Link href="#foo" />', options: components },
{ code: '<Link href="#foo" />', settings: componentsSettings },

// CUSTOM PROP TESTS
{ code: '<a {...props} />', options: specialLink },
Expand Down Expand Up @@ -332,6 +342,11 @@ ruleTester.run('anchor-is-valid', rule, {
errors: [preferButtonexpectedError],
options: components,
},
{
code: '<Link href="#" onClick={() => void 0} />',
errors: [preferButtonexpectedError],
settings: componentsSettings,
},

// CUSTOM PROP TESTS
// NO HREF
Expand Down
Expand Up @@ -36,6 +36,10 @@ ruleTester.run('aria-activedescendant-has-tabindex', rule, {
{
code: '<CustomComponent aria-activedescendant={someID} tabIndex={-1} />;',
},
{
code: '<CustomComponent aria-activedescendant={someID} tabIndex={0} />;',
settings: { 'jsx-a11y': { components: { CustomComponent: 'div' } } },
},
{
code: '<div />;',
},
Expand Down Expand Up @@ -81,5 +85,10 @@ ruleTester.run('aria-activedescendant-has-tabindex', rule, {
code: '<div aria-activedescendant={someID} />;',
errors: [expectedError],
},
{
code: '<CustomComponent aria-activedescendant={someID} />;',
errors: [expectedError],
settings: { 'jsx-a11y': { components: { CustomComponent: 'div' } } },
},
].map(parserOptionsMapper),
});
20 changes: 20 additions & 0 deletions __tests__/src/rules/aria-role-test.js
Expand Up @@ -47,6 +47,14 @@ const ignoreNonDOMSchema = [{
ignoreNonDOM: true,
}];

const customDivSettings = {
'jsx-a11y': {
components: {
Div: 'div',
},
},
};

ruleTester.run('aria-role', rule, {
valid: [
// Variables should pass, as we are only testing literals.
Expand All @@ -66,6 +74,11 @@ ruleTester.run('aria-role', rule, {
{ code: '<Foo role="bar" />', options: ignoreNonDOMSchema },
{ code: '<fakeDOM role="bar" />', options: ignoreNonDOMSchema },
{ code: '<img role="presentation" />', options: ignoreNonDOMSchema },
{
code: '<Div role="button" />',
errors: [errorMessage],
settings: customDivSettings,
},
].concat(validTests).map(parserOptionsMapper),

invalid: [
Expand All @@ -82,5 +95,12 @@ ruleTester.run('aria-role', rule, {
{ code: '<div role={null}></div>', errors: [errorMessage] },
{ code: '<Foo role="datepicker" />', errors: [errorMessage] },
{ code: '<Foo role="Button" />', errors: [errorMessage] },
{ code: '<Div role="Button" />', errors: [errorMessage], settings: customDivSettings },
{
code: '<Div role="Button" />',
errors: [errorMessage],
options: ignoreNonDOMSchema,
settings: customDivSettings,
},
].concat(invalidTests).map(parserOptionsMapper),
});
6 changes: 5 additions & 1 deletion __tests__/src/rules/aria-unsupported-elements-test.js
Expand Up @@ -54,7 +54,11 @@ const invalidRoleValidityTests = domElements
.map((reservedElem) => ({
code: `<${reservedElem} role {...props} />`,
errors: [errorMessage('role')],
}));
})).concat({
code: '<Meta aria-hidden />',
errors: [errorMessage('aria-hidden')],
settings: { 'jsx-a11y': { components: { Meta: 'meta' } } },
});

const invalidAriaValidityTests = domElements
.filter((element) => Boolean(dom.get(element).reserved))
Expand Down
11 changes: 11 additions & 0 deletions __tests__/src/rules/autocomplete-valid-test.js
Expand Up @@ -28,6 +28,14 @@ const inappropriateAutocomplete = [{
type: 'JSXOpeningElement',
}];

const componentsSettings = {
'jsx-a11y': {
components: {
Input: 'input',
},
},
};

ruleTester.run('autocomplete-valid', rule, {
valid: [
// INAPPLICABLE
Expand All @@ -46,6 +54,8 @@ ruleTester.run('autocomplete-valid', rule, {
{ code: '<input type="text" autocomplete={autocompl || "foo"} />;' },
{ code: '<Foo autocomplete="bar"></Foo>;' },
{ code: '<input type={isEmail ? "email" : "text"} autocomplete="none" />;' },
{ code: '<Input type="text" autocomplete="name" />', settings: componentsSettings },
{ code: '<Input type="text" autocomplete="baz" />' },

// PASSED "autocomplete-appropriate"
// see also: https://github.com/dequelabs/axe-core/issues/2912
Expand All @@ -61,5 +71,6 @@ ruleTester.run('autocomplete-valid', rule, {
{ 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'] }] },
{ code: '<Input type="text" autocomplete="baz" />', errors: invalidAutocomplete, settings: componentsSettings },
].map(parserOptionsMapper),
});
2 changes: 2 additions & 0 deletions __tests__/src/rules/click-events-have-key-events-test.js
Expand Up @@ -51,6 +51,7 @@ ruleTester.run('click-events-have-key-events', rule, {
{ code: '<div onClick={() => void 0} role="none" />;' },
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />' },
{ code: '<Footer onClick={doFoo} />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<div onClick={() => void 0} />;', errors: [expectedError] },
Expand All @@ -70,5 +71,6 @@ ruleTester.run('click-events-have-key-events', rule, {
},
{ code: '<a onClick={() => void 0} />', errors: [expectedError] },
{ code: '<a tabIndex="0" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<Footer onClick={doFoo} />', errors: [expectedError], settings: { 'jsx-a11y': { components: { Footer: 'footer' } } } },
].map(parserOptionsMapper),
});
2 changes: 2 additions & 0 deletions __tests__/src/rules/control-has-associated-label-test.js
Expand Up @@ -30,6 +30,7 @@ const alwaysValid = [
// Custom Control Components
{ code: '<CustomControl><span><span>Save</span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'] }] },
{ code: '<CustomControl><span><span label="Save"></span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'], labelAttributes: ['label'] }] },
{ code: '<CustomControl>Save</CustomControl>', settings: { 'jsx-a11y': { components: { CustomControl: 'button' } } } },
// Interactive Elements
{ code: '<button>Save</button>' },
{ code: '<button><span>Save</span></button>' },
Expand Down Expand Up @@ -255,6 +256,7 @@ const neverValid = [
{ code: '<button><span title="This is not a real label" /></button>', errors: [expectedError] },
{ code: '<button><span><span><span>Save</span></span></span></button>', options: [{ depth: 3 }], errors: [expectedError] },
{ code: '<CustomControl><span><span></span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'] }], errors: [expectedError] },
{ code: '<CustomControl></CustomControl>', errors: [expectedError], settings: { 'jsx-a11y': { components: { CustomControl: 'button' } } } },
{ code: '<a href="#" />', errors: [expectedError] },
{ code: '<area href="#" />', errors: [expectedError] },
{ code: '<menuitem />', errors: [expectedError] },
Expand Down
18 changes: 18 additions & 0 deletions __tests__/src/rules/heading-has-content-test.js
Expand Up @@ -26,6 +26,16 @@ const components = [{
components: ['Heading', 'Title'],
}];

const componentsSettings = {
'jsx-a11y': {
components: {
CustomInput: 'input',
Title: 'h1',
Heading: 'h2',
},
},
};

ruleTester.run('heading-has-content', rule, {
valid: [
// DEFAULT ELEMENT TESTS
Expand All @@ -51,16 +61,24 @@ ruleTester.run('heading-has-content', rule, {
{ code: '<Heading dangerouslySetInnerHTML={{ __html: "foo" }} />', options: components },
{ code: '<Heading children={children} />', options: components },
{ code: '<h1 aria-hidden />' },
// CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
{ code: '<Heading>Foo</Heading>', settings: componentsSettings },
{ code: '<h1><CustomInput type="hidden" /></h1>' },
].map(parserOptionsMapper),
invalid: [
// DEFAULT ELEMENT TESTS
{ code: '<h1 />', errors: [expectedError] },
{ code: '<h1><Bar aria-hidden /></h1>', errors: [expectedError] },
{ code: '<h1>{undefined}</h1>', errors: [expectedError] },
{ code: '<h1><input type="hidden" /></h1>', errors: [expectedError] },

// CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION
{ code: '<Heading />', errors: [expectedError], options: components },
{ code: '<Heading><Bar aria-hidden /></Heading>', errors: [expectedError], options: components },
{ code: '<Heading>{undefined}</Heading>', errors: [expectedError], options: components },

// CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
{ code: '<Heading />', errors: [expectedError], settings: componentsSettings },
{ code: '<h1><CustomInput type="hidden" /></h1>', errors: [expectedError], settings: componentsSettings },
].map(parserOptionsMapper),
});
2 changes: 2 additions & 0 deletions __tests__/src/rules/html-has-lang-test.js
Expand Up @@ -30,10 +30,12 @@ ruleTester.run('html-has-lang', rule, {
{ code: '<html lang={foo} />' },
{ code: '<html lang />' },
{ code: '<HTML />' },
{ code: '<HTMLTop lang="en" />', errors: [expectedError], settings: { 'jsx-a11y': { components: { HTMLTop: 'html' } } } },
].map(parserOptionsMapper),
invalid: [
{ code: '<html />', errors: [expectedError] },
{ code: '<html {...props} />', errors: [expectedError] },
{ code: '<html lang={undefined} />', errors: [expectedError] },
{ code: '<HTMLTop />', errors: [expectedError], settings: { 'jsx-a11y': { components: { HTMLTop: 'html' } } } },
].map(parserOptionsMapper),
});
10 changes: 10 additions & 0 deletions __tests__/src/rules/iframe-has-title-test.js
Expand Up @@ -22,12 +22,21 @@ const expectedError = {
type: 'JSXOpeningElement',
};

const componentsSettings = {
'jsx-a11y': {
components: {
FooComponent: 'iframe',
},
},
};

ruleTester.run('html-has-lang', rule, {
valid: [
{ code: '<div />;' },
{ code: '<iframe title="Unique title" />' },
{ code: '<iframe title={foo} />' },
{ code: '<FooComponent />' },
{ code: '<FooComponent title="Unique title" />', settings: componentsSettings },
].map(parserOptionsMapper),
invalid: [
{ code: '<iframe />', errors: [expectedError] },
Expand All @@ -40,5 +49,6 @@ ruleTester.run('html-has-lang', rule, {
{ code: '<iframe title={``} />', errors: [expectedError] },
{ code: '<iframe title={""} />', errors: [expectedError] },
{ code: '<iframe title={42} />', errors: [expectedError] },
{ code: '<FooComponent />', errors: [expectedError], settings: componentsSettings },
].map(parserOptionsMapper),
});