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 (
-
+
{svg}
);
@@ -100,7 +103,7 @@ const InternalIcon = ({
if (url) {
return (
-
+
);
}
@@ -131,7 +134,7 @@ const InternalIcon = ({
}
return (
-
+
{validIcon ? iconMap(name) : undefined}
);
diff --git a/src/table/__tests__/header-cell.test.tsx b/src/table/__tests__/header-cell.test.tsx
index 8056b17384..fe80872a06 100644
--- a/src/table/__tests__/header-cell.test.tsx
+++ b/src/table/__tests__/header-cell.test.tsx
@@ -119,7 +119,10 @@ describe('i18n', () => {
);
- expect(container.querySelector(`.${styles['edit-icon']}`)).toHaveAttribute('aria-label', 'Custom editable');
+ expect(container.querySelector(`.${styles['edit-icon']} [role=img]`)).toHaveAttribute(
+ 'aria-label',
+ 'Custom editable'
+ );
});
test('does not set tab index when negative', () => {
diff --git a/src/table/body-cell/index.tsx b/src/table/body-cell/index.tsx
index d45f2c3fdc..2c8d2f17db 100644
--- a/src/table/body-cell/index.tsx
+++ b/src/table/body-cell/index.tsx
@@ -104,15 +104,13 @@ function TableCellEditable({
<>
{
// Prevent the editor's Button blur event to be fired when clicking the success icon.
// This prevents unfocusing the button and triggers the `TableTdElement` onClick event which initiates the edit mode.
e.preventDefault();
}}
>
-
+
{i18n('ariaLabels.successfulEditLabel', ariaLabels?.successfulEditLabel?.(column))}
diff --git a/src/table/header-cell/index.tsx b/src/table/header-cell/index.tsx
index aaef4e9f92..9223a9800b 100644
--- a/src/table/header-cell/index.tsx
+++ b/src/table/header-cell/index.tsx
@@ -182,12 +182,11 @@ export function TableHeaderCell({
>
{column.header}
{isEditable ? (
-
-
+
+
) : null}