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',