diff --git a/.changeset/nine-mirrors-float.md b/.changeset/nine-mirrors-float.md new file mode 100644 index 00000000..952d2057 --- /dev/null +++ b/.changeset/nine-mirrors-float.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-primer-react': major +--- + +Add `a11y-no-title-usage` that warns against using `title` in some components diff --git a/docs/rules/a11y-no-title-usage.md b/docs/rules/a11y-no-title-usage.md new file mode 100644 index 00000000..0339b07c --- /dev/null +++ b/docs/rules/a11y-no-title-usage.md @@ -0,0 +1,40 @@ +## Rule Details + +This rule aims to prevent the use of the `title` attribute with some components from `@primer/react`. The `title` attribute is not keyboard accessible, which results in accessibility issues. Instead, we should utilize alternatives that are accessible. + +👎 Examples of **incorrect** code for this rule + +```jsx +import {RelativeTime} from '@primer/react' + +const App = () => +``` + +👍 Examples of **correct** code for this rule: + +```jsx +import {RelativeTime} from '@primer/react' + +const App = () => +``` + +The noTitle attribute is true by default, so it can be omitted. + +## With alternative tooltip + +If you want to still utilize a tooltip in a similar way to how the `title` attribute works, you can use the [Primer `Tooltip`](https://primer.style/components/tooltip/react/beta). If you use the `Tooltip` component, you must use it with an interactive element, such as with a button or a link. + +```jsx +import {RelativeTime, Tooltip} from '@primer/react' + +const App = () => { + const date = new Date('2020-01-01T00:00:00Z') + return ( + + + + + + ) +} +``` diff --git a/src/configs/recommended.js b/src/configs/recommended.js index 2b247df4..e29c3c00 100644 --- a/src/configs/recommended.js +++ b/src/configs/recommended.js @@ -15,6 +15,7 @@ module.exports = { 'primer-react/a11y-tooltip-interactive-trigger': 'error', 'primer-react/new-color-css-vars': 'error', 'primer-react/a11y-explicit-heading': 'error', + 'primer-react/a11y-no-title-usage': 'error', 'primer-react/no-deprecated-props': 'warn', 'primer-react/a11y-remove-disable-tooltip': 'error', 'primer-react/a11y-use-accessible-tooltip': 'error', diff --git a/src/index.js b/src/index.js index 2acf7c88..9ba22767 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ module.exports = { 'a11y-link-in-text-block': require('./rules/a11y-link-in-text-block'), 'a11y-remove-disable-tooltip': require('./rules/a11y-remove-disable-tooltip'), 'a11y-use-accessible-tooltip': require('./rules/a11y-use-accessible-tooltip'), + 'a11y-no-title-usage': require('./rules/a11y-no-title-usage'), 'use-deprecated-from-deprecated': require('./rules/use-deprecated-from-deprecated'), 'no-wildcard-imports': require('./rules/no-wildcard-imports'), 'no-unnecessary-components': require('./rules/no-unnecessary-components'), diff --git a/src/rules/__tests__/a11y-no-title-usage.test.js b/src/rules/__tests__/a11y-no-title-usage.test.js new file mode 100644 index 00000000..04b418bc --- /dev/null +++ b/src/rules/__tests__/a11y-no-title-usage.test.js @@ -0,0 +1,27 @@ +const rule = require('../a11y-no-title-usage') +const {RuleTester} = require('eslint') + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, +}) + +ruleTester.run('a11y-no-title-usage', rule, { + valid: [ + ``, + ``, + ``, + ], + invalid: [ + { + code: ``, + output: ``, + errors: [{messageId: 'noTitleOnRelativeTime'}], + }, + ], +}) diff --git a/src/rules/a11y-no-title-usage.js b/src/rules/a11y-no-title-usage.js new file mode 100644 index 00000000..53ef25f8 --- /dev/null +++ b/src/rules/a11y-no-title-usage.js @@ -0,0 +1,37 @@ +const url = require('../url') +const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute') + +module.exports = { + meta: { + type: 'error', + docs: { + description: 'Disallow usage of title attribute on some components', + recommended: true, + url: url(module), + }, + messages: { + noTitleOnRelativeTime: 'Avoid using the title attribute on RelativeTime.', + }, + fixable: 'code', + }, + + create(context) { + return { + JSXOpeningElement(jsxNode) { + const title = getJSXOpeningElementAttribute(jsxNode, 'noTitle') + + if (title && title.value && title.value.expression && title.value.expression.value !== true) { + context.report({ + node: title, + messageId: 'noTitleOnRelativeTime', + fix(fixer) { + const start = title.range[0] - 1 + const end = title.range[1] + return fixer.removeRange([start, end]) + }, + }) + } + }, + } + }, +}