uikit-react-public 0.30.1 → 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/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 +5740 -5217
- 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/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(
|