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,377 @@
1
+ import { act, render, screen } from '@testing-library/react';
2
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
3
+ import { ThemeContextProvider } from '../../../theme/useTheme';
4
+ import Snackbars, { type SnackbarsItem } from '../Snackbars';
5
+
6
+ const renderSnackbars = (
7
+ items: SnackbarsItem[],
8
+ onDismiss = vi.fn(),
9
+ props: Partial<React.ComponentProps<typeof Snackbars>> = {}
10
+ ) =>
11
+ render(
12
+ <ThemeContextProvider>
13
+ <Snackbars
14
+ items={items}
15
+ onDismiss={onDismiss}
16
+ duration={0}
17
+ {...props}
18
+ />
19
+ </ThemeContextProvider>
20
+ );
21
+
22
+ describe('Snackbars', () => {
23
+ beforeEach(() => {
24
+ vi.useFakeTimers();
25
+ });
26
+
27
+ afterEach(() => {
28
+ vi.clearAllTimers();
29
+ vi.useRealTimers();
30
+ });
31
+
32
+ test('renders a single snackbar with default lifecycle wrapper', () => {
33
+ renderSnackbars([{ id: 'saved', children: 'Saved' }]);
34
+
35
+ expect(screen.getByTestId('ucl-uikit-snackbars')).toBeDefined();
36
+ expect(screen.getByTestId('ucl-uikit-snackbars-saved')).toBeDefined();
37
+ expect(screen.getByText('Saved')).toBeDefined();
38
+ });
39
+
40
+ test('passes snackbar props through to each rendered snackbar', () => {
41
+ const onFocus = vi.fn();
42
+ const onBlur = vi.fn();
43
+
44
+ renderSnackbars([
45
+ {
46
+ id: 'saved',
47
+ children: 'Saved',
48
+ closeButtonAriaLabel: 'Close saved notification',
49
+ onFocus,
50
+ onBlur,
51
+ tabIndex: 0,
52
+ title: 'Saved notification',
53
+ },
54
+ ]);
55
+
56
+ const snackbar = screen.getByTestId('ucl-uikit-snackbars-saved');
57
+
58
+ expect(snackbar).toHaveAttribute('title', 'Saved notification');
59
+ expect(
60
+ screen.getByLabelText('Close saved notification')
61
+ ).toBeInTheDocument();
62
+
63
+ act(() => {
64
+ snackbar.focus();
65
+ });
66
+
67
+ expect(onFocus).toHaveBeenCalledTimes(1);
68
+
69
+ act(() => {
70
+ snackbar.blur();
71
+ });
72
+
73
+ expect(onBlur).toHaveBeenCalledTimes(1);
74
+ });
75
+
76
+ test('renders up to maxVisible snackbars', () => {
77
+ renderSnackbars(
78
+ [
79
+ { id: 'one', children: 'One' },
80
+ { id: 'two', children: 'Two' },
81
+ { id: 'three', children: 'Three' },
82
+ ],
83
+ vi.fn(),
84
+ { maxVisible: 2 }
85
+ );
86
+
87
+ expect(screen.getByText('One')).toBeDefined();
88
+ expect(screen.getByText('Two')).toBeDefined();
89
+ expect(screen.queryByText('Three')).toBeNull();
90
+ });
91
+
92
+ test('close action dismisses after transition duration', () => {
93
+ const onDismiss = vi.fn();
94
+ renderSnackbars([{ id: 'saved', children: 'Saved' }], onDismiss, {
95
+ transitionDuration: 200,
96
+ });
97
+
98
+ const closeButton = screen.getByTestId(
99
+ 'ucl-uikit-snackbars-saved-close-button'
100
+ );
101
+
102
+ act(() => {
103
+ closeButton.click();
104
+ });
105
+
106
+ expect(closeButton).toBeDisabled();
107
+ expect(onDismiss).not.toHaveBeenCalled();
108
+
109
+ act(() => {
110
+ vi.advanceTimersByTime(200);
111
+ });
112
+
113
+ expect(onDismiss).toHaveBeenCalledWith('saved');
114
+ });
115
+
116
+ test('close action is disabled while exiting', () => {
117
+ const onClose = vi.fn();
118
+ const onDismiss = vi.fn();
119
+ renderSnackbars([{ id: 'saved', children: 'Saved', onClose }], onDismiss, {
120
+ transitionDuration: 200,
121
+ });
122
+
123
+ const closeButton = screen.getByTestId(
124
+ 'ucl-uikit-snackbars-saved-close-button'
125
+ );
126
+
127
+ act(() => {
128
+ closeButton.click();
129
+ closeButton.click();
130
+ });
131
+
132
+ expect(closeButton).toBeDisabled();
133
+ expect(onClose).toHaveBeenCalledTimes(1);
134
+ expect(onDismiss).not.toHaveBeenCalled();
135
+ });
136
+
137
+ test('undo action calls onUndo and dismisses after transition duration', () => {
138
+ const onDismiss = vi.fn();
139
+ const onUndo = vi.fn();
140
+ renderSnackbars(
141
+ [
142
+ {
143
+ id: 'undoable',
144
+ action: 'undo',
145
+ children: 'Undoable',
146
+ onUndo,
147
+ },
148
+ ],
149
+ onDismiss,
150
+ { transitionDuration: 200 }
151
+ );
152
+
153
+ act(() => {
154
+ screen.getByTestId('ucl-uikit-snackbars-undoable-undo-button').click();
155
+ });
156
+
157
+ expect(
158
+ screen.getByTestId('ucl-uikit-snackbars-undoable-undo-button')
159
+ ).toBeDisabled();
160
+ expect(onUndo).toHaveBeenCalledTimes(1);
161
+ expect(onDismiss).not.toHaveBeenCalled();
162
+
163
+ act(() => {
164
+ vi.advanceTimersByTime(200);
165
+ });
166
+
167
+ expect(onDismiss).toHaveBeenCalledWith('undoable');
168
+ });
169
+
170
+ test('undo action is disabled while exiting', () => {
171
+ const onDismiss = vi.fn();
172
+ const onUndo = vi.fn();
173
+ renderSnackbars(
174
+ [
175
+ {
176
+ id: 'undoable',
177
+ action: 'undo',
178
+ children: 'Undoable',
179
+ onUndo,
180
+ },
181
+ ],
182
+ onDismiss,
183
+ { transitionDuration: 200 }
184
+ );
185
+
186
+ const undoButton = screen.getByTestId(
187
+ 'ucl-uikit-snackbars-undoable-undo-button'
188
+ );
189
+
190
+ act(() => {
191
+ undoButton.click();
192
+ undoButton.click();
193
+ });
194
+
195
+ expect(undoButton).toBeDisabled();
196
+ expect(onUndo).toHaveBeenCalledTimes(1);
197
+ expect(onDismiss).not.toHaveBeenCalled();
198
+ });
199
+
200
+ test('auto-dismisses after duration and transition duration', () => {
201
+ const onDismiss = vi.fn();
202
+ renderSnackbars([{ id: 'saved', children: 'Saved' }], onDismiss, {
203
+ duration: 100,
204
+ transitionDuration: 200,
205
+ });
206
+
207
+ act(() => {
208
+ vi.advanceTimersByTime(100);
209
+ });
210
+
211
+ expect(onDismiss).not.toHaveBeenCalled();
212
+
213
+ act(() => {
214
+ vi.advanceTimersByTime(200);
215
+ });
216
+
217
+ expect(onDismiss).toHaveBeenCalledWith('saved');
218
+ });
219
+
220
+ test('does not auto-dismiss when duration is zero', () => {
221
+ const onDismiss = vi.fn();
222
+ renderSnackbars([{ id: 'saved', children: 'Saved' }], onDismiss, {
223
+ duration: 0,
224
+ transitionDuration: 50,
225
+ });
226
+
227
+ act(() => {
228
+ vi.advanceTimersByTime(1000);
229
+ });
230
+
231
+ expect(onDismiss).not.toHaveBeenCalled();
232
+ });
233
+
234
+ test('undo action still auto-dismisses when duration is zero', () => {
235
+ const onDismiss = vi.fn();
236
+ renderSnackbars(
237
+ [{ id: 'undoable', action: 'undo', children: 'Undoable' }],
238
+ onDismiss,
239
+ {
240
+ duration: 0,
241
+ transitionDuration: 50,
242
+ }
243
+ );
244
+
245
+ act(() => {
246
+ vi.advanceTimersByTime(5000);
247
+ });
248
+
249
+ expect(onDismiss).not.toHaveBeenCalled();
250
+
251
+ act(() => {
252
+ vi.advanceTimersByTime(50);
253
+ });
254
+
255
+ expect(onDismiss).toHaveBeenCalledWith('undoable');
256
+ });
257
+
258
+ test('undo action item duration zero falls back to the collection duration', () => {
259
+ const onDismiss = vi.fn();
260
+ renderSnackbars(
261
+ [
262
+ {
263
+ id: 'undoable',
264
+ action: 'undo',
265
+ children: 'Undoable',
266
+ duration: 0,
267
+ },
268
+ ],
269
+ onDismiss,
270
+ {
271
+ duration: 100,
272
+ transitionDuration: 50,
273
+ }
274
+ );
275
+
276
+ act(() => {
277
+ vi.advanceTimersByTime(100);
278
+ });
279
+
280
+ expect(onDismiss).not.toHaveBeenCalled();
281
+
282
+ act(() => {
283
+ vi.advanceTimersByTime(50);
284
+ });
285
+
286
+ expect(onDismiss).toHaveBeenCalledWith('undoable');
287
+ });
288
+
289
+ test('pauses auto-dismiss while a snackbar has focus', () => {
290
+ const onDismiss = vi.fn();
291
+ renderSnackbars([{ id: 'saved', children: 'Saved' }], onDismiss, {
292
+ duration: 100,
293
+ transitionDuration: 50,
294
+ });
295
+
296
+ act(() => {
297
+ vi.advanceTimersByTime(60);
298
+ });
299
+
300
+ act(() => {
301
+ screen.getByTestId('ucl-uikit-snackbars-saved-close-button').focus();
302
+ });
303
+
304
+ act(() => {
305
+ vi.advanceTimersByTime(500);
306
+ });
307
+
308
+ expect(onDismiss).not.toHaveBeenCalled();
309
+
310
+ act(() => {
311
+ screen.getByTestId('ucl-uikit-snackbars-saved-close-button').blur();
312
+ });
313
+
314
+ act(() => {
315
+ vi.advanceTimersByTime(39);
316
+ });
317
+
318
+ expect(onDismiss).not.toHaveBeenCalled();
319
+
320
+ act(() => {
321
+ vi.advanceTimersByTime(1);
322
+ });
323
+
324
+ expect(onDismiss).not.toHaveBeenCalled();
325
+
326
+ act(() => {
327
+ vi.advanceTimersByTime(50);
328
+ });
329
+
330
+ expect(onDismiss).toHaveBeenCalledWith('saved');
331
+ });
332
+
333
+ test('adding a snackbar does not refresh existing snackbar duration timers', () => {
334
+ const onDismiss = vi.fn();
335
+ const { rerender } = render(
336
+ <ThemeContextProvider>
337
+ <Snackbars
338
+ items={[{ id: 'first', children: 'First' }]}
339
+ onDismiss={onDismiss}
340
+ duration={100}
341
+ transitionDuration={50}
342
+ />
343
+ </ThemeContextProvider>
344
+ );
345
+
346
+ act(() => {
347
+ vi.advanceTimersByTime(75);
348
+ });
349
+
350
+ rerender(
351
+ <ThemeContextProvider>
352
+ <Snackbars
353
+ items={[
354
+ { id: 'first', children: 'First' },
355
+ { id: 'second', children: 'Second' },
356
+ ]}
357
+ onDismiss={onDismiss}
358
+ duration={100}
359
+ transitionDuration={50}
360
+ />
361
+ </ThemeContextProvider>
362
+ );
363
+
364
+ act(() => {
365
+ vi.advanceTimersByTime(25);
366
+ });
367
+
368
+ expect(onDismiss).not.toHaveBeenCalled();
369
+
370
+ act(() => {
371
+ vi.advanceTimersByTime(50);
372
+ });
373
+
374
+ expect(onDismiss).toHaveBeenCalledTimes(1);
375
+ expect(onDismiss).toHaveBeenCalledWith('first');
376
+ });
377
+ });
@@ -1,22 +1,23 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`Snackbar > snapshot: custom test id 1`] = `
4
- <output
5
- class="ucl-uikit-snackbar css-1rhqqf3"
4
+ <div
5
+ class="ucl-uikit-snackbar css-10q5wpo"
6
6
  data-testid="custom-test-id"
7
7
  >
8
8
  <svg
9
- class="ucl-uikit-icon css-1uzxprk"
9
+ aria-hidden="true"
10
+ class="ucl-uikit-icon css-1qgh0wp"
10
11
  data-testid="ucl-uikit-icon"
11
12
  fill="none"
12
13
  focusable="false"
13
- height="24"
14
+ height="32"
14
15
  stroke="currentColor"
15
16
  stroke-linecap="round"
16
17
  stroke-linejoin="round"
17
18
  stroke-width="2"
18
19
  viewBox="0 0 24 24"
19
- width="24"
20
+ width="32"
20
21
  xmlns="http://www.w3.org/2000/svg"
21
22
  >
22
23
  <path
@@ -26,39 +27,49 @@ exports[`Snackbar > snapshot: custom test id 1`] = `
26
27
  d="M22 4 12 14.01l-3-3"
27
28
  />
28
29
  </svg>
29
- <span>
30
+ <span
31
+ class="ucl-uikit-text css-5w4evg"
32
+ data-testid="ucl-uikit-text"
33
+ role="status"
34
+ >
30
35
  Test snackbar
31
36
  </span>
32
37
  <span
33
- class="css-1l2xg72"
38
+ class="css-1xjm0bq"
34
39
  >
35
40
  <button
36
- class="css-1nj6pk0"
41
+ class="css-bt8z9v"
37
42
  data-testid="custom-test-id-undo-button"
38
43
  >
39
- UNDO
44
+ <span
45
+ class="ucl-uikit-text css-1ccjqc4"
46
+ data-testid="ucl-uikit-text"
47
+ >
48
+ UNDO
49
+ </span>
40
50
  </button>
41
51
  </span>
42
- </output>
52
+ </div>
43
53
  `;
44
54
 
45
55
  exports[`Snackbar > snapshot: minimal props 1`] = `
46
- <output
47
- class="ucl-uikit-snackbar css-1rhqqf3"
56
+ <div
57
+ class="ucl-uikit-snackbar css-10q5wpo"
48
58
  data-testid="ucl-uikit-snackbar"
49
59
  >
50
60
  <svg
51
- class="ucl-uikit-icon css-1uzxprk"
61
+ aria-hidden="true"
62
+ class="ucl-uikit-icon css-1qgh0wp"
52
63
  data-testid="ucl-uikit-icon"
53
64
  fill="none"
54
65
  focusable="false"
55
- height="24"
66
+ height="32"
56
67
  stroke="currentColor"
57
68
  stroke-linecap="round"
58
69
  stroke-linejoin="round"
59
70
  stroke-width="2"
60
71
  viewBox="0 0 24 24"
61
- width="24"
72
+ width="32"
62
73
  xmlns="http://www.w3.org/2000/svg"
63
74
  >
64
75
  <path
@@ -68,17 +79,23 @@ exports[`Snackbar > snapshot: minimal props 1`] = `
68
79
  d="M22 4 12 14.01l-3-3"
69
80
  />
70
81
  </svg>
71
- <span>
82
+ <span
83
+ class="ucl-uikit-text css-5w4evg"
84
+ data-testid="ucl-uikit-text"
85
+ role="status"
86
+ >
72
87
  Test snackbar
73
88
  </span>
74
89
  <span
75
- class="css-1l2xg72"
90
+ class="css-1xjm0bq"
76
91
  >
77
92
  <button
78
- class="ucl-uikit-icon-button css-14jzv8g"
93
+ aria-label="Dismiss notification"
94
+ class="ucl-uikit-icon-button css-wreorn"
79
95
  data-testid="ucl-uikit-snackbar-close-button"
80
96
  >
81
97
  <svg
98
+ aria-hidden="true"
82
99
  class="ucl-uikit-icon css-148hpxb"
83
100
  data-testid="ucl-uikit-icon"
84
101
  fill="none"
@@ -98,5 +115,5 @@ exports[`Snackbar > snapshot: minimal props 1`] = `
98
115
  </svg>
99
116
  </button>
100
117
  </span>
101
- </output>
118
+ </div>
102
119
  `;
@@ -1,2 +1,8 @@
1
1
  export { default } from './Snackbar';
2
+ export { default as Snackbars } from './Snackbars';
2
3
  export type { SnackbarProps } from './Snackbar';
4
+ export type {
5
+ SnackbarsItem,
6
+ SnackbarsPlacement,
7
+ SnackbarsProps,
8
+ } from './Snackbars';
@@ -26,7 +26,13 @@ export { default as Overlay } from './Overlay';
26
26
  export type { OverlayProps } from './Overlay';
27
27
 
28
28
  export { default as Snackbar } from './Snackbar';
29
- export type { SnackbarProps } from './Snackbar';
29
+ export { Snackbars } from './Snackbar';
30
+ export type {
31
+ SnackbarProps,
32
+ SnackbarsItem,
33
+ SnackbarsPlacement,
34
+ SnackbarsProps,
35
+ } from './Snackbar';
30
36
 
31
37
  export { default as Avatar } from './Avatar';
32
38
  export type { AvatarProps } from './Avatar';
@@ -207,6 +213,9 @@ export type { DropdownMenuProps } from './DropdownMenu';
207
213
  export { default as DrawerMenu } from './DrawerMenu';
208
214
  export type { DrawerMenuProps } from './DrawerMenu';
209
215
 
216
+ export { default as Sidebar } from './Sidebar';
217
+ export type { SidebarProps } from './Sidebar';
218
+
210
219
  export { default as DropdownUserMenu } from './DropdownUserMenu';
211
220
  export type { DropdownUserMenuProps } from './DropdownUserMenu';
212
221
 
@@ -60,6 +60,59 @@ describe('announce (ARIA live region)', () => {
60
60
  expect(allRegions.length).toBe(1);
61
61
  });
62
62
 
63
+ test('appends the live region to the provided container', async () => {
64
+ const container = document.createElement('section');
65
+ container.setAttribute('data-testid', 'announce-container');
66
+ document.body.appendChild(container);
67
+
68
+ announce('Container message', false, container);
69
+
70
+ const region = await screen.findByTestId('aria-live-region');
71
+
72
+ expect(container.contains(region)).toBe(true);
73
+ expect(document.body.contains(region)).toBe(true);
74
+ });
75
+
76
+ test('falls back to document.body when no container is provided', async () => {
77
+ announce('Default container message');
78
+
79
+ const region = await screen.findByTestId('aria-live-region');
80
+
81
+ expect(region.parentElement).toBe(document.body);
82
+ });
83
+
84
+ test('moves live region when a new container is provided later', async () => {
85
+ const firstContainer = document.createElement('div');
86
+ const secondContainer = document.createElement('div');
87
+ document.body.appendChild(firstContainer);
88
+ document.body.appendChild(secondContainer);
89
+
90
+ announce('First container message', false, firstContainer);
91
+
92
+ const region = await screen.findByTestId('aria-live-region');
93
+ expect(firstContainer.contains(region)).toBe(true);
94
+
95
+ announce('Second container message', false, secondContainer);
96
+
97
+ expect(firstContainer.contains(region)).toBe(false);
98
+ expect(secondContainer.contains(region)).toBe(true);
99
+ });
100
+
101
+ test('recreates live region when previous one was removed from the DOM', async () => {
102
+ announce('Initial message');
103
+
104
+ const initialRegion = await screen.findByTestId('aria-live-region');
105
+ initialRegion.remove();
106
+
107
+ expect(screen.queryByTestId('aria-live-region')).toBeNull();
108
+
109
+ announce('Message after removal');
110
+
111
+ const recreatedRegion = await screen.findByTestId('aria-live-region');
112
+ expect(recreatedRegion).not.toBe(initialRegion);
113
+ expect(recreatedRegion.parentElement).toBe(document.body);
114
+ });
115
+
63
116
  test('queues multiple announcements and processes sequentially', async () => {
64
117
  vi.useFakeTimers();
65
118
 
@@ -11,6 +11,10 @@
11
11
  *
12
12
  * @param force - If true, forces the announcement even if it's the same as the last one.
13
13
  *
14
+ * @param container - Optional element to contain the live region. Accepts `null`
15
+ * to support passing values like `ref.current`; falls back to `document.body`
16
+ * when no container is available.
17
+ *
14
18
  * @example
15
19
  * announce("Item removed");
16
20
  *
@@ -26,14 +30,28 @@ const SR_DETECTION_DELAY = 100; // Time for SR to detect change
26
30
  const SR_PROCESSING_DELAY = 1000; // Time for SR to finish announcement
27
31
 
28
32
  /**
29
- * Creates the live region if it does not already exist.
33
+ * Creates the live region if it does not already exist, or moves it to a new
34
+ * container if one is provided. If the existing region has been removed from
35
+ * the DOM (e.g. a dialog unmounted), it will be recreated.
30
36
  */
31
- const initLiveRegion = (): void => {
32
- if (typeof document === 'undefined' || liveRegion) return;
37
+ const initLiveRegion = (container?: HTMLElement | null): void => {
38
+ if (typeof document === 'undefined') return;
33
39
 
34
- // Guard: body may not exist yet (e.g., script in <head>)
35
- const container = document.body || document.documentElement;
36
- if (!container) return;
40
+ // Reset if existing region has been removed from the DOM
41
+ if (liveRegion && !document.contains(liveRegion)) {
42
+ liveRegion = null;
43
+ }
44
+
45
+ const target = container || document.body || document.documentElement;
46
+ if (!target) return;
47
+
48
+ // If a specific container is requested and the region is elsewhere, move it
49
+ if (liveRegion && container && liveRegion.parentNode !== container) {
50
+ container.appendChild(liveRegion);
51
+ return;
52
+ }
53
+
54
+ if (liveRegion) return;
37
55
 
38
56
  const region = document.createElement('div');
39
57
  region.setAttribute('aria-live', 'polite');
@@ -48,7 +66,7 @@ const initLiveRegion = (): void => {
48
66
  region.style.height = '1px';
49
67
  region.style.overflow = 'hidden';
50
68
 
51
- document.body.appendChild(region);
69
+ target.appendChild(region);
52
70
  liveRegion = region;
53
71
  };
54
72
 
@@ -86,13 +104,18 @@ const processQueue = (): void => {
86
104
 
87
105
  /**
88
106
  * Announces a message to screen readers.
107
+ *
89
108
  */
90
- const announce = (message: string, force = false): void => {
109
+ const announce = (
110
+ message: string,
111
+ force = false,
112
+ container?: HTMLElement | null
113
+ ): void => {
91
114
  // SSR guard: do nothing if document is unavailable
92
115
  if (typeof document === 'undefined') return;
93
116
 
94
- // Initialize live region on first use
95
- if (!liveRegion) initLiveRegion();
117
+ // Ensure live region exists and is attached to the requested container when provided
118
+ initLiveRegion(container);
96
119
 
97
120
  // If forcing, clear the queue to prevent old messages
98
121
  if (force) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uikit-react-public",
3
3
  "private": false,
4
4
  "license": "UNLICENSED",
5
- "version": "0.30.0",
5
+ "version": "0.32.3",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",