diff --git a/README.md b/README.md index c75e78dad..214dcc7d0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/__tests__/__util__/parserOptionsMapper.js b/__tests__/__util__/parserOptionsMapper.js index c3d2f77bd..74db94a78 100644 --- a/__tests__/__util__/parserOptionsMapper.js +++ b/__tests__/__util__/parserOptionsMapper.js @@ -11,6 +11,7 @@ export default function parserOptionsMapper({ errors, options = [], parserOptions = {}, + settings, }) { return { code, @@ -20,5 +21,6 @@ export default function parserOptionsMapper({ ...defaultParserOptions, ...parserOptions, }, + settings, }; } diff --git a/__tests__/__util__/ruleOptionsMapperFactory.js b/__tests__/__util__/ruleOptionsMapperFactory.js index 7175bf91b..1e1b628b0 100644 --- a/__tests__/__util__/ruleOptionsMapperFactory.js +++ b/__tests__/__util__/ruleOptionsMapperFactory.js @@ -6,7 +6,8 @@ type ESLintTestRunnerTestCase = { code: string, errors: ?Array<{ message: string, type: string }>, options: ?Array, - parserOptions: ?Array + parserOptions: ?Array, + settings?: {[string]: mixed}, }; type RuleOptionsMapperFactoryType = ( @@ -15,7 +16,7 @@ type RuleOptionsMapperFactoryType = ( export default function ruleOptionsMapperFactory(ruleOptions: Array = []): RuleOptionsMapperFactoryType { // eslint-disable-next-line - return ({ code, errors, options, parserOptions }: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => { + return ({ code, errors, options, parserOptions, settings }: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => { return { code, errors, @@ -25,6 +26,7 @@ export default function ruleOptionsMapperFactory(ruleOptions: Array = []) ...item, }], [{}]), parserOptions, + settings, }; }; } diff --git a/__tests__/src/rules/accessible-emoji-test.js b/__tests__/src/rules/accessible-emoji-test.js index bcdb42964..dc3d19683 100644 --- a/__tests__/src/rules/accessible-emoji-test.js +++ b/__tests__/src/rules/accessible-emoji-test.js @@ -37,6 +37,11 @@ ruleTester.run('accessible-emoji', rule, { { code: '' }, { code: '🐼' }, { code: '' }, + { code: '🐼' }, + { + code: '🐼', + settings: { 'jsx-a11y': { components: { CustomInput: 'input' } } }, + }, ].map(parserOptionsMapper), invalid: [ { code: '🐼', errors: [expectedError] }, @@ -46,5 +51,6 @@ ruleTester.run('accessible-emoji', rule, { { code: '🐼', errors: [expectedError] }, { code: '🐼', errors: [expectedError] }, { code: '🐼', errors: [expectedError] }, + { code: '🐼', errors: [expectedError] }, ].map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/alt-text-test.js b/__tests__/src/rules/alt-text-test.js index 5379376fc..bd2463d73 100644 --- a/__tests__/src/rules/alt-text-test.js +++ b/__tests__/src/rules/alt-text-test.js @@ -42,6 +42,14 @@ const areaError = 'Each area of an image map must have a text alternative throug const inputImageError = ' 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'], @@ -112,6 +120,7 @@ ruleTester.run('alt-text', rule, { { code: '' }, { code: '' }, { code: '' }, + { code: '', settings: componentsSettings }, // CUSTOM ELEMENT TESTS FOR ARRAY OPTION TESTS { code: ';', options: array }, @@ -263,5 +272,6 @@ ruleTester.run('alt-text', rule, { { code: '', errors: [inputImageError], options: array }, { code: 'Foo', errors: [inputImageError], options: array }, { code: '', errors: [inputImageError], options: array }, + { code: '', errors: [inputImageError], settings: componentsSettings }, ].map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/anchor-has-content-test.js b/__tests__/src/rules/anchor-has-content-test.js index 77559da31..0935395e0 100644 --- a/__tests__/src/rules/anchor-has-content-test.js +++ b/__tests__/src/rules/anchor-has-content-test.js @@ -31,10 +31,20 @@ ruleTester.run('anchor-has-content', rule, { { code: '{foo.bar}' }, { code: '' }, { code: '' }, + { code: '' }, + { + code: 'foo', + settings: { 'jsx-a11y': { components: { Link: 'a' } } }, + }, ].map(parserOptionsMapper), invalid: [ { code: '', errors: [expectedError] }, { code: '', errors: [expectedError] }, { code: '{undefined}', errors: [expectedError] }, + { + code: '', + errors: [expectedError], + settings: { 'jsx-a11y': { components: { Link: 'a' } } }, + }, ].map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/anchor-is-valid-test.js b/__tests__/src/rules/anchor-is-valid-test.js index fd7f44940..5b368d9e7 100644 --- a/__tests__/src/rules/anchor-is-valid-test.js +++ b/__tests__/src/rules/anchor-is-valid-test.js @@ -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: ';' }, + { code: '' }, { code: '' }, { code: '' }, { code: '' }, @@ -119,6 +128,7 @@ ruleTester.run('anchor-is-valid', rule, { { code: '', options: components }, { code: '', options: components }, { code: '', options: components }, + { code: '', settings: componentsSettings }, // CUSTOM PROP TESTS { code: '', options: specialLink }, @@ -332,6 +342,11 @@ ruleTester.run('anchor-is-valid', rule, { errors: [preferButtonexpectedError], options: components, }, + { + code: ' void 0} />', + errors: [preferButtonexpectedError], + settings: componentsSettings, + }, // CUSTOM PROP TESTS // NO HREF diff --git a/__tests__/src/rules/aria-activedescendant-has-tabindex-test.js b/__tests__/src/rules/aria-activedescendant-has-tabindex-test.js index 917898e80..6c251cf23 100644 --- a/__tests__/src/rules/aria-activedescendant-has-tabindex-test.js +++ b/__tests__/src/rules/aria-activedescendant-has-tabindex-test.js @@ -36,6 +36,10 @@ ruleTester.run('aria-activedescendant-has-tabindex', rule, { { code: ';', }, + { + code: ';', + settings: { 'jsx-a11y': { components: { CustomComponent: 'div' } } }, + }, { code: '
;', }, @@ -81,5 +85,10 @@ ruleTester.run('aria-activedescendant-has-tabindex', rule, { code: '
;', errors: [expectedError], }, + { + code: ';', + errors: [expectedError], + settings: { 'jsx-a11y': { components: { CustomComponent: 'div' } } }, + }, ].map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/aria-role-test.js b/__tests__/src/rules/aria-role-test.js index a9cdfeb51..9ccca139d 100644 --- a/__tests__/src/rules/aria-role-test.js +++ b/__tests__/src/rules/aria-role-test.js @@ -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. @@ -66,6 +74,11 @@ ruleTester.run('aria-role', rule, { { code: '', options: ignoreNonDOMSchema }, { code: '', options: ignoreNonDOMSchema }, { code: '', options: ignoreNonDOMSchema }, + { + code: '
', + errors: [errorMessage], + settings: customDivSettings, + }, ].concat(validTests).map(parserOptionsMapper), invalid: [ @@ -82,5 +95,12 @@ ruleTester.run('aria-role', rule, { { code: '
', errors: [errorMessage] }, { code: '', errors: [errorMessage] }, { code: '', errors: [errorMessage] }, + { code: '
', errors: [errorMessage], settings: customDivSettings }, + { + code: '
', + errors: [errorMessage], + options: ignoreNonDOMSchema, + settings: customDivSettings, + }, ].concat(invalidTests).map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/aria-unsupported-elements-test.js b/__tests__/src/rules/aria-unsupported-elements-test.js index d34b517eb..b29a617c5 100644 --- a/__tests__/src/rules/aria-unsupported-elements-test.js +++ b/__tests__/src/rules/aria-unsupported-elements-test.js @@ -54,7 +54,11 @@ const invalidRoleValidityTests = domElements .map((reservedElem) => ({ code: `<${reservedElem} role {...props} />`, errors: [errorMessage('role')], - })); + })).concat({ + code: '', + errors: [errorMessage('aria-hidden')], + settings: { 'jsx-a11y': { components: { Meta: 'meta' } } }, + }); const invalidAriaValidityTests = domElements .filter((element) => Boolean(dom.get(element).reserved)) diff --git a/__tests__/src/rules/autocomplete-valid-test.js b/__tests__/src/rules/autocomplete-valid-test.js index ef731b19c..49f194067 100644 --- a/__tests__/src/rules/autocomplete-valid-test.js +++ b/__tests__/src/rules/autocomplete-valid-test.js @@ -28,6 +28,14 @@ const inappropriateAutocomplete = [{ type: 'JSXOpeningElement', }]; +const componentsSettings = { + 'jsx-a11y': { + components: { + Input: 'input', + }, + }, +}; + ruleTester.run('autocomplete-valid', rule, { valid: [ // INAPPLICABLE @@ -46,6 +54,8 @@ ruleTester.run('autocomplete-valid', rule, { { code: ';' }, { code: ';' }, { code: ';' }, + { code: '', settings: componentsSettings }, + { code: '' }, // PASSED "autocomplete-appropriate" // see also: https://github.com/dequelabs/axe-core/issues/2912 @@ -61,5 +71,6 @@ ruleTester.run('autocomplete-valid', rule, { { code: ';', errors: invalidAutocomplete }, { code: ';', errors: invalidAutocomplete }, { code: ';', errors: invalidAutocomplete, options: [{ inputComponents: ['Bar'] }] }, + { code: '', errors: invalidAutocomplete, settings: componentsSettings }, ].map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/click-events-have-key-events-test.js b/__tests__/src/rules/click-events-have-key-events-test.js index 89965a57b..5460504f6 100644 --- a/__tests__/src/rules/click-events-have-key-events-test.js +++ b/__tests__/src/rules/click-events-have-key-events-test.js @@ -51,6 +51,7 @@ ruleTester.run('click-events-have-key-events', rule, { { code: '
void 0} role="none" />;' }, { code: '' }, { code: '' }, { code: '' }, @@ -255,6 +256,7 @@ const neverValid = [ { code: '', errors: [expectedError] }, { code: '', options: [{ depth: 3 }], errors: [expectedError] }, { code: '', options: [{ depth: 3, controlComponents: ['CustomControl'] }], errors: [expectedError] }, + { code: '', errors: [expectedError], settings: { 'jsx-a11y': { components: { CustomControl: 'button' } } } }, { code: '', errors: [expectedError] }, { code: '', errors: [expectedError] }, { code: '', errors: [expectedError] }, diff --git a/__tests__/src/rules/heading-has-content-test.js b/__tests__/src/rules/heading-has-content-test.js index e52930452..a2a201be0 100644 --- a/__tests__/src/rules/heading-has-content-test.js +++ b/__tests__/src/rules/heading-has-content-test.js @@ -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 @@ -51,16 +61,24 @@ ruleTester.run('heading-has-content', rule, { { code: '', options: components }, { code: '', options: components }, { code: '

' }, + // CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS + { code: 'Foo', settings: componentsSettings }, + { code: '

' }, ].map(parserOptionsMapper), invalid: [ // DEFAULT ELEMENT TESTS { code: '

', errors: [expectedError] }, { code: '

', errors: [expectedError] }, { code: '

{undefined}

', errors: [expectedError] }, + { code: '

', errors: [expectedError] }, // CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION { code: '', errors: [expectedError], options: components }, { code: '', errors: [expectedError], options: components }, { code: '{undefined}', errors: [expectedError], options: components }, + + // CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS + { code: '', errors: [expectedError], settings: componentsSettings }, + { code: '

', errors: [expectedError], settings: componentsSettings }, ].map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/html-has-lang-test.js b/__tests__/src/rules/html-has-lang-test.js index 61348f676..ba9e58d0a 100644 --- a/__tests__/src/rules/html-has-lang-test.js +++ b/__tests__/src/rules/html-has-lang-test.js @@ -30,10 +30,12 @@ ruleTester.run('html-has-lang', rule, { { code: '' }, { code: '' }, { code: '' }, + { code: '', errors: [expectedError], settings: { 'jsx-a11y': { components: { HTMLTop: 'html' } } } }, ].map(parserOptionsMapper), invalid: [ { code: '', errors: [expectedError] }, { code: '', errors: [expectedError] }, { code: '', errors: [expectedError] }, + { code: '', errors: [expectedError], settings: { 'jsx-a11y': { components: { HTMLTop: 'html' } } } }, ].map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/iframe-has-title-test.js b/__tests__/src/rules/iframe-has-title-test.js index 195fdba44..716be75e0 100644 --- a/__tests__/src/rules/iframe-has-title-test.js +++ b/__tests__/src/rules/iframe-has-title-test.js @@ -22,12 +22,21 @@ const expectedError = { type: 'JSXOpeningElement', }; +const componentsSettings = { + 'jsx-a11y': { + components: { + FooComponent: 'iframe', + }, + }, +}; + ruleTester.run('html-has-lang', rule, { valid: [ { code: '
;' }, { code: '