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.
- package/dist/components/Dialog/BaseDialog.d.ts +1 -1
- package/dist/components/Dialog/Dialog.d.ts +1 -1
- package/dist/components/Dialog/Dialog.stories.d.ts +1 -1
- package/dist/components/Heading/Heading.stories.d.ts +5 -7
- package/dist/components/MenuNew/Menu.context.d.ts +9 -0
- package/dist/components/MenuNew/Menu.d.ts +2 -1
- package/dist/components/MenuNew/MenuHeading.d.ts +1 -1
- package/dist/components/MenuNew/MenuItemText.d.ts +2 -1
- package/dist/components/MenuNew/PrimaryMenuItem.d.ts +1 -1
- package/dist/components/Select/Select.types.d.ts +1 -1
- package/dist/components/Sidebar/Sidebar.d.ts +12 -0
- package/dist/components/Sidebar/Sidebar.stories.d.ts +38 -0
- package/dist/components/Sidebar/__tests__/Sidebar.test.d.ts +1 -0
- package/dist/components/Sidebar/index.d.ts +2 -0
- package/dist/components/Snackbar/Snackbar.d.ts +9 -5
- package/dist/components/Snackbar/Snackbar.stories.d.ts +2 -1
- package/dist/components/Snackbar/Snackbars.d.ts +19 -0
- package/dist/components/Snackbar/Snackbars.stories.d.ts +52 -0
- package/dist/components/Snackbar/__tests__/Snackbars.test.d.ts +1 -0
- package/dist/components/Snackbar/index.d.ts +2 -0
- package/dist/components/index.d.ts +4 -1
- package/dist/index.js +5897 -5358
- package/dist/utils/announce.d.ts +2 -1
- package/lib/components/Dialog/BaseDialog.tsx +180 -161
- package/lib/components/Dialog/Dialog.tsx +15 -11
- package/lib/components/Dropdown/Dropdown.stories.tsx +69 -74
- package/lib/components/Heading/Documentation.mdx +4 -14
- package/lib/components/Heading/Heading.stories.tsx +16 -30
- package/lib/components/MenuNew/Menu.context.tsx +18 -0
- package/lib/components/MenuNew/Menu.tsx +14 -9
- package/lib/components/MenuNew/MenuDivider.tsx +12 -1
- package/lib/components/MenuNew/MenuHeading.tsx +6 -0
- package/lib/components/MenuNew/MenuItemText.tsx +27 -3
- package/lib/components/MenuNew/MenuSection.tsx +11 -0
- package/lib/components/MenuNew/PrimaryMenuItem.tsx +71 -2
- package/lib/components/Select/Select.types.ts +1 -7
- package/lib/components/Select/__tests__/Select.test.tsx +112 -0
- package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +5 -0
- package/lib/components/Select/subcomponents/CustomSelect.tsx +75 -3
- package/lib/components/Select/subcomponents/FilterInput.tsx +1 -1
- package/lib/components/Sidebar/Sidebar.stories.tsx +94 -0
- package/lib/components/Sidebar/Sidebar.tsx +208 -0
- package/lib/components/Sidebar/__tests__/Sidebar.test.tsx +96 -0
- package/lib/components/Sidebar/__tests__/__snapshots__/Sidebar.test.tsx.snap +272 -0
- package/lib/components/Sidebar/index.ts +2 -0
- package/lib/components/Snackbar/Snackbar.stories.tsx +11 -0
- package/lib/components/Snackbar/Snackbar.tsx +162 -118
- package/lib/components/Snackbar/Snackbars.stories.tsx +130 -0
- package/lib/components/Snackbar/Snackbars.tsx +377 -0
- package/lib/components/Snackbar/__tests__/Snackbar.test.tsx +104 -0
- package/lib/components/Snackbar/__tests__/Snackbars.test.tsx +377 -0
- package/lib/components/Snackbar/__tests__/__snapshots__/Snackbar.test.tsx.snap +36 -19
- package/lib/components/Snackbar/index.ts +6 -0
- package/lib/components/index.ts +10 -1
- package/lib/utils/__tests__/announce.test.ts +53 -0
- package/lib/utils/announce.ts +33 -10
- 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
|
-
<
|
|
5
|
-
class="ucl-uikit-snackbar css-
|
|
4
|
+
<div
|
|
5
|
+
class="ucl-uikit-snackbar css-10q5wpo"
|
|
6
6
|
data-testid="custom-test-id"
|
|
7
7
|
>
|
|
8
8
|
<svg
|
|
9
|
-
|
|
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="
|
|
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="
|
|
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-
|
|
38
|
+
class="css-1xjm0bq"
|
|
34
39
|
>
|
|
35
40
|
<button
|
|
36
|
-
class="css-
|
|
41
|
+
class="css-bt8z9v"
|
|
37
42
|
data-testid="custom-test-id-undo-button"
|
|
38
43
|
>
|
|
39
|
-
|
|
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
|
-
</
|
|
52
|
+
</div>
|
|
43
53
|
`;
|
|
44
54
|
|
|
45
55
|
exports[`Snackbar > snapshot: minimal props 1`] = `
|
|
46
|
-
<
|
|
47
|
-
class="ucl-uikit-snackbar css-
|
|
56
|
+
<div
|
|
57
|
+
class="ucl-uikit-snackbar css-10q5wpo"
|
|
48
58
|
data-testid="ucl-uikit-snackbar"
|
|
49
59
|
>
|
|
50
60
|
<svg
|
|
51
|
-
|
|
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="
|
|
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="
|
|
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-
|
|
90
|
+
class="css-1xjm0bq"
|
|
76
91
|
>
|
|
77
92
|
<button
|
|
78
|
-
|
|
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
|
-
</
|
|
118
|
+
</div>
|
|
102
119
|
`;
|
package/lib/components/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
package/lib/utils/announce.ts
CHANGED
|
@@ -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'
|
|
37
|
+
const initLiveRegion = (container?: HTMLElement | null): void => {
|
|
38
|
+
if (typeof document === 'undefined') return;
|
|
33
39
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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 = (
|
|
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
|
-
//
|
|
95
|
-
|
|
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) {
|