From 4a66db610011c76c47f3f99ddacf4a3e0e3abb56 Mon Sep 17 00:00:00 2001 From: Avinash Dwarapu Date: Tue, 4 Feb 2025 16:05:18 +0100 Subject: [PATCH 1/2] feat: Add ariaLabel property to icon component --- .../__snapshots__/documenter.test.ts.snap | 9 ++++- src/icon/__tests__/icon.test.tsx | 36 +++++++++++++++++++ src/icon/interfaces.ts | 11 +++++- src/icon/internal.tsx | 9 +++-- 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index fcda60b2aa..670b12fa10 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -8890,12 +8890,19 @@ exports[`Documenter definition for icon matches the snapshot: icon 1`] = ` "name": "Icon", "properties": [ { - "description": "Specifies alternate text for a custom icon (using the \`url\` attribute). We recommend that you provide this for accessibility. + "deprecatedTag": "Use \`ariaLabel\` instead.", + "description": "Specifies alternate text for a custom icon (using the \`url\` attribute). This property is ignored if you use a predefined icon or if you set your custom icon using the \`svg\` slot.", "name": "alt", "optional": true, "type": "string", }, + { + "description": "Specifies alternate text for the icon. We recommend that you provide this for accessibility.", + "name": "ariaLabel", + "optional": true, + "type": "string", + }, { "deprecatedTag": "Custom CSS is not supported. For testing and other use cases, use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes).", "description": "Adds the specified classes to the root element of the component.", diff --git a/src/icon/__tests__/icon.test.tsx b/src/icon/__tests__/icon.test.tsx index 91be20ae9d..07842c4623 100644 --- a/src/icon/__tests__/icon.test.tsx +++ b/src/icon/__tests__/icon.test.tsx @@ -85,6 +85,28 @@ describe('Icon Component', () => { expect(img).toHaveAttribute('alt', 'custom icon'); }); + test('should render a custom icon with alternate text when a url and ariaLabel are provided', () => { + const { container } = render(); + const wrapper = createWrapper(container); + expect(wrapper.findIcon()!.getElement()).not.toHaveAttribute('aria-label'); + expect(container.querySelector('img')).toHaveAttribute('alt', 'custom icon'); + }); + + test('should prefer ariaLabel when alt is also provided', () => { + const { container } = render(); + const wrapper = createWrapper(container); + expect(wrapper.findIcon()!.getElement()).not.toHaveAttribute('aria-label'); + expect(container.querySelector('img')).toHaveAttribute('alt', 'custom icon'); + }); + + test('should not set aria-hidden="true" for custom svg icons if an ariaLabel is provided', () => { + const { container } = render(); + const wrapper = createWrapper(container); + expect(wrapper.findIcon()!.getElement()).not.toHaveAttribute('aria-hidden', 'true'); + expect(wrapper.findIcon()!.getElement()).toHaveAttribute('role', 'img'); + expect(wrapper.findIcon()!.getElement()).toHaveAttribute('aria-label', 'custom icon'); + }); + test('should render a custom icon when both name and url are provided', () => { const { container } = render(); const img = container.querySelector('img'); @@ -126,6 +148,20 @@ describe('Icon Component', () => { expect(container.firstElementChild).toBeEmptyDOMElement(); }); + test('sets role="img" and the label on the wrapper element when provided', () => { + const { container } = render(); + const wrapper = createWrapper(container); + expect(wrapper.findIcon()!.getElement()).toHaveAttribute('role', 'img'); + expect(wrapper.findIcon()!.getElement()).toHaveAttribute('aria-label', 'Calendar'); + }); + + test('sets role="img" and the label on the wrapper element even when ariaLabel is an empty string', () => { + const { container } = render(); + const wrapper = createWrapper(container); + expect(wrapper.findIcon()!.getElement()).toHaveAttribute('role', 'img'); + expect(wrapper.findIcon()!.getElement()).toHaveAttribute('aria-label', ''); + }); + describe('Prototype Pollution attack', () => { beforeEach(() => { (Object.prototype as any).attack = 'vulnerable'; diff --git a/src/icon/interfaces.ts b/src/icon/interfaces.ts index 545b94f6c0..5230087db5 100644 --- a/src/icon/interfaces.ts +++ b/src/icon/interfaces.ts @@ -9,6 +9,7 @@ export interface IconProps extends BaseComponentProps { * Specifies the icon to be displayed. */ name?: IconProps.Name; + /** * Specifies the size of the icon. * @@ -32,12 +33,20 @@ export interface IconProps extends BaseComponentProps { * If you set both `url` and `svg`, `svg` will take precedence. */ url?: string; + /** - * Specifies alternate text for a custom icon (using the `url` attribute). We recommend that you provide this for accessibility. + * Specifies alternate text for a custom icon (using the `url` attribute). * This property is ignored if you use a predefined icon or if you set your custom icon using the `svg` slot. + * + * @deprecated Use `ariaLabel` instead. */ alt?: string; + /** + * Specifies alternate text for the icon. We recommend that you provide this for accessibility. + */ + ariaLabel?: string; + /** * Specifies the SVG of a custom icon. * diff --git a/src/icon/internal.tsx b/src/icon/internal.tsx index 63984e027d..b73c2e5f57 100644 --- a/src/icon/internal.tsx +++ b/src/icon/internal.tsx @@ -44,6 +44,7 @@ const InternalIcon = ({ variant = 'normal', url, alt, + ariaLabel, svg, badge, __internalRootRef = null, @@ -82,6 +83,8 @@ const InternalIcon = ({ }); const mergedRef = useMergeRefs(iconRef, __internalRootRef); + const hasAriaLabel = typeof ariaLabel === 'string'; + const labelAttributes = hasAriaLabel ? { role: 'img', 'aria-label': ariaLabel } : {}; if (svg) { if (url) { @@ -91,7 +94,7 @@ const InternalIcon = ({ ); } return ( -