diff --git a/packages/eslint-plugin/changelogs/upcoming/9366.md b/packages/eslint-plugin/changelogs/upcoming/9366.md new file mode 100644 index 000000000000..b16e19a625fe --- /dev/null +++ b/packages/eslint-plugin/changelogs/upcoming/9366.md @@ -0,0 +1,2 @@ +- Prevented `badge-accessibility-rules` rule autofix from duplicating `aria-hidden` attributes. +- Skip `badge-accessibility-rules` rule validation when a spread operator is used in a component. diff --git a/packages/eslint-plugin/src/rules/a11y/icon_accessibility_rules.test.ts b/packages/eslint-plugin/src/rules/a11y/icon_accessibility_rules.test.ts index e37b2c68c9e3..ee7771e2c9fa 100644 --- a/packages/eslint-plugin/src/rules/a11y/icon_accessibility_rules.test.ts +++ b/packages/eslint-plugin/src/rules/a11y/icon_accessibility_rules.test.ts @@ -46,6 +46,14 @@ ruleTester.run('EuiIconAccessibilityRules', EuiIconAccessibilityRules, { `, languageOptions, }, + { + code: dedent` + const MyComponent = () => ( + + ) + `, + languageOptions, + }, { code: dedent` const MyComponent = () => ( @@ -54,6 +62,15 @@ ruleTester.run('EuiIconAccessibilityRules', EuiIconAccessibilityRules, { `, languageOptions, }, + { + code: dedent` + const extraProps = {title: 'Search'}; + const MyComponent = () => ( + + ) + `, + languageOptions, + } ], invalid: [ // Missing accessible name -> autofix adds aria-hidden={true} diff --git a/packages/eslint-plugin/src/rules/a11y/icon_accessibility_rules.ts b/packages/eslint-plugin/src/rules/a11y/icon_accessibility_rules.ts index c8f2765c5d83..a3304190b0f2 100644 --- a/packages/eslint-plugin/src/rules/a11y/icon_accessibility_rules.ts +++ b/packages/eslint-plugin/src/rules/a11y/icon_accessibility_rules.ts @@ -1,5 +1,6 @@ import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import { removeAttribute } from '../../utils/remove_attr'; +import { hasSpread } from '../../utils/has_spread'; const COMPONENT = 'EuiIcon'; @@ -16,6 +17,12 @@ export const EuiIconAccessibilityRules = ESLintUtils.RuleCreator.withoutDocs({ return; } + // Skip fixing when spread props are present (e.g., ) + // because we cannot safely determine or modify aria-related attributes. + if (hasSpread(openingElement.attributes)) { + return; + } + let ariaHiddenAttr: TSESTree.JSXAttribute | undefined; let tabIndexAttr: TSESTree.JSXAttribute | undefined; let isIconNamed = false; @@ -30,36 +37,22 @@ export const EuiIconAccessibilityRules = ESLintUtils.RuleCreator.withoutDocs({ } } - const hasAriaHiddenTrue = - !!ariaHiddenAttr && - ariaHiddenAttr.value && - ( - // aria-hidden={true} - (ariaHiddenAttr.value.type === 'JSXExpressionContainer' && - ariaHiddenAttr.value.expression.type === 'Literal' && - ariaHiddenAttr.value.expression.value === true) || - // aria-hidden='true' - (ariaHiddenAttr.value.type === 'Literal' && - ariaHiddenAttr.value.value === 'true') - ); - // Case: `tabIndex` and `aria-hidden` cannot be used together - if (tabIndexAttr && hasAriaHiddenTrue) { + if (tabIndexAttr && ariaHiddenAttr) { context.report({ node: openingElement, messageId: 'tabIndexWithAriaHidden', fix: fixer => { if (!ariaHiddenAttr?.range) return null; const [start, end] = removeAttribute(context, ariaHiddenAttr); - return [fixer.removeRange([start, end])]; } }); return; } - // Require accessible name or `aria-hidden={true}`; - if (!isIconNamed && !hasAriaHiddenTrue) { + // Require accessible name or `aria-hidden`; if `aria-hidden` exists, do not insert a value + if (!isIconNamed && !ariaHiddenAttr) { context.report({ node: openingElement, messageId: 'missingTitleOrAriaHidden',