diff --git a/README.md b/README.md index 5c7fe52d..bffcccbb 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ This config will be interpreted in the following way: | :------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------- | :- | :- | :- | | [a11y-aria-label-is-well-formatted](docs/rules/a11y-aria-label-is-well-formatted.md) | [aria-label] text should be formatted as you would visual text. | ⚛️ | | | | [a11y-no-generic-link-text](docs/rules/a11y-no-generic-link-text.md) | disallow generic link text | | | ❌ | +| [a11y-no-title-attribute](docs/rules/a11y-no-title-attribute.md) | Guards against developers using the title attribute | ⚛️ | | | | [a11y-no-visually-hidden-interactive-element](docs/rules/a11y-no-visually-hidden-interactive-element.md) | Ensures that interactive elements are not visually hidden | ⚛️ | | | | [a11y-svg-has-accessible-name](docs/rules/a11y-svg-has-accessible-name.md) | SVGs must have an accessible name | ⚛️ | | | | [array-foreach](docs/rules/array-foreach.md) | enforce `for..of` loops over `Array.forEach` | ✅ | | | diff --git a/docs/rules/a11y-no-title-attribute.md b/docs/rules/a11y-no-title-attribute.md new file mode 100644 index 00000000..29b382d1 --- /dev/null +++ b/docs/rules/a11y-no-title-attribute.md @@ -0,0 +1,45 @@ +# Guards against developers using the title attribute (`github/a11y-no-title-attribute`) + +💼 This rule is enabled in the ⚛️ `react` config. + + + +The title attribute is strongly discouraged. The only exception is on an ` +``` + +## Version diff --git a/lib/configs/react.js b/lib/configs/react.js index 2644a266..98c95304 100644 --- a/lib/configs/react.js +++ b/lib/configs/react.js @@ -11,6 +11,7 @@ module.exports = { 'jsx-a11y/role-supports-aria-props': 'off', // Override with github/role-supports-aria-props until https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/910 is resolved 'github/a11y-aria-label-is-well-formatted': 'error', 'github/a11y-no-visually-hidden-interactive-element': 'error', + 'github/a11y-no-title-attribute': 'error', 'github/a11y-svg-has-accessible-name': 'error', 'github/role-supports-aria-props': 'error', 'jsx-a11y/no-aria-hidden-on-focusable': 'error', diff --git a/lib/index.js b/lib/index.js index d6a08fa3..38036e0f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,6 +2,7 @@ module.exports = { rules: { 'a11y-no-visually-hidden-interactive-element': require('./rules/a11y-no-visually-hidden-interactive-element'), 'a11y-no-generic-link-text': require('./rules/a11y-no-generic-link-text'), + 'a11y-no-title-attribute': require('./rules/a11y-no-title-attribute'), 'a11y-aria-label-is-well-formatted': require('./rules/a11y-aria-label-is-well-formatted'), 'a11y-svg-has-accessible-name': require('./rules/a11y-svg-has-accessible-name'), 'array-foreach': require('./rules/array-foreach'), diff --git a/lib/rules/a11y-no-title-attribute.js b/lib/rules/a11y-no-title-attribute.js new file mode 100644 index 00000000..83d61719 --- /dev/null +++ b/lib/rules/a11y-no-title-attribute.js @@ -0,0 +1,66 @@ +const {getProp, getPropValue} = require('jsx-ast-utils') +const {getElementType} = require('../utils/get-element-type') + +const SEMANTIC_ELEMENTS = [ + 'a', + 'button', + 'summary', + 'select', + 'option', + 'textarea', + 'input', + 'span', + 'div', + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'details', + 'summary', + 'dialog', + 'tr', + 'th', + 'td', + 'label', +] + +const ifSemanticElement = (context, node) => { + const elementType = getElementType(context, node.openingElement, true) + + for (const semanticElement of SEMANTIC_ELEMENTS) { + if (elementType === semanticElement) { + return true + } + } + return false +} + +module.exports = { + meta: { + docs: { + description: 'Guards against developers using the title attribute', + url: require('../url')(module), + }, + schema: [], + }, + + create(context) { + return { + JSXElement: node => { + const elementType = getElementType(context, node.openingElement) + if (elementType !== `iframe` && ifSemanticElement(context, node)) { + const titleProp = getPropValue(getProp(node.openingElement.attributes, `title`)) + if (titleProp) { + context.report({ + node, + message: 'The title attribute is not accessible and should never be used unless for an `'}, + {code: 'some information'}, + {code: 'GitHub'}, + { + code: 'Submit', + settings: { + github: { + components: { + Component: 'iframe', + }, + }, + }, + }, + { + // Note: we are only checking semantic elements. We cannot make assumptions about how a React Components is using the title prop. + code: 'Submit', + settings: { + github: { + components: { + Link: 'a', + }, + }, + }, + }, + ], + invalid: [ + {code: 'GitHub', errors: [{message: errorMessage}]}, + {code: '', errors: [{message: errorMessage}]}, + { + code: 'Submit', + errors: [{message: errorMessage}], + settings: { + github: { + components: { + Component: 'iframe', + }, + }, + }, + }, + ], +})