uikit-react-public 0.30.0 → 0.32.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/components/Dialog/BaseDialog.d.ts +1 -1
  2. package/dist/components/Dialog/Dialog.d.ts +1 -1
  3. package/dist/components/Dialog/Dialog.stories.d.ts +1 -1
  4. package/dist/components/Heading/Heading.stories.d.ts +5 -7
  5. package/dist/components/MenuNew/Menu.context.d.ts +9 -0
  6. package/dist/components/MenuNew/Menu.d.ts +2 -1
  7. package/dist/components/MenuNew/MenuHeading.d.ts +1 -1
  8. package/dist/components/MenuNew/MenuItemText.d.ts +2 -1
  9. package/dist/components/MenuNew/PrimaryMenuItem.d.ts +1 -1
  10. package/dist/components/Select/Select.types.d.ts +1 -1
  11. package/dist/components/Sidebar/Sidebar.d.ts +12 -0
  12. package/dist/components/Sidebar/Sidebar.stories.d.ts +38 -0
  13. package/dist/components/Sidebar/__tests__/Sidebar.test.d.ts +1 -0
  14. package/dist/components/Sidebar/index.d.ts +2 -0
  15. package/dist/components/Snackbar/Snackbar.d.ts +9 -5
  16. package/dist/components/Snackbar/Snackbar.stories.d.ts +2 -1
  17. package/dist/components/Snackbar/Snackbars.d.ts +19 -0
  18. package/dist/components/Snackbar/Snackbars.stories.d.ts +52 -0
  19. package/dist/components/Snackbar/__tests__/Snackbars.test.d.ts +1 -0
  20. package/dist/components/Snackbar/index.d.ts +2 -0
  21. package/dist/components/index.d.ts +4 -1
  22. package/dist/index.js +5897 -5358
  23. package/dist/utils/announce.d.ts +2 -1
  24. package/lib/components/Dialog/BaseDialog.tsx +180 -161
  25. package/lib/components/Dialog/Dialog.tsx +15 -11
  26. package/lib/components/Dropdown/Dropdown.stories.tsx +69 -74
  27. package/lib/components/Heading/Documentation.mdx +4 -14
  28. package/lib/components/Heading/Heading.stories.tsx +16 -30
  29. package/lib/components/MenuNew/Menu.context.tsx +18 -0
  30. package/lib/components/MenuNew/Menu.tsx +14 -9
  31. package/lib/components/MenuNew/MenuDivider.tsx +12 -1
  32. package/lib/components/MenuNew/MenuHeading.tsx +6 -0
  33. package/lib/components/MenuNew/MenuItemText.tsx +27 -3
  34. package/lib/components/MenuNew/MenuSection.tsx +11 -0
  35. package/lib/components/MenuNew/PrimaryMenuItem.tsx +71 -2
  36. package/lib/components/Select/Select.types.ts +1 -7
  37. package/lib/components/Select/__tests__/Select.test.tsx +112 -0
  38. package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +5 -0
  39. package/lib/components/Select/subcomponents/CustomSelect.tsx +75 -3
  40. package/lib/components/Select/subcomponents/FilterInput.tsx +1 -1
  41. package/lib/components/Sidebar/Sidebar.stories.tsx +94 -0
  42. package/lib/components/Sidebar/Sidebar.tsx +208 -0
  43. package/lib/components/Sidebar/__tests__/Sidebar.test.tsx +96 -0
  44. package/lib/components/Sidebar/__tests__/__snapshots__/Sidebar.test.tsx.snap +272 -0
  45. package/lib/components/Sidebar/index.ts +2 -0
  46. package/lib/components/Snackbar/Snackbar.stories.tsx +11 -0
  47. package/lib/components/Snackbar/Snackbar.tsx +162 -118
  48. package/lib/components/Snackbar/Snackbars.stories.tsx +130 -0
  49. package/lib/components/Snackbar/Snackbars.tsx +377 -0
  50. package/lib/components/Snackbar/__tests__/Snackbar.test.tsx +104 -0
  51. package/lib/components/Snackbar/__tests__/Snackbars.test.tsx +377 -0
  52. package/lib/components/Snackbar/__tests__/__snapshots__/Snackbar.test.tsx.snap +36 -19
  53. package/lib/components/Snackbar/index.ts +6 -0
  54. package/lib/components/index.ts +10 -1
  55. package/lib/utils/__tests__/announce.test.ts +53 -0
  56. package/lib/utils/announce.ts +33 -10
  57. package/package.json +1 -1
@@ -0,0 +1,130 @@
1
+ import { useRef, useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { css } from '@emotion/css';
4
+ import Button from '../Button/Button';
5
+ import Snackbars, { type SnackbarsItem } from './Snackbars';
6
+
7
+ const meta = {
8
+ title: 'Components/Snackbars',
9
+ component: Snackbars,
10
+ parameters: {
11
+ layout: 'fullscreen',
12
+ docs: {
13
+ description: {
14
+ component: 'Manages a stack of Snackbar notifications.',
15
+ },
16
+ },
17
+ },
18
+ argTypes: {
19
+ items: { control: false },
20
+ onDismiss: { control: false },
21
+ duration: { control: { type: 'number' } },
22
+ transitionDuration: { control: { type: 'number' } },
23
+ maxVisible: { control: { type: 'number' } },
24
+ placement: {
25
+ control: { type: 'select' },
26
+ options: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
27
+ },
28
+ },
29
+ args: {
30
+ duration: 0,
31
+ transitionDuration: 200,
32
+ maxVisible: 3,
33
+ placement: 'bottom-right',
34
+ },
35
+ tags: ['autodocs'],
36
+ } satisfies Meta<typeof Snackbars>;
37
+
38
+ export default meta;
39
+
40
+ type Story = StoryObj<typeof meta>;
41
+
42
+ const previewStyle = css`
43
+ min-height: 360px;
44
+ padding: 24px;
45
+ `;
46
+
47
+ export const Interactive: Story = {
48
+ args: {
49
+ duration: 5000,
50
+ items: [],
51
+ onDismiss: () => undefined,
52
+ },
53
+ render: (args) => {
54
+ const snackbarIdCounterRef = useRef(0);
55
+ const [items, setItems] = useState<SnackbarsItem[]>([
56
+ {
57
+ id: 'initial',
58
+ children: 'Use the buttons to add more snackbars',
59
+ duration: 0,
60
+ },
61
+ ]);
62
+
63
+ const controlsStyle = css`
64
+ display: flex;
65
+ flex-wrap: wrap;
66
+ gap: 12px;
67
+ padding: 24px;
68
+ `;
69
+
70
+ const getNextSnackbarId = () => {
71
+ snackbarIdCounterRef.current += 1;
72
+ return `storybook-snackbar-${snackbarIdCounterRef.current}`;
73
+ };
74
+
75
+ const addSnackbar = (item: Omit<SnackbarsItem, 'id'>) => {
76
+ setItems((current) => [
77
+ ...current,
78
+ {
79
+ ...item,
80
+ id: getNextSnackbarId(),
81
+ },
82
+ ]);
83
+ };
84
+
85
+ return (
86
+ <div className={previewStyle}>
87
+ <div className={controlsStyle}>
88
+ <Button
89
+ onClick={() =>
90
+ addSnackbar({
91
+ children: `Snackbar ${items.length + 1}`,
92
+ })
93
+ }
94
+ >
95
+ Add snackbar
96
+ </Button>
97
+ <Button
98
+ onClick={() =>
99
+ addSnackbar({
100
+ action: 'undo',
101
+ children: 'Snackbar with undo',
102
+ onUndo: () => alert('Undo action triggered'),
103
+ })
104
+ }
105
+ >
106
+ Add snackbar with undo
107
+ </Button>
108
+ <Button
109
+ onClick={() =>
110
+ addSnackbar({
111
+ children: 'Error snackbar',
112
+ size: 'small',
113
+ variant: 'error',
114
+ })
115
+ }
116
+ >
117
+ Add error snackbar
118
+ </Button>
119
+ </div>
120
+ <Snackbars
121
+ {...args}
122
+ items={items}
123
+ onDismiss={(id) =>
124
+ setItems((current) => current.filter((item) => item.id !== id))
125
+ }
126
+ />
127
+ </div>
128
+ );
129
+ },
130
+ };
@@ -0,0 +1,377 @@
1
+ import {
2
+ type HTMLAttributes,
3
+ memo,
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+ import { css, cx, keyframes } from '@emotion/css';
11
+ import useTheme from '../../theme/useTheme';
12
+ import Snackbar, { type SnackbarProps } from './Snackbar';
13
+
14
+ export const NAME = 'ucl-uikit-snackbars';
15
+
16
+ export type SnackbarsPlacement =
17
+ 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
18
+
19
+ const DEFAULT_AUTO_DISMISS_DURATION = 5000;
20
+
21
+ export interface SnackbarsItem extends SnackbarProps {
22
+ id: string;
23
+ duration?: number;
24
+ }
25
+
26
+ export interface SnackbarsProps extends Omit<
27
+ HTMLAttributes<HTMLDivElement>,
28
+ 'children'
29
+ > {
30
+ items: SnackbarsItem[];
31
+ onDismiss: (id: string) => void;
32
+ duration?: number;
33
+ transitionDuration?: number;
34
+ maxVisible?: number;
35
+ placement?: SnackbarsPlacement;
36
+ testId?: string;
37
+ }
38
+
39
+ const Snackbars = ({
40
+ items,
41
+ onDismiss,
42
+ duration = DEFAULT_AUTO_DISMISS_DURATION,
43
+ transitionDuration = 200,
44
+ maxVisible = 3,
45
+ placement = 'bottom-right',
46
+ testId = NAME,
47
+ className,
48
+ ...props
49
+ }: SnackbarsProps) => {
50
+ const [theme] = useTheme();
51
+ const [exitingIds, setExitingIds] = useState<Set<string>>(new Set());
52
+ const autoDismissTimersRef = useRef<Record<string, number>>({});
53
+ const autoDismissStartedAtRef = useRef<Record<string, number>>({});
54
+ const autoDismissRemainingRef = useRef<Record<string, number>>({});
55
+ const exitTimersRef = useRef<Record<string, number>>({});
56
+ const dismissRef = useRef(onDismiss);
57
+ const focusedIdsRef = useRef<Set<string>>(new Set());
58
+
59
+ useEffect(() => {
60
+ dismissRef.current = onDismiss;
61
+ }, [onDismiss]);
62
+
63
+ const visibleItems = useMemo(
64
+ () => items.slice(0, maxVisible),
65
+ [items, maxVisible]
66
+ );
67
+
68
+ const visibleItemIds = useMemo(
69
+ () => new Set(visibleItems.map((item) => item.id)),
70
+ [visibleItems]
71
+ );
72
+
73
+ const itemIds = useMemo(() => new Set(items.map((item) => item.id)), [items]);
74
+
75
+ const clearAutoDismissTimer = useCallback((id: string) => {
76
+ if (autoDismissTimersRef.current[id] === undefined) return;
77
+
78
+ window.clearTimeout(autoDismissTimersRef.current[id]);
79
+ delete autoDismissTimersRef.current[id];
80
+ delete autoDismissStartedAtRef.current[id];
81
+ }, []);
82
+
83
+ const beginDismiss = useCallback(
84
+ (id: string) => {
85
+ clearAutoDismissTimer(id);
86
+ delete autoDismissRemainingRef.current[id];
87
+ focusedIdsRef.current.delete(id);
88
+
89
+ setExitingIds((current) => {
90
+ if (current.has(id)) return current;
91
+
92
+ const next = new Set(current);
93
+ next.add(id);
94
+ return next;
95
+ });
96
+
97
+ if (exitTimersRef.current[id] !== undefined) return;
98
+
99
+ exitTimersRef.current[id] = window.setTimeout(() => {
100
+ delete exitTimersRef.current[id];
101
+ setExitingIds((current) => {
102
+ const next = new Set(current);
103
+ next.delete(id);
104
+ return next;
105
+ });
106
+ dismissRef.current(id);
107
+ }, transitionDuration);
108
+ },
109
+ [clearAutoDismissTimer, transitionDuration]
110
+ );
111
+
112
+ const startAutoDismissTimer = useCallback(
113
+ (id: string, timeoutDuration: number) => {
114
+ autoDismissStartedAtRef.current[id] = Date.now();
115
+ autoDismissTimersRef.current[id] = window.setTimeout(() => {
116
+ delete autoDismissTimersRef.current[id];
117
+ delete autoDismissStartedAtRef.current[id];
118
+ delete autoDismissRemainingRef.current[id];
119
+ beginDismiss(id);
120
+ }, timeoutDuration);
121
+ },
122
+ [beginDismiss]
123
+ );
124
+
125
+ const pauseAutoDismiss = useCallback(
126
+ (id: string) => {
127
+ if (autoDismissTimersRef.current[id] === undefined) return;
128
+
129
+ const startedAt = autoDismissStartedAtRef.current[id];
130
+ const remainingDuration = autoDismissRemainingRef.current[id];
131
+
132
+ if (startedAt !== undefined && remainingDuration !== undefined) {
133
+ autoDismissRemainingRef.current[id] = Math.max(
134
+ 0,
135
+ remainingDuration - (Date.now() - startedAt)
136
+ );
137
+ }
138
+
139
+ clearAutoDismissTimer(id);
140
+ },
141
+ [clearAutoDismissTimer]
142
+ );
143
+
144
+ const resumeAutoDismiss = useCallback(
145
+ (id: string) => {
146
+ if (focusedIdsRef.current.has(id)) return;
147
+ if (autoDismissTimersRef.current[id] !== undefined) return;
148
+
149
+ const remainingDuration = autoDismissRemainingRef.current[id];
150
+ if (remainingDuration === undefined) return;
151
+
152
+ if (remainingDuration <= 0) {
153
+ beginDismiss(id);
154
+ return;
155
+ }
156
+
157
+ startAutoDismissTimer(id, remainingDuration);
158
+ },
159
+ [beginDismiss, startAutoDismissTimer]
160
+ );
161
+
162
+ const getAutoDismissDuration = useCallback(
163
+ (item: SnackbarsItem) => {
164
+ const itemDuration = item.duration ?? duration;
165
+
166
+ if ((item.action ?? 'close') !== 'undo' || itemDuration > 0) {
167
+ return itemDuration;
168
+ }
169
+
170
+ return duration > 0 ? duration : DEFAULT_AUTO_DISMISS_DURATION;
171
+ },
172
+ [duration]
173
+ );
174
+
175
+ useEffect(() => {
176
+ visibleItems.forEach((item) => {
177
+ if (exitingIds.has(item.id)) return;
178
+ if (autoDismissTimersRef.current[item.id] !== undefined) return;
179
+
180
+ const itemDuration = getAutoDismissDuration(item);
181
+ if (itemDuration <= 0) return;
182
+
183
+ const remainingDuration =
184
+ autoDismissRemainingRef.current[item.id] ?? itemDuration;
185
+ autoDismissRemainingRef.current[item.id] = remainingDuration;
186
+
187
+ if (focusedIdsRef.current.has(item.id)) return;
188
+
189
+ startAutoDismissTimer(item.id, remainingDuration);
190
+ });
191
+ }, [
192
+ clearAutoDismissTimer,
193
+ exitingIds,
194
+ getAutoDismissDuration,
195
+ startAutoDismissTimer,
196
+ visibleItems,
197
+ ]);
198
+
199
+ useEffect(() => {
200
+ setExitingIds((current) => {
201
+ const next = new Set([...current].filter((id) => visibleItemIds.has(id)));
202
+ return next.size === current.size ? current : next;
203
+ });
204
+
205
+ Object.entries(exitTimersRef.current).forEach(([id, timerId]) => {
206
+ if (itemIds.has(id)) return;
207
+
208
+ window.clearTimeout(timerId);
209
+ delete exitTimersRef.current[id];
210
+ });
211
+
212
+ Object.entries(autoDismissTimersRef.current).forEach(([id, timerId]) => {
213
+ if (visibleItemIds.has(id)) return;
214
+
215
+ window.clearTimeout(timerId);
216
+ delete autoDismissTimersRef.current[id];
217
+ delete autoDismissStartedAtRef.current[id];
218
+ delete autoDismissRemainingRef.current[id];
219
+ focusedIdsRef.current.delete(id);
220
+ });
221
+
222
+ Object.keys(autoDismissRemainingRef.current).forEach((id) => {
223
+ if (visibleItemIds.has(id)) return;
224
+
225
+ delete autoDismissRemainingRef.current[id];
226
+ focusedIdsRef.current.delete(id);
227
+ });
228
+ }, [itemIds, visibleItemIds]);
229
+
230
+ useEffect(() => {
231
+ const autoDismissTimers = autoDismissTimersRef.current;
232
+ const exitTimers = exitTimersRef.current;
233
+
234
+ return () => {
235
+ Object.values(autoDismissTimers).forEach((timerId) =>
236
+ window.clearTimeout(timerId)
237
+ );
238
+ Object.values(exitTimers).forEach((timerId) =>
239
+ window.clearTimeout(timerId)
240
+ );
241
+ };
242
+ }, []);
243
+
244
+ const isBottomPlacement = placement.startsWith('bottom');
245
+ const isRightPlacement = placement.endsWith('right');
246
+ const stackedItems = isBottomPlacement
247
+ ? [...visibleItems].reverse()
248
+ : visibleItems;
249
+
250
+ const enterAnimation = keyframes`
251
+ from {
252
+ opacity: 0;
253
+ transform: translateY(
254
+ ${isBottomPlacement ? theme.margin.m8 : `-${theme.margin.m8}`}
255
+ );
256
+ }
257
+
258
+ to {
259
+ opacity: 1;
260
+ transform: translateY(0);
261
+ }
262
+ `;
263
+
264
+ const containerStyle = css`
265
+ position: fixed;
266
+ z-index: 9999;
267
+ display: flex;
268
+ flex-direction: column;
269
+ gap: ${theme.margin.m8};
270
+ pointer-events: none;
271
+ ${
272
+ isBottomPlacement
273
+ ? `bottom: ${theme.margin.m24};`
274
+ : `top: ${theme.margin.m24};`
275
+ }
276
+ right: ${theme.margin.m16};
277
+ left: ${theme.margin.m16};
278
+
279
+ @media (min-width: ${theme.breakpoints.tablet}px) {
280
+ ${
281
+ isRightPlacement
282
+ ? `right: ${theme.margin.m24}; left: auto;`
283
+ : `right: auto; left: ${theme.margin.m24};`
284
+ }
285
+ }
286
+ `;
287
+
288
+ const snackbarStyle = css`
289
+ position: static;
290
+ pointer-events: auto;
291
+ opacity: 1;
292
+ transform: translateY(0);
293
+ transition:
294
+ opacity ${transitionDuration}ms ease,
295
+ transform ${transitionDuration}ms ease;
296
+ animation: ${enterAnimation} ${transitionDuration}ms ease;
297
+
298
+ @media (prefers-reduced-motion: reduce) {
299
+ transition: none;
300
+ animation: none;
301
+ }
302
+ `;
303
+
304
+ const exitingStyle = css`
305
+ animation: none;
306
+ opacity: 0;
307
+ transform: translateY(
308
+ ${isBottomPlacement ? theme.margin.m8 : `-${theme.margin.m8}`}
309
+ );
310
+ `;
311
+
312
+ const style = cx(NAME, containerStyle, className);
313
+
314
+ return (
315
+ <div
316
+ className={style}
317
+ data-testid={testId}
318
+ {...props}
319
+ >
320
+ {stackedItems.map(({ id, ...item }) => {
321
+ const itemIsExiting = exitingIds.has(id);
322
+ delete item.duration;
323
+
324
+ return (
325
+ <Snackbar
326
+ key={id}
327
+ action={item.action ?? 'close'}
328
+ {...item}
329
+ actionDisabled={item.actionDisabled || itemIsExiting}
330
+ className={cx(
331
+ snackbarStyle,
332
+ itemIsExiting && exitingStyle,
333
+ item.className
334
+ )}
335
+ onClose={() => {
336
+ if (exitTimersRef.current[id] !== undefined) return;
337
+
338
+ item.onClose?.();
339
+ beginDismiss(id);
340
+ }}
341
+ onUndo={() => {
342
+ if (exitTimersRef.current[id] !== undefined) return;
343
+
344
+ item.onUndo?.();
345
+ beginDismiss(id);
346
+ }}
347
+ onFocus={(event) => {
348
+ item.onFocus?.(event);
349
+ focusedIdsRef.current.add(id);
350
+ pauseAutoDismiss(id);
351
+ }}
352
+ onBlur={(event) => {
353
+ item.onBlur?.(event);
354
+ const nextFocusedElement = event.relatedTarget;
355
+ if (
356
+ nextFocusedElement instanceof Node &&
357
+ event.currentTarget.contains(nextFocusedElement)
358
+ ) {
359
+ return;
360
+ }
361
+
362
+ focusedIdsRef.current.delete(id);
363
+ resumeAutoDismiss(id);
364
+ }}
365
+ size={item.size}
366
+ testId={item.testId ?? `${testId}-${id}`}
367
+ variant={item.variant}
368
+ >
369
+ {item.children}
370
+ </Snackbar>
371
+ );
372
+ })}
373
+ </div>
374
+ );
375
+ };
376
+
377
+ export default memo(Snackbars);
@@ -62,6 +62,78 @@ describe('Snackbar', () => {
62
62
  expect(customTestId).toBeDefined();
63
63
  });
64
64
 
65
+ test('passes ref to the root element', () => {
66
+ const ref = { current: null as HTMLDivElement | null };
67
+
68
+ render(
69
+ <ThemeContextProvider>
70
+ <Snackbar ref={ref}>Test snackbar</Snackbar>
71
+ </ThemeContextProvider>
72
+ );
73
+
74
+ expect(ref.current).toBe(screen.getByTestId('ucl-uikit-snackbar'));
75
+ });
76
+
77
+ test('Default variant uses polite status semantics', () => {
78
+ render(
79
+ <ThemeContextProvider>
80
+ <Snackbar action='undo'>Test snackbar</Snackbar>
81
+ </ThemeContextProvider>
82
+ );
83
+
84
+ const snackbar = screen.getByRole('status');
85
+ expect(snackbar.textContent).toBe('Test snackbar');
86
+ });
87
+
88
+ test('Error variant uses assertive alert semantics', () => {
89
+ render(
90
+ <ThemeContextProvider>
91
+ <Snackbar
92
+ action='close'
93
+ variant='error'
94
+ >
95
+ Test snackbar
96
+ </Snackbar>
97
+ </ThemeContextProvider>
98
+ );
99
+
100
+ const snackbar = screen.getByRole('alert');
101
+ expect(snackbar.textContent).toBe('Test snackbar');
102
+ });
103
+
104
+ test('Close button has a default accessible name', () => {
105
+ render(
106
+ <ThemeContextProvider>
107
+ <Snackbar action='close'>Test snackbar</Snackbar>
108
+ </ThemeContextProvider>
109
+ );
110
+
111
+ const closeButton = screen.getByRole('button', {
112
+ name: 'Dismiss notification',
113
+ });
114
+
115
+ expect(closeButton).toBeDefined();
116
+ });
117
+
118
+ test('Close button accessible name can be customised', () => {
119
+ render(
120
+ <ThemeContextProvider>
121
+ <Snackbar
122
+ action='close'
123
+ closeButtonAriaLabel='Dismiss saved notification'
124
+ >
125
+ Test snackbar
126
+ </Snackbar>
127
+ </ThemeContextProvider>
128
+ );
129
+
130
+ const closeButton = screen.getByRole('button', {
131
+ name: 'Dismiss saved notification',
132
+ });
133
+
134
+ expect(closeButton).toBeDefined();
135
+ });
136
+
65
137
  test('Close callback is called', () => {
66
138
  const onClose = vi.fn();
67
139
  render(
@@ -96,6 +168,38 @@ describe('Snackbar', () => {
96
168
  expect(onUndo).toHaveBeenCalled();
97
169
  });
98
170
 
171
+ test('Error variant renders warning icon', () => {
172
+ render(
173
+ <ThemeContextProvider>
174
+ <Snackbar
175
+ action='close'
176
+ variant='error'
177
+ >
178
+ Test snackbar
179
+ </Snackbar>
180
+ </ThemeContextProvider>
181
+ );
182
+
183
+ const [icon] = screen.getAllByTestId('ucl-uikit-icon');
184
+ const warningIconPath = icon.querySelector('path');
185
+
186
+ expect(warningIconPath?.getAttribute('d')).toContain('M10.29 3.86');
187
+ });
188
+
189
+ test('Decorative icons are hidden from assistive technology', () => {
190
+ render(
191
+ <ThemeContextProvider>
192
+ <Snackbar action='close'>Test snackbar</Snackbar>
193
+ </ThemeContextProvider>
194
+ );
195
+
196
+ const icons = screen.getAllByTestId('ucl-uikit-icon');
197
+
198
+ icons.forEach((icon) => {
199
+ expect(icon.getAttribute('aria-hidden')).toBe('true');
200
+ });
201
+ });
202
+
99
203
  test("Close callback cannot be called when action is 'undo'", () => {
100
204
  const onClose = vi.fn();
101
205
  render(