diff --git a/README.md b/README.md index 02f1ed5c0..229faccc1 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,7 @@ will be evaluated as an `h3`. If no `polymorphicPropName` is set, then the compo | [no-aria-hidden-on-focusable](docs/rules/no-aria-hidden-on-focusable.md) | Disallow `aria-hidden="true"` from being set on focusable elements. | | | | | [no-autofocus](docs/rules/no-autofocus.md) | Enforce autoFocus prop is not used. | ☑️ 🔒 | | | | [no-distracting-elements](docs/rules/no-distracting-elements.md) | Enforce distracting elements are not used. | ☑️ 🔒 | | | +| [no-duplicate-ids](docs/rules/no-duplicate-ids.md) | Disallow duplicate ids. | ☑️ 🔒 | | | | [no-interactive-element-to-noninteractive-role](docs/rules/no-interactive-element-to-noninteractive-role.md) | Interactive elements should not be assigned non-interactive roles. | ☑️ 🔒 | | | | [no-noninteractive-element-interactions](docs/rules/no-noninteractive-element-interactions.md) | Non-interactive elements should not be assigned mouse or keyboard event listeners. | ☑️ 🔒 | | | | [no-noninteractive-element-to-interactive-role](docs/rules/no-noninteractive-element-to-interactive-role.md) | Non-interactive elements should not be assigned interactive roles. | ☑️ 🔒 | | | diff --git a/__tests__/src/rules/no-duplicate-ids.js b/__tests__/src/rules/no-duplicate-ids.js new file mode 100644 index 000000000..71a0bc6c1 --- /dev/null +++ b/__tests__/src/rules/no-duplicate-ids.js @@ -0,0 +1,44 @@ +/** + * @fileoverview Disallow duplicate ids. + * @author Chris Ng + */ + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +import { RuleTester } from 'eslint'; +import parserOptionsMapper from '../../__util__/parserOptionsMapper'; +import parsers from '../../__util__/helpers/parsers'; +import rule from '../../../src/rules/no-duplicate-ids'; + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const ruleTester = new RuleTester(); + +const expectedError = (idValue) => ({ + message: `Duplicate ID "${idValue}" found. ID attribute values must be unique.`, + type: 'JSXOpeningElement', +}); + +const expectedJSXError = (idValue) => ({ + message: `Duplicate ID "${idValue}" found. ID attribute JSX experssions must be unique.`, + type: 'JSXOpeningElement', +}); + +ruleTester.run('no-duplicate-ids', rule, { + valid: parsers.all([].concat( + { code: '
' }, + { code: '
' }, + { code: '
' }, + { code: '
' }, + )).map(parserOptionsMapper), + invalid: parsers.all([].concat( + { code: '
', errors: [expectedError('chris')] }, + { code: '
', errors: [expectedJSXError('chris')] }, + { code: '
', errors: [expectedError('chris')] }, + { code: '
', errors: [expectedJSXError('chris')] }, + )).map(parserOptionsMapper), +}); diff --git a/docs/rules/no-duplicate-ids.md b/docs/rules/no-duplicate-ids.md new file mode 100644 index 000000000..50f016a80 --- /dev/null +++ b/docs/rules/no-duplicate-ids.md @@ -0,0 +1,32 @@ +# jsx-a11y/no-duplicate-ids + +💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`. + + + +Enforces that no `id` attributes are reused. This rule does a basic check to ensure that `id` attribute values are not the same. In the case of a JSX expression, it checks that no `id` attributes reuse the same expression. + +## Rule details + +This rule takes no arguments. + +### Succeed + +```jsx +
+
+ +
+``` + +### Fail + +```jsx +
+
+ +
+``` + +## Accessibility guidelines +- [WCAG 4.1.1](https://www.w3.org/WAI/WCAG21/Understanding/parsing.html) diff --git a/src/index.js b/src/index.js index 7b931fe34..0a6c856da 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,7 @@ module.exports = { 'no-aria-hidden-on-focusable': require('./rules/no-aria-hidden-on-focusable'), 'no-autofocus': require('./rules/no-autofocus'), 'no-distracting-elements': require('./rules/no-distracting-elements'), + 'no-duplicate-ids': require('./rules/no-duplicate-ids'), 'no-interactive-element-to-noninteractive-role': require('./rules/no-interactive-element-to-noninteractive-role'), 'no-noninteractive-element-interactions': require('./rules/no-noninteractive-element-interactions'), 'no-noninteractive-element-to-interactive-role': require('./rules/no-noninteractive-element-to-interactive-role'), @@ -116,6 +117,7 @@ module.exports = { 'jsx-a11y/no-access-key': 'error', 'jsx-a11y/no-autofocus': 'error', 'jsx-a11y/no-distracting-elements': 'error', + 'jsx-a11y/no-duplicate-ids': 'error', 'jsx-a11y/no-interactive-element-to-noninteractive-role': [ 'error', { @@ -273,6 +275,7 @@ module.exports = { 'jsx-a11y/no-access-key': 'error', 'jsx-a11y/no-autofocus': 'error', 'jsx-a11y/no-distracting-elements': 'error', + 'jsx-a11y/no-duplicate-ids': 'error', 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error', 'jsx-a11y/no-noninteractive-element-interactions': [ 'error', diff --git a/src/rules/no-duplicate-ids.js b/src/rules/no-duplicate-ids.js new file mode 100644 index 000000000..46ad3dc97 --- /dev/null +++ b/src/rules/no-duplicate-ids.js @@ -0,0 +1,55 @@ +/** + * @fileoverview Disallow duplicate ids. + * @author Chris Ng + */ + +// ---------------------------------------------------------------------------- +// Rule Definition +// ---------------------------------------------------------------------------- + +import { getProp, getPropValue } from 'jsx-ast-utils'; +import { generateObjSchema } from '../util/schemas'; + +const schema = generateObjSchema(); + +export default { + meta: { + docs: { + url: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/no-duplicate-ids.md', + description: 'Disallow duplicate ids.', + }, + schema: [schema], + }, + + create(context) { + const idsUsedSet = new Set(); + const jsxExperissionIDsUsedSet = new Set(); + + return { + JSXOpeningElement(node) { + const { attributes } = node; + const idProp = getProp(attributes, 'id'); + const idValue = getPropValue(idProp); + + // Special case if id is assigned using JSXExpressionContainer + if (idProp && idProp.type === 'JSXAttribute' && idProp.value.type === 'JSXExpressionContainer') { + if (jsxExperissionIDsUsedSet.has(idValue)) { + context.report({ + node, + message: `Duplicate ID "${idValue}" found. ID attribute JSX experssions must be unique.`, + }); + } else { + jsxExperissionIDsUsedSet.add(idValue); + } + } else if (idsUsedSet.has(idValue)) { + context.report({ + node, + message: `Duplicate ID "${idValue}" found. ID attribute values must be unique.`, + }); + } else { + idsUsedSet.add(idValue); + } + }, + }; + }, +};