diff --git a/README.md b/README.md index 4da7ca9f..4dac7539 100644 --- a/README.md +++ b/README.md @@ -47,15 +47,16 @@ _Note: This is experimental and subject to change._ The `react` config includes rules which target specific HTML elements. You may provide a mapping of custom components to an HTML element in your `eslintrc` configuration to increase linter coverage. -For each component, you may specify a `default` and/or `props`. `default` may make sense if there's a 1:1 mapping between a component and an HTML element. However, if the HTML output of a component is dependent on a prop value, you can provide a mapping using the `props` key. To minimize conflicts and complexity, this currently only supports the mapping of a single prop type. +By default, these eslint rules will check the "as" prop for underlying element changes. If your repo uses a different prop name for polymorphic components provide the prop name in your `eslintrc` configuration under `polymorphicPropName`. ```json { "settings": { "github": { + "polymorphicPropName": "asChild", "components": { - "Box": {"default": "p"}, - "Link": {"props": {"as": {"undefined": "a", "a": "a", "button": "button"}}} + "Box": "p", + "Link": "a" } } } @@ -66,9 +67,7 @@ This config will be interpreted in the following way: - All `` elements will be treated as a `p` element type. - `` without a defined `as` prop will be treated as a `a`. -- `` will treated as an `a` element type. - `` will be treated as a `button` element type. -- `` will be treated as the raw `Link` type because there is no configuration set for `as='summary'`. ### Rules @@ -82,28 +81,31 @@ This config will be interpreted in the following way: πŸ”§ Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\ ❌ Deprecated. -| NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β  | Description | πŸ’Ό | πŸ”§ | ❌ | -| :----------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------- | :- | :- | :- | -| [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 | | | ❌ | +| NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β  | Description | πŸ’Ό | πŸ”§ | ❌ | +| :------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------- | :- | :- | :- | +| [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-role-supports-aria-props](docs/rules/a11y-role-supports-aria-props.md) | Enforce that elements with explicit or implicit roles defined contain only `aria-*` properties supported by that `role`. | βš›οΈ | | | -| [array-foreach](docs/rules/array-foreach.md) | enforce `for..of` loops over `Array.forEach` | βœ… | | | -| [async-currenttarget](docs/rules/async-currenttarget.md) | disallow `event.currentTarget` calls inside of async functions | πŸ” | | | -| [async-preventdefault](docs/rules/async-preventdefault.md) | disallow `event.preventDefault` calls inside of async functions | πŸ” | | | -| [authenticity-token](docs/rules/authenticity-token.md) | disallow usage of CSRF tokens in JavaScript | πŸ” | | | -| [get-attribute](docs/rules/get-attribute.md) | disallow wrong usage of attribute names | πŸ” | πŸ”§ | | -| [js-class-name](docs/rules/js-class-name.md) | enforce a naming convention for js- prefixed classes | πŸ” | | | -| [no-blur](docs/rules/no-blur.md) | disallow usage of `Element.prototype.blur()` | πŸ” | | | -| [no-d-none](docs/rules/no-d-none.md) | disallow usage the `d-none` CSS class | πŸ” | | | -| [no-dataset](docs/rules/no-dataset.md) | enforce usage of `Element.prototype.getAttribute` instead of `Element.prototype.datalist` | πŸ” | | | -| [no-dynamic-script-tag](docs/rules/no-dynamic-script-tag.md) | disallow creating dynamic script tags | βœ… | | | -| [no-implicit-buggy-globals](docs/rules/no-implicit-buggy-globals.md) | disallow implicit global variables | βœ… | | | -| [no-inner-html](docs/rules/no-inner-html.md) | disallow `Element.prototype.innerHTML` in favor of `Element.prototype.textContent` | πŸ” | | | -| [no-innerText](docs/rules/no-innerText.md) | disallow `Element.prototype.innerText` in favor of `Element.prototype.textContent` | πŸ” | πŸ”§ | | -| [no-then](docs/rules/no-then.md) | enforce using `async/await` syntax over Promises | βœ… | | | -| [no-useless-passive](docs/rules/no-useless-passive.md) | disallow marking a event handler as passive when it has no effect | πŸ” | πŸ”§ | | -| [prefer-observers](docs/rules/prefer-observers.md) | disallow poorly performing event listeners | πŸ” | | | -| [require-passive-events](docs/rules/require-passive-events.md) | enforce marking high frequency event handlers as passive | πŸ” | | | -| [unescaped-html-literal](docs/rules/unescaped-html-literal.md) | disallow unescaped HTML literals | πŸ” | | | +| [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` | βœ… | | | +| [async-currenttarget](docs/rules/async-currenttarget.md) | disallow `event.currentTarget` calls inside of async functions | πŸ” | | | +| [async-preventdefault](docs/rules/async-preventdefault.md) | disallow `event.preventDefault` calls inside of async functions | πŸ” | | | +| [authenticity-token](docs/rules/authenticity-token.md) | disallow usage of CSRF tokens in JavaScript | πŸ” | | | +| [get-attribute](docs/rules/get-attribute.md) | disallow wrong usage of attribute names | πŸ” | πŸ”§ | | +| [js-class-name](docs/rules/js-class-name.md) | enforce a naming convention for js- prefixed classes | πŸ” | | | +| [no-blur](docs/rules/no-blur.md) | disallow usage of `Element.prototype.blur()` | πŸ” | | | +| [no-d-none](docs/rules/no-d-none.md) | disallow usage the `d-none` CSS class | πŸ” | | | +| [no-dataset](docs/rules/no-dataset.md) | enforce usage of `Element.prototype.getAttribute` instead of `Element.prototype.datalist` | πŸ” | | | +| [no-dynamic-script-tag](docs/rules/no-dynamic-script-tag.md) | disallow creating dynamic script tags | βœ… | | | +| [no-implicit-buggy-globals](docs/rules/no-implicit-buggy-globals.md) | disallow implicit global variables | βœ… | | | +| [no-inner-html](docs/rules/no-inner-html.md) | disallow `Element.prototype.innerHTML` in favor of `Element.prototype.textContent` | πŸ” | | | +| [no-innerText](docs/rules/no-innerText.md) | disallow `Element.prototype.innerText` in favor of `Element.prototype.textContent` | πŸ” | πŸ”§ | | +| [no-then](docs/rules/no-then.md) | enforce using `async/await` syntax over Promises | βœ… | | | +| [no-useless-passive](docs/rules/no-useless-passive.md) | disallow marking a event handler as passive when it has no effect | πŸ” | πŸ”§ | | +| [prefer-observers](docs/rules/prefer-observers.md) | disallow poorly performing event listeners | πŸ” | | | +| [require-passive-events](docs/rules/require-passive-events.md) | enforce marking high frequency event handlers as passive | πŸ” | | | +| [unescaped-html-literal](docs/rules/unescaped-html-literal.md) | disallow unescaped HTML literals | πŸ” | | | 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/docs/rules/a11y-no-visually-hidden-interactive-element.md b/docs/rules/a11y-no-visually-hidden-interactive-element.md new file mode 100644 index 00000000..24af1e8b --- /dev/null +++ b/docs/rules/a11y-no-visually-hidden-interactive-element.md @@ -0,0 +1,79 @@ +# Ensures that interactive elements are not visually hidden (`github/a11y-no-visually-hidden-interactive-element`) + +πŸ’Ό This rule is enabled in the βš›οΈ `react` config. + + + +## Rule Details + +This rule guards against visually hiding interactive elements. If a sighted keyboard user navigates to an interactive element that is visually hidden they might become confused and assume that keyboard focus has been lost. + +Note: we are not guarding against visually hidden `input` elements at this time. Some visually hidden inputs might cause a false positive (e.g. some file inputs). + +### Why do we visually hide content? + +Visually hiding content can be useful when you want to provide information specifically to screen reader users or other assitive technology users while keeping content hidden from sighted users. + +Applying the following css will visually hide content while still making it accessible to screen reader users. + +```css +clip-path: inset(50%); +height: 1px; +overflow: hidden; +position: absolute; +white-space: nowrap; +width: 1px; +``` + +πŸ‘Ž Examples of **incorrect** code for this rule: + +```jsx + +``` + +```jsx + + + +``` + +```jsx +Submit +``` + +πŸ‘ Examples of **correct** code for this rule: + +```jsx +

Welcome to GitHub

+``` + +```jsx + +

Welcome to GitHub

+
+``` + +```jsx +Welcome to GitHub +``` + +## Options + +- className - A css className that visually hides content. Defaults to `sr-only`. +- componentName - A react component name that visually hides content. Defaults to `VisuallyHidden`. +- htmlPropName - A prop name used to replace the semantic element that is rendered. Defaults to `as`. + +```json +{ + "a11y-no-visually-hidden-interactive-element": [ + "error", + { + "className": "visually-hidden", + "componentName": "VisuallyHidden", + "htmlPropName": "as" + } + ] +} +``` + +## Version diff --git a/docs/rules/a11y-svg-has-accessible-name.md b/docs/rules/a11y-svg-has-accessible-name.md new file mode 100644 index 00000000..088e918b --- /dev/null +++ b/docs/rules/a11y-svg-has-accessible-name.md @@ -0,0 +1,73 @@ +# SVGs must have an accessible name (`github/a11y-svg-has-accessible-name`) + +πŸ’Ό This rule is enabled in the βš›οΈ `react` config. + + + +## Rule Details + +An `` must have an accessible name. Set `aria-label` or `aria-labelledby`, or nest a `` element as the first child of the `` element. + +However, if the `` is purely decorative, hide it with `aria-hidden="true"` or `role="presentation"`. + +## Resources + +- [Accessible SVGs](https://css-tricks.com/accessible-svgs/) + +## Examples + +### **Incorrect** code for this rule πŸ‘Ž + +```html + + + +``` + +```html + + + +``` + +```html + + + Circle with a black outline and red fill + +``` + +### **Correct** code for this rule πŸ‘ + +```html + + Circle with a black outline and red fill + + +``` + +```html + + + +``` + +```html + + + +``` + +```html + +``` + +```html + + + +``` + +## Version diff --git a/lib/configs/react.js b/lib/configs/react.js index 523e8805..4b2645db 100644 --- a/lib/configs/react.js +++ b/lib/configs/react.js @@ -8,8 +8,11 @@ module.exports = { plugins: ['github', 'jsx-a11y'], extends: ['plugin:jsx-a11y/recommended'], rules: { - '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 + 'jsx-a11y/role-supports-aria-props': 'off', // Override with github/a11y-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/a11y-role-supports-aria-props': 'error', 'jsx-a11y/no-aria-hidden-on-focusable': 'error', 'jsx-a11y/no-autofocus': 'off', diff --git a/lib/index.js b/lib/index.js index a959594c..68dca43a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,8 +1,11 @@ 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-role-supports-aria-props': require('./rules/a11y-role-supports-aria-props'), + 'a11y-svg-has-accessible-name': require('./rules/a11y-svg-has-accessible-name'), 'array-foreach': require('./rules/array-foreach'), 'async-currenttarget': require('./rules/async-currenttarget'), 'async-preventdefault': require('./rules/async-preventdefault'), 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 `