@transferwise/components 46.167.0 → 46.168.0
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/README.md +1 -1
- package/build/Link/Link.js.map +1 -1
- package/build/Link/Link.mjs.map +1 -1
- package/build/Loader/Loader.js.map +1 -1
- package/build/Loader/Loader.mjs.map +1 -1
- package/build/Prompt/PrimitivePrompt/PrimitivePrompt.js +2 -1
- package/build/Prompt/PrimitivePrompt/PrimitivePrompt.js.map +1 -1
- package/build/Prompt/PrimitivePrompt/PrimitivePrompt.mjs +2 -1
- package/build/Prompt/PrimitivePrompt/PrimitivePrompt.mjs.map +1 -1
- package/build/Prompt/Snackbar/Snackbar.js +205 -0
- package/build/Prompt/Snackbar/Snackbar.js.map +1 -0
- package/build/Prompt/Snackbar/Snackbar.messages.js +14 -0
- package/build/Prompt/Snackbar/Snackbar.messages.js.map +1 -0
- package/build/Prompt/Snackbar/Snackbar.messages.mjs +10 -0
- package/build/Prompt/Snackbar/Snackbar.messages.mjs.map +1 -0
- package/build/Prompt/Snackbar/Snackbar.mjs +203 -0
- package/build/Prompt/Snackbar/Snackbar.mjs.map +1 -0
- package/build/Prompt/Snackbar/SnackbarContext.js.map +1 -0
- package/build/Prompt/Snackbar/SnackbarContext.mjs.map +1 -0
- package/build/Prompt/Snackbar/SnackbarPortal.js +12 -0
- package/build/Prompt/Snackbar/SnackbarPortal.js.map +1 -0
- package/build/Prompt/Snackbar/SnackbarPortal.mjs +8 -0
- package/build/Prompt/Snackbar/SnackbarPortal.mjs.map +1 -0
- package/build/Prompt/Snackbar/SnackbarProvider.js +42 -0
- package/build/Prompt/Snackbar/SnackbarProvider.js.map +1 -0
- package/build/Prompt/Snackbar/SnackbarProvider.mjs +38 -0
- package/build/Prompt/Snackbar/SnackbarProvider.mjs.map +1 -0
- package/build/Prompt/Snackbar/constants.js +17 -0
- package/build/Prompt/Snackbar/constants.js.map +1 -0
- package/build/Prompt/Snackbar/constants.mjs +11 -0
- package/build/Prompt/Snackbar/constants.mjs.map +1 -0
- package/build/Prompt/Snackbar/useSnackbar.js.map +1 -0
- package/build/Prompt/Snackbar/useSnackbar.mjs.map +1 -0
- package/build/Prompt/Snackbar/useSnackbarAutoDismiss.js +66 -0
- package/build/Prompt/Snackbar/useSnackbarAutoDismiss.js.map +1 -0
- package/build/Prompt/Snackbar/useSnackbarAutoDismiss.mjs +62 -0
- package/build/Prompt/Snackbar/useSnackbarAutoDismiss.mjs.map +1 -0
- package/build/Prompt/Snackbar/useSnackbarManager.js +153 -0
- package/build/Prompt/Snackbar/useSnackbarManager.js.map +1 -0
- package/build/Prompt/Snackbar/useSnackbarManager.mjs +149 -0
- package/build/Prompt/Snackbar/useSnackbarManager.mjs.map +1 -0
- package/build/Prompt/Snackbar/useSnackbarSwipeDismiss.js +135 -0
- package/build/Prompt/Snackbar/useSnackbarSwipeDismiss.js.map +1 -0
- package/build/Prompt/Snackbar/useSnackbarSwipeDismiss.mjs +131 -0
- package/build/Prompt/Snackbar/useSnackbarSwipeDismiss.mjs.map +1 -0
- package/build/Section/Section.js.map +1 -1
- package/build/Section/Section.mjs.map +1 -1
- package/build/_deprecated/Select/Select.js +1 -1
- package/build/_deprecated/Select/Select.js.map +1 -1
- package/build/_deprecated/Select/Select.mjs +1 -1
- package/build/_deprecated/Select/Select.mjs.map +1 -1
- package/build/common/BottomSheet/BottomSheet.js +1 -3
- package/build/common/BottomSheet/BottomSheet.js.map +1 -1
- package/build/common/BottomSheet/BottomSheet.mjs +1 -3
- package/build/common/BottomSheet/BottomSheet.mjs.map +1 -1
- package/build/i18n/en.json +1 -0
- package/build/i18n/en.json.js +1 -0
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +1 -0
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/index.js +3 -3
- package/build/index.mjs +3 -3
- package/build/main.css +137 -75
- package/build/styles/IconButton/IconButton.css +3 -4
- package/build/styles/Prompt/Snackbar/Snackbar.css +134 -0
- package/build/styles/main.css +137 -75
- package/build/types/Link/Link.d.ts +1 -1
- package/build/types/Loader/Loader.d.ts +1 -1
- package/build/types/Prompt/PrimitivePrompt/PrimitivePrompt.d.ts +11 -0
- package/build/types/Prompt/PrimitivePrompt/PrimitivePrompt.d.ts.map +1 -1
- package/build/types/Prompt/Snackbar/Snackbar.d.ts +62 -0
- package/build/types/Prompt/Snackbar/Snackbar.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/Snackbar.messages.d.ts +9 -0
- package/build/types/Prompt/Snackbar/Snackbar.messages.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/Snackbar.types.d.ts +12 -0
- package/build/types/Prompt/Snackbar/Snackbar.types.d.ts.map +1 -0
- package/build/types/{Snackbar → Prompt/Snackbar}/SnackbarContext.d.ts +2 -2
- package/build/types/Prompt/Snackbar/SnackbarContext.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/SnackbarPortal.d.ts +4 -0
- package/build/types/Prompt/Snackbar/SnackbarPortal.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/SnackbarProvider.d.ts +13 -0
- package/build/types/Prompt/Snackbar/SnackbarProvider.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/constants.d.ts +9 -0
- package/build/types/Prompt/Snackbar/constants.d.ts.map +1 -0
- package/build/types/{Snackbar → Prompt/Snackbar}/index.d.ts +1 -1
- package/build/types/Prompt/Snackbar/index.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/useSnackbar.d.ts +3 -0
- package/build/types/Prompt/Snackbar/useSnackbar.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/useSnackbarAutoDismiss.d.ts +13 -0
- package/build/types/Prompt/Snackbar/useSnackbarAutoDismiss.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/useSnackbarManager.d.ts +20 -0
- package/build/types/Prompt/Snackbar/useSnackbarManager.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/useSnackbarSwipeDismiss.d.ts +22 -0
- package/build/types/Prompt/Snackbar/useSnackbarSwipeDismiss.d.ts.map +1 -0
- package/build/types/Section/Section.d.ts +1 -1
- package/build/types/_deprecated/Select/Select.d.ts +1 -1
- package/build/types/common/BottomSheet/BottomSheet.d.ts +1 -3
- package/build/types/common/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/build/types/index.d.ts +5 -5
- package/build/types/index.d.ts.map +1 -1
- package/build/types/test-utils/index.d.ts +2 -0
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/IconButton/IconButton.css +3 -4
- package/src/IconButton/IconButton.less +14 -1
- package/src/Link/Link.tsx +1 -1
- package/src/ListItem/AdditionalInfo/_stories/ListItemAdditionalInfo.story.tsx +1 -1
- package/src/ListItem/AvatarView/_stories/ListItemAvatarView.story.tsx +3 -3
- package/src/ListItem/_stories/ListItem.disabled.story.tsx +1 -1
- package/src/ListItem/_stories/ListItem.scenarios.story.tsx +2 -2
- package/src/Loader/Loader.tsx +1 -1
- package/src/Prompt/PrimitivePrompt/PrimitivePrompt.test.tsx +24 -1
- package/src/Prompt/PrimitivePrompt/PrimitivePrompt.tsx +8 -2
- package/src/Prompt/Snackbar/Snackbar.css +134 -0
- package/src/Prompt/Snackbar/Snackbar.less +159 -0
- package/src/Prompt/Snackbar/Snackbar.messages.ts +9 -0
- package/src/Prompt/Snackbar/Snackbar.test.tsx +1079 -0
- package/src/Prompt/Snackbar/Snackbar.tsx +245 -0
- package/src/Prompt/Snackbar/Snackbar.types.ts +17 -0
- package/src/{Snackbar → Prompt/Snackbar}/SnackbarContext.ts +2 -2
- package/src/Prompt/Snackbar/SnackbarPortal.tsx +8 -0
- package/src/Prompt/Snackbar/SnackbarProvider.test.tsx +783 -0
- package/src/Prompt/Snackbar/SnackbarProvider.tsx +41 -0
- package/src/Prompt/Snackbar/_stories/Snackbar.accessibility.docs.mdx +58 -0
- package/src/{Snackbar → Prompt/Snackbar}/_stories/Snackbar.rtl.test.story.tsx +17 -9
- package/src/Prompt/Snackbar/_stories/Snackbar.story.tsx +416 -0
- package/src/Prompt/Snackbar/_stories/Snackbar.test.story.tsx +263 -0
- package/src/Prompt/Snackbar/constants.ts +10 -0
- package/src/{Snackbar → Prompt/Snackbar}/index.ts +1 -1
- package/src/Prompt/Snackbar/useSnackbarAutoDismiss.ts +74 -0
- package/src/Prompt/Snackbar/useSnackbarManager.ts +169 -0
- package/src/Prompt/Snackbar/useSnackbarSwipeDismiss.ts +169 -0
- package/src/Section/Section.tsx +1 -1
- package/src/_deprecated/Select/Select.tsx +1 -1
- package/src/_deprecated/Select/_stories/Select.story.tsx +1 -1
- package/src/common/BottomSheet/BottomSheet.tsx +1 -3
- package/src/i18n/en.json +1 -0
- package/src/index.ts +5 -5
- package/src/main.css +137 -75
- package/src/main.less +1 -1
- package/build/Snackbar/Snackbar.js +0 -128
- package/build/Snackbar/Snackbar.js.map +0 -1
- package/build/Snackbar/Snackbar.mjs +0 -123
- package/build/Snackbar/Snackbar.mjs.map +0 -1
- package/build/Snackbar/SnackbarContext.js.map +0 -1
- package/build/Snackbar/SnackbarContext.mjs.map +0 -1
- package/build/Snackbar/SnackbarProvider.js +0 -46
- package/build/Snackbar/SnackbarProvider.js.map +0 -1
- package/build/Snackbar/SnackbarProvider.mjs +0 -42
- package/build/Snackbar/SnackbarProvider.mjs.map +0 -1
- package/build/Snackbar/useSnackbar.js.map +0 -1
- package/build/Snackbar/useSnackbar.mjs.map +0 -1
- package/build/styles/Snackbar/Snackbar.css +0 -71
- package/build/types/Snackbar/Snackbar.d.ts +0 -16
- package/build/types/Snackbar/Snackbar.d.ts.map +0 -1
- package/build/types/Snackbar/SnackbarContext.d.ts.map +0 -1
- package/build/types/Snackbar/SnackbarProvider.d.ts +0 -6
- package/build/types/Snackbar/SnackbarProvider.d.ts.map +0 -1
- package/build/types/Snackbar/index.d.ts.map +0 -1
- package/build/types/Snackbar/useSnackbar.d.ts +0 -3
- package/build/types/Snackbar/useSnackbar.d.ts.map +0 -1
- package/src/Snackbar/Snackbar.css +0 -71
- package/src/Snackbar/Snackbar.less +0 -80
- package/src/Snackbar/Snackbar.test.tsx +0 -206
- package/src/Snackbar/Snackbar.tsx +0 -129
- package/src/Snackbar/SnackbarProvider.tsx +0 -33
- package/src/Snackbar/_stories/Snackbar.story.tsx +0 -89
- package/src/Snackbar/_stories/Snackbar.test.story.tsx +0 -139
- /package/build/{Snackbar → Prompt/Snackbar}/SnackbarContext.js +0 -0
- /package/build/{Snackbar → Prompt/Snackbar}/SnackbarContext.mjs +0 -0
- /package/build/{Snackbar → Prompt/Snackbar}/useSnackbar.js +0 -0
- /package/build/{Snackbar → Prompt/Snackbar}/useSnackbar.mjs +0 -0
- /package/src/{Snackbar → Prompt/Snackbar}/useSnackbar.ts +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { userEvent, within, expect, waitFor } from 'storybook/test';
|
|
2
|
+
import { action } from 'storybook/actions';
|
|
3
|
+
import { Card } from '@transferwise/icons';
|
|
4
|
+
import { withVariantConfig } from '../../../../.storybook/helpers';
|
|
5
|
+
import { allModes } from '../../../../.storybook/modes';
|
|
6
|
+
import { lorem20 } from '../../../test-utils';
|
|
7
|
+
import { wait } from '../../../test-utils/wait';
|
|
8
|
+
import Button from '../../../Button';
|
|
9
|
+
import { Snackbar } from '../Snackbar';
|
|
10
|
+
import type { SnackbarCreationProps } from '../Snackbar.types';
|
|
11
|
+
import { SnackbarConsumer } from '../SnackbarContext';
|
|
12
|
+
import SnackbarProvider from '../SnackbarProvider';
|
|
13
|
+
import useSnackbar from '../useSnackbar';
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
title: 'Prompts/Snackbar/Tests',
|
|
17
|
+
component: Snackbar,
|
|
18
|
+
argTypes: {},
|
|
19
|
+
tags: ['!autodocs', '!manifest'],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const launchSnackbar = async (canvasElement: HTMLElement) => {
|
|
23
|
+
await wait(0);
|
|
24
|
+
const canvas = within(canvasElement);
|
|
25
|
+
await userEvent.click(canvas.getByRole('button', { name: 'Show Snackbar' }));
|
|
26
|
+
await waitFor(async () =>
|
|
27
|
+
expect(document.querySelector('.wds-Snackbar-container-appear-done')).not.toBeNull(),
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type Args = Partial<SnackbarCreationProps> & {
|
|
32
|
+
extraContext?: React.ReactNode;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function SnackbarTestTrigger({ text = 'Some text', extraContext, ...props }: Args) {
|
|
36
|
+
const createSnackbar = useSnackbar();
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<Button v2 block onClick={() => createSnackbar({ text, ...props })}>
|
|
41
|
+
Show Snackbar
|
|
42
|
+
</Button>
|
|
43
|
+
{extraContext}
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const Default = {
|
|
49
|
+
args: {
|
|
50
|
+
duration: 'indefinite',
|
|
51
|
+
action: { label: 'View', onClick: action('View button clicked.') },
|
|
52
|
+
} as Args,
|
|
53
|
+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
54
|
+
await launchSnackbar(canvasElement);
|
|
55
|
+
},
|
|
56
|
+
render: (args: Args) => {
|
|
57
|
+
return (
|
|
58
|
+
<SnackbarProvider>
|
|
59
|
+
<SnackbarTestTrigger {...args} />
|
|
60
|
+
</SnackbarProvider>
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const Basic = {
|
|
66
|
+
...Default,
|
|
67
|
+
args: {
|
|
68
|
+
...Default.args,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const Success = {
|
|
73
|
+
...Default,
|
|
74
|
+
args: { ...Default.args, sentiment: 'success' },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const Warning = {
|
|
78
|
+
...Default,
|
|
79
|
+
args: { ...Default.args, sentiment: 'warning' },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const Negative = {
|
|
83
|
+
...Default,
|
|
84
|
+
args: { ...Default.args, sentiment: 'negative' },
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const Proposition = {
|
|
88
|
+
...Default,
|
|
89
|
+
args: { ...Default.args, sentiment: 'proposition' },
|
|
90
|
+
parameters: {
|
|
91
|
+
chromatic: {
|
|
92
|
+
modes: { Desktop: allModes.desktop, 'Zoom 400%': allModes.zoom400 },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const WithoutAction = {
|
|
98
|
+
...Default,
|
|
99
|
+
args: { ...Default.args, action: undefined },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const CustomMedia = {
|
|
103
|
+
...Default,
|
|
104
|
+
render: (args: Args) => (
|
|
105
|
+
<SnackbarProvider>
|
|
106
|
+
<SnackbarTestTrigger {...args} media={<Card aria-hidden />} />
|
|
107
|
+
</SnackbarProvider>
|
|
108
|
+
),
|
|
109
|
+
parameters: {
|
|
110
|
+
chromatic: {
|
|
111
|
+
modes: {
|
|
112
|
+
Desktop: allModes.desktop,
|
|
113
|
+
Dark: { ...allModes.desktop, ...allModes.dark },
|
|
114
|
+
Mobile: allModes.largeMobile,
|
|
115
|
+
'Zoom 400%': allModes.zoom400,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const Loading = {
|
|
122
|
+
...Default,
|
|
123
|
+
args: {
|
|
124
|
+
...Default.args,
|
|
125
|
+
loading: true,
|
|
126
|
+
},
|
|
127
|
+
parameters: {
|
|
128
|
+
chromatic: {
|
|
129
|
+
modes: {
|
|
130
|
+
Desktop: allModes.desktop,
|
|
131
|
+
Dark: { ...allModes.desktop, ...allModes.dark },
|
|
132
|
+
Mobile: allModes.largeMobile,
|
|
133
|
+
'Zoom 400%': allModes.zoom400,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const NavigationAction = {
|
|
140
|
+
...Default,
|
|
141
|
+
args: {
|
|
142
|
+
...Default.args,
|
|
143
|
+
action: {
|
|
144
|
+
label: 'View transfer',
|
|
145
|
+
href: '/transfers/123',
|
|
146
|
+
target: '_blank',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
150
|
+
await launchSnackbar(canvasElement);
|
|
151
|
+
|
|
152
|
+
const link = await waitFor(() =>
|
|
153
|
+
within(document.body).getByRole('link', { name: 'View transfer' }),
|
|
154
|
+
);
|
|
155
|
+
await expect(link).toHaveAttribute('href', '/transfers/123');
|
|
156
|
+
await expect(link).toHaveAttribute('target', '_blank');
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const Mobile = {
|
|
161
|
+
...Default,
|
|
162
|
+
args: {
|
|
163
|
+
...Default.args,
|
|
164
|
+
},
|
|
165
|
+
...withVariantConfig(['mobile'], Default),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const DarkMode = {
|
|
169
|
+
...Default,
|
|
170
|
+
args: {
|
|
171
|
+
...Default.args,
|
|
172
|
+
},
|
|
173
|
+
globals: { screenMode: 'dark' },
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const Zoom400 = {
|
|
177
|
+
...Default,
|
|
178
|
+
args: {
|
|
179
|
+
...Default.args,
|
|
180
|
+
extraContext: (
|
|
181
|
+
<div className="p-t-3">
|
|
182
|
+
Snackbar isn't transparent (I'm going to make this text very long so that it will
|
|
183
|
+
fill up the entire page and go behind the snackbar. This way, when you look at it you will
|
|
184
|
+
easily be able to tell if the snackbar is transparent and recognize the bug because we
|
|
185
|
+
don't want it to be transparent. Transparent is bad. When we first did rebrand we had a
|
|
186
|
+
lot of problems with transparencies. {lorem20}
|
|
187
|
+
</div>
|
|
188
|
+
),
|
|
189
|
+
},
|
|
190
|
+
...withVariantConfig(['400%'], Default),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const Zoom400ShortMessage = {
|
|
194
|
+
...Default,
|
|
195
|
+
...withVariantConfig(['400%'], Default),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/** Deliberately unsuitable content: records how unbroken words can overflow the layout. */
|
|
199
|
+
export const LongUnbrokenText = {
|
|
200
|
+
...Default,
|
|
201
|
+
args: {
|
|
202
|
+
...Default.args,
|
|
203
|
+
text: `Your transfer is ready. ${'VeryLongUnbrokenTransferReference'.repeat(5)}`,
|
|
204
|
+
},
|
|
205
|
+
parameters: {
|
|
206
|
+
docs: {
|
|
207
|
+
description: {
|
|
208
|
+
story:
|
|
209
|
+
'Stress case, not recommended usage. Very long unbroken words can overflow the current layout; keep Snackbar messages short and readable.',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
chromatic: {
|
|
213
|
+
modes: { Desktop: allModes.desktop, Mobile: allModes.largeMobile },
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Snackbar is only shown once even if trigger is clicked many times
|
|
220
|
+
*/
|
|
221
|
+
export const MultipleClicks = {
|
|
222
|
+
...Default,
|
|
223
|
+
args: {
|
|
224
|
+
...Default.args,
|
|
225
|
+
},
|
|
226
|
+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
227
|
+
const canvas = within(canvasElement);
|
|
228
|
+
for (let i = 0; i < 8; i += 1) {
|
|
229
|
+
await userEvent.click(canvas.getByRole('button'));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await expect(within(document.body).getAllByRole('status')).toHaveLength(1);
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** Empty text dismisses the current Snackbar without rendering a blank replacement. */
|
|
237
|
+
export const ClearedMessage = {
|
|
238
|
+
...Default,
|
|
239
|
+
args: {
|
|
240
|
+
...Default.args,
|
|
241
|
+
extraContext: (
|
|
242
|
+
<SnackbarConsumer>
|
|
243
|
+
{({ createSnackbar }) => (
|
|
244
|
+
<Button v2 onClick={() => createSnackbar({ text: '' })}>
|
|
245
|
+
Clear Snackbar
|
|
246
|
+
</Button>
|
|
247
|
+
)}
|
|
248
|
+
</SnackbarConsumer>
|
|
249
|
+
),
|
|
250
|
+
},
|
|
251
|
+
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
252
|
+
const canvas = within(canvasElement);
|
|
253
|
+
await userEvent.click(canvas.getByRole('button', { name: 'Show Snackbar' }));
|
|
254
|
+
await waitFor(async () => {
|
|
255
|
+
await expect(within(document.body).getByRole('status')).toBeInTheDocument();
|
|
256
|
+
});
|
|
257
|
+
await userEvent.click(canvas.getByRole('button', { name: 'Clear Snackbar' }));
|
|
258
|
+
await waitFor(async () => {
|
|
259
|
+
await expect(within(document.body).queryByRole('status')).not.toBeInTheDocument();
|
|
260
|
+
await expect(document.querySelector('.wds-Snackbar')).toBeNull();
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const SNACKBAR_ENTER_DURATION = 300;
|
|
2
|
+
export const SNACKBAR_EXIT_DURATION = 150;
|
|
3
|
+
|
|
4
|
+
/** Default auto-dismiss duration for short feedback, in milliseconds. */
|
|
5
|
+
export const SNACKBAR_DEFAULT_DURATION = 4000;
|
|
6
|
+
/** Extended auto-dismiss duration for messages needing more reading time, in milliseconds. */
|
|
7
|
+
export const SNACKBAR_EXTENDED_DURATION = 6000;
|
|
8
|
+
|
|
9
|
+
/** Fraction of the Snackbar surface height a downward swipe must travel to dismiss (0.5 = half). */
|
|
10
|
+
export const SNACKBAR_SWIPE_DISMISS_THRESHOLD = 0.5;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { SNACKBAR_DEFAULT_DURATION, SNACKBAR_EXTENDED_DURATION } from './constants';
|
|
4
|
+
import type { SnackbarProps } from './Snackbar';
|
|
5
|
+
import type { SnackbarDuration } from './Snackbar.types';
|
|
6
|
+
|
|
7
|
+
const DURATIONS: Record<SnackbarDuration, number | null> = {
|
|
8
|
+
default: SNACKBAR_DEFAULT_DURATION,
|
|
9
|
+
extended: SNACKBAR_EXTENDED_DURATION,
|
|
10
|
+
indefinite: null,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Starts after entry finishes and pauses on hover, focus or swipe, preserving the remaining time. */
|
|
14
|
+
const useSnackbarAutoDismiss = ({
|
|
15
|
+
duration,
|
|
16
|
+
loading,
|
|
17
|
+
onDismissRequest,
|
|
18
|
+
timeout,
|
|
19
|
+
visible,
|
|
20
|
+
}: Pick<SnackbarProps, 'duration' | 'loading' | 'onDismissRequest' | 'timeout' | 'visible'>) => {
|
|
21
|
+
const surfaceRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
const milliseconds = loading
|
|
23
|
+
? null
|
|
24
|
+
: duration
|
|
25
|
+
? DURATIONS[duration]
|
|
26
|
+
: (timeout ?? SNACKBAR_DEFAULT_DURATION);
|
|
27
|
+
const remaining = useRef(milliseconds);
|
|
28
|
+
const [entered, setEntered] = useState(false);
|
|
29
|
+
const [hovered, setHovered] = useState(false);
|
|
30
|
+
const [focusWithin, setFocusWithin] = useState(false);
|
|
31
|
+
const [swiping, setSwiping] = useState(false);
|
|
32
|
+
|
|
33
|
+
const running = visible && entered && !hovered && !focusWithin && !swiping;
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
// Only a timing change starts a fresh budget; interaction and callback changes preserve it.
|
|
37
|
+
remaining.current = milliseconds;
|
|
38
|
+
}, [milliseconds]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const ms = remaining.current;
|
|
42
|
+
if (!running || ms === null) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const startedAt = Date.now();
|
|
47
|
+
const id = window.setTimeout(() => onDismissRequest(), ms);
|
|
48
|
+
return () => {
|
|
49
|
+
window.clearTimeout(id);
|
|
50
|
+
remaining.current = Math.max(0, ms - (Date.now() - startedAt));
|
|
51
|
+
};
|
|
52
|
+
}, [running, onDismissRequest, milliseconds]);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
handleBlur: (event: React.FocusEvent<HTMLDivElement>) => {
|
|
56
|
+
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
57
|
+
setFocusWithin(false);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
handleEntered: () => {
|
|
61
|
+
// Entry can finish beneath the pointer or with focus already inside the surface.
|
|
62
|
+
setHovered(surfaceRef.current?.matches(':hover') ?? false);
|
|
63
|
+
setFocusWithin(Boolean(surfaceRef.current?.contains(document.activeElement)));
|
|
64
|
+
setEntered(true);
|
|
65
|
+
},
|
|
66
|
+
handleFocus: () => setFocusWithin(true),
|
|
67
|
+
handleMouseEnter: () => setHovered(true),
|
|
68
|
+
handleMouseLeave: () => setHovered(false),
|
|
69
|
+
setSwiping,
|
|
70
|
+
surfaceRef,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default useSnackbarAutoDismiss;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { useCallback, useEffect, useReducer, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { SnackbarCreationProps } from './Snackbar.types';
|
|
4
|
+
|
|
5
|
+
type SnackbarInstance = {
|
|
6
|
+
id: number;
|
|
7
|
+
props: SnackbarCreationProps;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type SnackbarManagerState = {
|
|
11
|
+
/** The instance currently entering, visible or exiting. `null` means the provider is idle. */
|
|
12
|
+
active: SnackbarInstance | null;
|
|
13
|
+
/** Marks the active provider render as committed, not its entry animation as finished. */
|
|
14
|
+
activeHasRendered: boolean;
|
|
15
|
+
/** Changes when a new instance becomes active so repeated message text can be announced. */
|
|
16
|
+
announcementKey: number;
|
|
17
|
+
/** The latest replacement waiting for active to exit. `null` means no replacement is waiting. */
|
|
18
|
+
pending: SnackbarInstance | null;
|
|
19
|
+
/** Whether active should enter/stay visible; false requests its exit or indicates idle state. */
|
|
20
|
+
visible: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type SnackbarManagerAction =
|
|
24
|
+
| { type: 'show'; instance: SnackbarInstance }
|
|
25
|
+
| {
|
|
26
|
+
/** Ends pre-render coalescing; subsequent creates must wait for the committed instance to exit. */
|
|
27
|
+
type: 'rendered';
|
|
28
|
+
instanceId: number;
|
|
29
|
+
}
|
|
30
|
+
| { type: 'dismiss' }
|
|
31
|
+
| {
|
|
32
|
+
/** Like `dismiss`, but also cancels a queued replacement. Sent when `createSnackbar` gets empty text. */
|
|
33
|
+
type: 'clear';
|
|
34
|
+
}
|
|
35
|
+
| { type: 'exited'; instanceId: number };
|
|
36
|
+
|
|
37
|
+
const emptyState: SnackbarManagerState = {
|
|
38
|
+
active: null,
|
|
39
|
+
activeHasRendered: false,
|
|
40
|
+
announcementKey: 0,
|
|
41
|
+
pending: null,
|
|
42
|
+
visible: false,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Coalesces creates before the first commit, then keeps active content until its exit finishes.
|
|
47
|
+
* Only the latest pending replacement is retained. Exited promotes pending or returns to idle.
|
|
48
|
+
* Instance IDs prevent stale exit callbacks from affecting a newer Snackbar.
|
|
49
|
+
*/
|
|
50
|
+
const snackbarManagerReducer = (
|
|
51
|
+
state: SnackbarManagerState,
|
|
52
|
+
action: SnackbarManagerAction,
|
|
53
|
+
): SnackbarManagerState => {
|
|
54
|
+
switch (action.type) {
|
|
55
|
+
case 'show':
|
|
56
|
+
if (!state.active || !state.activeHasRendered) {
|
|
57
|
+
// Batched creates collapse to the latest instance before anything has entered.
|
|
58
|
+
return {
|
|
59
|
+
active: action.instance,
|
|
60
|
+
activeHasRendered: false,
|
|
61
|
+
announcementKey: state.announcementKey + 1,
|
|
62
|
+
pending: null,
|
|
63
|
+
visible: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Keep the active content until it exits; newer creates replace only the pending instance.
|
|
68
|
+
return { ...state, pending: action.instance, visible: false };
|
|
69
|
+
case 'rendered':
|
|
70
|
+
if (state.active?.id !== action.instanceId || state.activeHasRendered) {
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { ...state, activeHasRendered: true };
|
|
75
|
+
case 'dismiss':
|
|
76
|
+
case 'clear':
|
|
77
|
+
if (!state.active) {
|
|
78
|
+
return state;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!state.activeHasRendered) {
|
|
82
|
+
return { ...emptyState, announcementKey: state.announcementKey };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Dismissal lets a queued replacement show next; clearing cancels it as well.
|
|
86
|
+
return { ...state, pending: action.type === 'clear' ? null : state.pending, visible: false };
|
|
87
|
+
case 'exited':
|
|
88
|
+
// Ignore callbacks from an instance that has already been replaced.
|
|
89
|
+
if (state.active?.id !== action.instanceId) {
|
|
90
|
+
return state;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (state.pending) {
|
|
94
|
+
return {
|
|
95
|
+
active: state.pending,
|
|
96
|
+
activeHasRendered: false,
|
|
97
|
+
announcementKey: state.announcementKey + 1,
|
|
98
|
+
pending: null,
|
|
99
|
+
visible: true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { ...emptyState, announcementKey: state.announcementKey };
|
|
104
|
+
default:
|
|
105
|
+
return state;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Owns latest-wins replacement through reducer actions, separate from rendering and animation.
|
|
111
|
+
* Stable create/dismiss callbacks request state changes; the surface's onExited callback completes
|
|
112
|
+
* dismissal. The provider uses this state to mount/unmount the portal without a notification queue.
|
|
113
|
+
*/
|
|
114
|
+
const useSnackbarManager = () => {
|
|
115
|
+
const [state, dispatch] = useReducer(snackbarManagerReducer, emptyState);
|
|
116
|
+
const nextInstanceId = useRef(0);
|
|
117
|
+
|
|
118
|
+
const createSnackbar = useCallback((props: SnackbarCreationProps) => {
|
|
119
|
+
const { text } = props;
|
|
120
|
+
if (
|
|
121
|
+
text == null ||
|
|
122
|
+
typeof text === 'boolean' ||
|
|
123
|
+
(typeof text === 'string' && text.trim() === '')
|
|
124
|
+
) {
|
|
125
|
+
// A clear request must dismiss active and cancel pending, never create a blank replacement.
|
|
126
|
+
dispatch({ type: 'clear' });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
nextInstanceId.current += 1;
|
|
131
|
+
dispatch({ type: 'show', instance: { id: nextInstanceId.current, props } });
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
const dismissSnackbar = useCallback(() => dispatch({ type: 'dismiss' }), []);
|
|
135
|
+
|
|
136
|
+
const handleExited = useCallback(() => {
|
|
137
|
+
if (!state.active) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
state.active.props.onDismiss?.();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// This runs in a transition commit; an escaping error could unmount the provider.
|
|
145
|
+
// eslint-disable-next-line no-console
|
|
146
|
+
console.error('Snackbar onDismiss callback failed:', error);
|
|
147
|
+
} finally {
|
|
148
|
+
dispatch({ type: 'exited', instanceId: state.active.id });
|
|
149
|
+
}
|
|
150
|
+
}, [state.active]);
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (state.active && state.visible && !state.activeHasRendered) {
|
|
154
|
+
// Later creates can now wait for this instance's exit instead of coalescing before render.
|
|
155
|
+
dispatch({ type: 'rendered', instanceId: state.active.id });
|
|
156
|
+
}
|
|
157
|
+
}, [state.active, state.activeHasRendered, state.visible]);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
active: state.active,
|
|
161
|
+
announcementKey: state.announcementKey,
|
|
162
|
+
createSnackbar,
|
|
163
|
+
dismissSnackbar,
|
|
164
|
+
handleExited,
|
|
165
|
+
visible: state.visible,
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default useSnackbarManager;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { SNACKBAR_SWIPE_DISMISS_THRESHOLD } from './constants';
|
|
4
|
+
|
|
5
|
+
const DISMISS_CLASS = 'wds-Snackbar--swipe-dismiss';
|
|
6
|
+
const SETTLE_CLASS = 'wds-Snackbar--swipe-settle';
|
|
7
|
+
|
|
8
|
+
type SwipePointer = {
|
|
9
|
+
id: number;
|
|
10
|
+
target: Element;
|
|
11
|
+
startX: number;
|
|
12
|
+
startY: number;
|
|
13
|
+
threshold: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Captures one primary pointer and follows downward, mostly vertical movement without re-rendering.
|
|
18
|
+
* Release dismisses at half the surface height; shorter/cancelled gestures settle back into place.
|
|
19
|
+
* The outer wrapper moves independently of entry/exit motion, and a committed swipe consumes its
|
|
20
|
+
* follow-up click so dragging from the action cannot activate it.
|
|
21
|
+
*/
|
|
22
|
+
const useSnackbarSwipeDismiss = ({
|
|
23
|
+
onDismissRequest,
|
|
24
|
+
onSwipeChange,
|
|
25
|
+
visible,
|
|
26
|
+
}: {
|
|
27
|
+
onDismissRequest: () => void;
|
|
28
|
+
/** Reports when a gesture starts and ends so auto-dismiss can pause beneath the pointer. */
|
|
29
|
+
onSwipeChange: (swiping: boolean) => void;
|
|
30
|
+
visible: boolean;
|
|
31
|
+
}) => {
|
|
32
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
33
|
+
const pointerRef = useRef<SwipePointer | null>(null);
|
|
34
|
+
const suppressClick = useRef(false);
|
|
35
|
+
|
|
36
|
+
const clearSwipePosition = useCallback(() => {
|
|
37
|
+
const pointer = pointerRef.current;
|
|
38
|
+
pointerRef.current = null;
|
|
39
|
+
if (pointer) {
|
|
40
|
+
if (pointer.target.hasPointerCapture?.(pointer.id)) {
|
|
41
|
+
pointer.target.releasePointerCapture(pointer.id);
|
|
42
|
+
}
|
|
43
|
+
onSwipeChange(false);
|
|
44
|
+
}
|
|
45
|
+
suppressClick.current = false;
|
|
46
|
+
containerRef.current?.classList.remove(DISMISS_CLASS, SETTLE_CLASS);
|
|
47
|
+
containerRef.current?.style.removeProperty('transform');
|
|
48
|
+
}, [onSwipeChange]);
|
|
49
|
+
|
|
50
|
+
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
|
|
51
|
+
if (event.button !== 0 || !event.isPrimary || pointerRef.current) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
clearSwipePosition();
|
|
56
|
+
const target = event.target as Element;
|
|
57
|
+
target.setPointerCapture?.(event.pointerId);
|
|
58
|
+
pointerRef.current = {
|
|
59
|
+
id: event.pointerId,
|
|
60
|
+
target,
|
|
61
|
+
startX: event.clientX,
|
|
62
|
+
startY: event.clientY,
|
|
63
|
+
threshold: event.currentTarget.offsetHeight * SNACKBAR_SWIPE_DISMISS_THRESHOLD,
|
|
64
|
+
};
|
|
65
|
+
onSwipeChange(true);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
|
|
69
|
+
const pointer = pointerRef.current;
|
|
70
|
+
if (pointer?.id !== event.pointerId) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const offsetY = event.clientY - pointer.startY;
|
|
75
|
+
const offsetX = event.clientX - pointer.startX;
|
|
76
|
+
if (offsetY > 0 && offsetY > Math.abs(offsetX)) {
|
|
77
|
+
containerRef.current?.style.setProperty('transform', `translateY(${offsetY}px)`);
|
|
78
|
+
} else {
|
|
79
|
+
containerRef.current?.style.removeProperty('transform');
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
|
|
84
|
+
const pointer = pointerRef.current;
|
|
85
|
+
if (pointer?.id !== event.pointerId) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const offsetY = event.clientY - pointer.startY;
|
|
90
|
+
const offsetX = event.clientX - pointer.startX;
|
|
91
|
+
const container = containerRef.current;
|
|
92
|
+
const bounds = pointer.target.closest('a, button')?.getBoundingClientRect();
|
|
93
|
+
clearSwipePosition();
|
|
94
|
+
|
|
95
|
+
if (offsetY >= pointer.threshold && offsetY > Math.abs(offsetX)) {
|
|
96
|
+
if (container) {
|
|
97
|
+
// Include wrapper padding and the safe area, not only the surface, to clear the viewport.
|
|
98
|
+
container.classList.add(DISMISS_CLASS);
|
|
99
|
+
container.style.setProperty(
|
|
100
|
+
'transform',
|
|
101
|
+
`translateY(${Math.max(offsetY, container.offsetHeight)}px)`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
suppressClick.current = true;
|
|
105
|
+
onDismissRequest();
|
|
106
|
+
} else {
|
|
107
|
+
// Capture retargets clicks to the pressed control even when released outside it.
|
|
108
|
+
suppressClick.current =
|
|
109
|
+
!!bounds &&
|
|
110
|
+
(offsetX !== 0 || offsetY !== 0) &&
|
|
111
|
+
(event.clientX < bounds.left ||
|
|
112
|
+
event.clientX > bounds.right ||
|
|
113
|
+
event.clientY < bounds.top ||
|
|
114
|
+
event.clientY > bounds.bottom);
|
|
115
|
+
container?.classList.add(SETTLE_CLASS);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handlePointerCancel = useCallback(
|
|
120
|
+
(event: Pick<PointerEvent, 'pointerId'>) => {
|
|
121
|
+
if (pointerRef.current?.id !== event.pointerId) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
clearSwipePosition();
|
|
126
|
+
containerRef.current?.classList.add(SETTLE_CLASS);
|
|
127
|
+
},
|
|
128
|
+
[clearSwipePosition],
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (visible) {
|
|
133
|
+
clearSwipePosition();
|
|
134
|
+
} else if (pointerRef.current) {
|
|
135
|
+
// An exit starting mid-gesture (timeout, Escape, replacement) ends it without dismissing again.
|
|
136
|
+
handlePointerCancel({ pointerId: pointerRef.current.id });
|
|
137
|
+
}
|
|
138
|
+
}, [visible, clearSwipePosition, handlePointerCancel]);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
// Removed descendants report capture loss on their document, not the Snackbar surface.
|
|
142
|
+
const ownerDocument = containerRef.current?.ownerDocument;
|
|
143
|
+
ownerDocument?.addEventListener('lostpointercapture', handlePointerCancel);
|
|
144
|
+
return () => {
|
|
145
|
+
ownerDocument?.removeEventListener('lostpointercapture', handlePointerCancel);
|
|
146
|
+
clearSwipePosition();
|
|
147
|
+
};
|
|
148
|
+
}, [clearSwipePosition, handlePointerCancel]);
|
|
149
|
+
|
|
150
|
+
const handleClickCapture = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
151
|
+
if (suppressClick.current && event.detail > 0) {
|
|
152
|
+
suppressClick.current = false;
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
event.stopPropagation();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
clearSwipePosition,
|
|
160
|
+
containerRef,
|
|
161
|
+
handleClickCapture,
|
|
162
|
+
handlePointerCancel,
|
|
163
|
+
handlePointerDown,
|
|
164
|
+
handlePointerMove,
|
|
165
|
+
handlePointerUp,
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default useSnackbarSwipeDismiss;
|
package/src/Section/Section.tsx
CHANGED