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/alert/__tests__/alert.test.tsx b/src/alert/__tests__/alert.test.tsx index 6762b284a7..ff89b3b9d8 100644 --- a/src/alert/__tests__/alert.test.tsx +++ b/src/alert/__tests__/alert.test.tsx @@ -85,7 +85,7 @@ describe('Alert Component', () => { }); it('status icon does not have a label by default', () => { const { wrapper } = renderAlert({}); - expect(wrapper.find('[role="img"]')!.getElement()).not.toHaveAttribute('aria-label'); + expect(wrapper.find('[role="img"]')).toBeNull(); }); it('status icon can have a label', () => { const { wrapper } = renderAlert({ i18nStrings }); @@ -215,7 +215,7 @@ describe('Alert Component', () => { ); const wrapper = createWrapper(container)!.findAlert()!; - const statusIcon = wrapper.findByClassName(styles.icon)!.getElement(); + const statusIcon = wrapper.findByClassName(styles.icon)!.findIcon()!.getElement(); const dismissButton = wrapper.findDismissButton()!.getElement(); return { statusIcon, dismissButton }; } diff --git a/src/alert/internal.tsx b/src/alert/internal.tsx index bd60f29f26..4a62ddadbf 100644 --- a/src/alert/internal.tsx +++ b/src/alert/internal.tsx @@ -132,8 +132,8 @@ const InternalAlert = React.forwardRef( )} >
-
- +
+
  - - - + )} diff --git a/src/flashbar/collapsible-flashbar.tsx b/src/flashbar/collapsible-flashbar.tsx index 68dc1a18b4..59e0d075a3 100644 --- a/src/flashbar/collapsible-flashbar.tsx +++ b/src/flashbar/collapsible-flashbar.tsx @@ -372,10 +372,8 @@ const NotificationTypeCount = ({ }) => { return ( - - + + {count} diff --git a/src/flashbar/flash.tsx b/src/flashbar/flash.tsx index f84d20fa26..813c9692b9 100644 --- a/src/flashbar/flash.tsx +++ b/src/flashbar/flash.tsx @@ -134,9 +134,18 @@ export const Flash = React.forwardRef( const headerRef = useMergeRefs(headerRefAction, headerRefContent, headerRefObject); const contentRef = useMergeRefs(contentRefAction, contentRefContent, contentRefObject); - const iconType = ICON_TYPES[type]; + const statusIconAriaLabel = + props.statusIconAriaLabel || + i18nStrings?.[`${loading || type === 'in-progress' ? 'inProgress' : type}IconAriaLabel`]; - const icon = loading ? : ; + const iconType = ICON_TYPES[type]; + const icon = loading ? ( + + + + ) : ( + + ); const effectiveType = loading ? 'info' : type; @@ -144,10 +153,6 @@ export const Flash = React.forwardRef( [DATA_ATTR_ANALYTICS_FLASHBAR]: effectiveType, }; - const statusIconAriaLabel = - props.statusIconAriaLabel || - i18nStrings?.[`${loading || type === 'in-progress' ? 'inProgress' : type}IconAriaLabel`]; - return ( // We're not using "polite" or "assertive" here, just turning default behavior off. // eslint-disable-next-line @cloudscape-design/prefer-live-region @@ -175,13 +180,7 @@ export const Flash = React.forwardRef( >
-
- {icon} -
+
{icon}
-
- +
+
@@ -75,8 +75,8 @@ export function FormFieldWarning({ id, children, warningIconAriaLabel }: FormFie <>
-
- +
+
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 ( -