@transferwise/components 0.0.0-experimental-4b93e2b → 0.0.0-experimental-f144cf1
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/build/Calendar/DateHeader/DateHeader.js +6 -2
- package/build/Calendar/DateHeader/DateHeader.js.map +1 -1
- package/build/Calendar/DateHeader/DateHeader.mjs +7 -3
- package/build/Calendar/DateHeader/DateHeader.mjs.map +1 -1
- package/build/Carousel/Carousel.js +2 -2
- package/build/Carousel/Carousel.js.map +1 -1
- package/build/Carousel/Carousel.mjs +3 -3
- package/build/Carousel/Carousel.mjs.map +1 -1
- package/build/Inputs/SelectInput/OptionContent/SelectInputOptionContent.js +3 -4
- package/build/Inputs/SelectInput/OptionContent/SelectInputOptionContent.js.map +1 -1
- package/build/Inputs/SelectInput/OptionContent/SelectInputOptionContent.mjs +3 -4
- package/build/Inputs/SelectInput/OptionContent/SelectInputOptionContent.mjs.map +1 -1
- package/build/ListItem/Navigation/ListItemNavigation.js +1 -1
- package/build/ListItem/Navigation/ListItemNavigation.js.map +1 -1
- package/build/ListItem/Navigation/ListItemNavigation.mjs +2 -2
- package/build/ListItem/Navigation/ListItemNavigation.mjs.map +1 -1
- package/build/PhoneNumberInput/PhoneNumberInput.js +1 -2
- package/build/PhoneNumberInput/PhoneNumberInput.js.map +1 -1
- package/build/PhoneNumberInput/PhoneNumberInput.mjs +1 -2
- package/build/PhoneNumberInput/PhoneNumberInput.mjs.map +1 -1
- package/build/PromoCard/Indicator/PromoCardIndicator.js +1 -1
- package/build/PromoCard/Indicator/PromoCardIndicator.js.map +1 -1
- package/build/PromoCard/Indicator/PromoCardIndicator.mjs +2 -2
- package/build/PromoCard/Indicator/PromoCardIndicator.mjs.map +1 -1
- package/build/Snackbar/Snackbar.js +212 -108
- package/build/Snackbar/Snackbar.js.map +1 -1
- package/build/Snackbar/Snackbar.messages.js +14 -0
- package/build/Snackbar/Snackbar.messages.js.map +1 -0
- package/build/Snackbar/Snackbar.messages.mjs +10 -0
- package/build/Snackbar/Snackbar.messages.mjs.map +1 -0
- package/build/Snackbar/Snackbar.mjs +213 -107
- package/build/Snackbar/Snackbar.mjs.map +1 -1
- package/build/Snackbar/SnackbarContext.js.map +1 -1
- package/build/Snackbar/SnackbarContext.mjs.map +1 -1
- package/build/Snackbar/SnackbarPortal.js +92 -0
- package/build/Snackbar/SnackbarPortal.js.map +1 -0
- package/build/Snackbar/SnackbarPortal.mjs +89 -0
- package/build/Snackbar/SnackbarPortal.mjs.map +1 -0
- package/build/Snackbar/SnackbarProvider.js +21 -31
- package/build/Snackbar/SnackbarProvider.js.map +1 -1
- package/build/Snackbar/SnackbarProvider.mjs +22 -32
- package/build/Snackbar/SnackbarProvider.mjs.map +1 -1
- package/build/Snackbar/constants.js +14 -0
- package/build/Snackbar/constants.js.map +1 -0
- package/build/Snackbar/constants.mjs +8 -0
- package/build/Snackbar/constants.mjs.map +1 -0
- package/build/Snackbar/useSnackbarAutoDismiss.js +135 -0
- package/build/Snackbar/useSnackbarAutoDismiss.js.map +1 -0
- package/build/Snackbar/useSnackbarAutoDismiss.mjs +131 -0
- package/build/Snackbar/useSnackbarAutoDismiss.mjs.map +1 -0
- package/build/Snackbar/useSnackbarManager.js +164 -0
- package/build/Snackbar/useSnackbarManager.js.map +1 -0
- package/build/Snackbar/useSnackbarManager.mjs +160 -0
- package/build/Snackbar/useSnackbarManager.mjs.map +1 -0
- package/build/Snackbar/useSnackbarSwipeDismiss.js +114 -0
- package/build/Snackbar/useSnackbarSwipeDismiss.js.map +1 -0
- package/build/Snackbar/useSnackbarSwipeDismiss.mjs +110 -0
- package/build/Snackbar/useSnackbarSwipeDismiss.mjs.map +1 -0
- package/build/common/PropsValues/typography.js +0 -6
- package/build/common/PropsValues/typography.js.map +1 -1
- package/build/common/PropsValues/typography.mjs +0 -6
- package/build/common/PropsValues/typography.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 -1
- package/build/index.js.map +1 -1
- package/build/index.mjs +2 -1
- package/build/index.mjs.map +1 -1
- package/build/main.css +864 -1509
- package/build/styles/Button/Button.css +3 -0
- package/build/styles/DateInput/DateInput.css +0 -13
- package/build/styles/Inputs/Input/Input.css +24 -0
- package/build/styles/Inputs/TextArea/TextArea.css +24 -0
- package/build/styles/ListItem/ListItem.css +1 -1
- package/build/styles/PhoneNumberInput/PhoneNumberInput.css +0 -3
- package/build/styles/Popover/Popover.css +6 -0
- package/build/styles/PromoCard/PromoCard.css +3 -0
- package/build/styles/Snackbar/Snackbar.css +182 -61
- package/build/styles/_deprecated/Select/Select.css +6 -0
- package/build/styles/css/accordion.css +12 -0
- package/build/styles/css/button-groups.css +6 -0
- package/build/styles/css/buttons.css +2 -0
- package/build/styles/css/decision.css +12 -0
- package/build/styles/css/dropdowns.css +18 -2
- package/build/styles/css/droppable.css +6 -0
- package/build/styles/css/footer.css +12 -0
- package/build/styles/css/input-groups.css +42 -2
- package/build/styles/css/list-group.css +6 -0
- package/build/styles/css/navbar.css +18 -2
- package/build/styles/css/neptune-core.css +512 -4
- package/build/styles/css/neptune.css +640 -1426
- package/build/styles/css/popovers.css +18 -0
- package/build/styles/css/tooltip.css +6 -0
- package/build/styles/less/neptune-tokens.less +0 -8
- package/build/styles/main.css +864 -1509
- package/build/styles/props/neptune-tokens.css +0 -8
- package/build/styles/styles/less/accordion.css +12 -0
- package/build/styles/styles/less/button-groups.css +6 -0
- package/build/styles/styles/less/buttons.css +2 -0
- package/build/styles/styles/less/decision.css +12 -0
- package/build/styles/styles/less/dropdowns.css +18 -2
- package/build/styles/styles/less/droppable.css +6 -0
- package/build/styles/styles/less/footer.css +12 -0
- package/build/styles/styles/less/forms/bootstrap-forms.css +24 -2
- package/build/styles/styles/less/input-groups.css +42 -2
- package/build/styles/styles/less/list-group.css +6 -0
- package/build/styles/styles/less/navbar.css +18 -2
- package/build/styles/styles/less/neptune-core.css +512 -4
- package/build/styles/styles/less/neptune.css +640 -1426
- package/build/styles/styles/less/popovers.css +18 -0
- package/build/styles/styles/less/tooltip.css +6 -0
- package/build/types/Inputs/SelectInput/OptionContent/SelectInputOptionContent.d.ts +1 -2
- package/build/types/Inputs/SelectInput/OptionContent/SelectInputOptionContent.d.ts.map +1 -1
- package/build/types/PhoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
- package/build/types/Snackbar/Snackbar.d.ts +41 -24
- package/build/types/Snackbar/Snackbar.d.ts.map +1 -1
- package/build/types/Snackbar/Snackbar.messages.d.ts +9 -0
- package/build/types/Snackbar/Snackbar.messages.d.ts.map +1 -0
- package/build/types/Snackbar/Snackbar.types.d.ts +21 -0
- package/build/types/Snackbar/Snackbar.types.d.ts.map +1 -0
- package/build/types/Snackbar/SnackbarContext.d.ts +2 -2
- package/build/types/Snackbar/SnackbarContext.d.ts.map +1 -1
- package/build/types/Snackbar/SnackbarPortal.d.ts +5 -0
- package/build/types/Snackbar/SnackbarPortal.d.ts.map +1 -0
- package/build/types/Snackbar/SnackbarProvider.d.ts +6 -0
- package/build/types/Snackbar/SnackbarProvider.d.ts.map +1 -1
- package/build/types/Snackbar/constants.d.ts +6 -0
- package/build/types/Snackbar/constants.d.ts.map +1 -0
- package/build/types/Snackbar/index.d.ts +3 -1
- package/build/types/Snackbar/index.d.ts.map +1 -1
- package/build/types/Snackbar/useSnackbar.d.ts +1 -1
- package/build/types/Snackbar/useSnackbar.d.ts.map +1 -1
- package/build/types/Snackbar/useSnackbarAutoDismiss.d.ts +18 -0
- package/build/types/Snackbar/useSnackbarAutoDismiss.d.ts.map +1 -0
- package/build/types/Snackbar/useSnackbarManager.d.ts +17 -0
- package/build/types/Snackbar/useSnackbarManager.d.ts.map +1 -0
- package/build/types/Snackbar/useSnackbarSwipeDismiss.d.ts +11 -0
- package/build/types/Snackbar/useSnackbarSwipeDismiss.d.ts.map +1 -0
- package/build/types/common/PropsValues/typography.d.ts +1 -13
- package/build/types/common/PropsValues/typography.d.ts.map +1 -1
- package/build/types/index.d.ts +2 -2
- 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/Button/Button.css +3 -0
- package/src/Button/Button.less +16 -0
- package/src/Button/_stories/Button.test.story.tsx +12 -12
- package/src/Calendar/DateHeader/DateHeader.tsx +3 -3
- package/src/Carousel/Carousel.tsx +3 -3
- package/src/DateInput/DateInput.css +0 -13
- package/src/DateInput/DateInput.less +2 -10
- package/src/DateInput/_stories/DateInput.test.story.tsx +13 -0
- package/src/Display/_stories/Display.story.tsx +6 -20
- package/src/ExpressiveMoneyInput/_stories/ExpressiveMoneyInput.rtl.test.story.tsx +1 -1
- package/src/InputWithDisplayFormat/_stories/InputWithDisplayFormat.test.story.tsx +0 -52
- package/src/Inputs/Input/Input.css +24 -0
- package/src/Inputs/SelectInput/OptionContent/SelectInputOptionContent.tsx +1 -4
- package/src/Inputs/SelectInput/SelectInput.test.tsx +0 -74
- package/src/Inputs/TextArea/TextArea.css +24 -0
- package/src/ListItem/ListItem.css +1 -1
- package/src/ListItem/ListItem.grid.less +4 -4
- package/src/ListItem/ListItem.less +1 -1
- package/src/ListItem/Navigation/ListItemNavigation.tsx +2 -2
- package/src/PhoneNumberInput/PhoneNumberInput.css +0 -3
- package/src/PhoneNumberInput/PhoneNumberInput.less +2 -10
- package/src/PhoneNumberInput/PhoneNumberInput.tsx +0 -1
- package/src/PhoneNumberInput/_stories/PhoneNumberInput.rtl.test.story.tsx +1 -1
- package/src/Popover/Popover.css +6 -0
- package/src/PromoCard/Indicator/PromoCardIndicator.tsx +2 -2
- package/src/PromoCard/PromoCard.css +3 -0
- package/src/PromoCard/PromoCard.less +5 -0
- package/src/Prompt/CriticalBanner/_stories/CriticalBanner.test.story.tsx +1 -1
- package/src/Snackbar/Snackbar.css +182 -61
- package/src/Snackbar/Snackbar.less +185 -56
- package/src/Snackbar/Snackbar.messages.ts +9 -0
- package/src/Snackbar/Snackbar.test.tsx +697 -0
- package/src/Snackbar/Snackbar.tsx +227 -96
- package/src/Snackbar/Snackbar.types.ts +26 -0
- package/src/Snackbar/SnackbarContext.ts +2 -2
- package/src/Snackbar/SnackbarPortal.tsx +110 -0
- package/src/Snackbar/SnackbarProvider.test.tsx +202 -0
- package/src/Snackbar/SnackbarProvider.tsx +25 -25
- package/src/Snackbar/_stories/Snackbar.accessibility.docs.mdx +73 -0
- package/src/Snackbar/_stories/Snackbar.rtl.test.story.tsx +1 -12
- package/src/Snackbar/_stories/Snackbar.story.tsx +447 -62
- package/src/Snackbar/_stories/Snackbar.test.story.tsx +91 -16
- package/src/Snackbar/constants.ts +5 -0
- package/src/Snackbar/index.ts +3 -1
- package/src/Snackbar/useSnackbarAutoDismiss.ts +155 -0
- package/src/Snackbar/useSnackbarManager.ts +181 -0
- package/src/Snackbar/useSnackbarSwipeDismiss.ts +129 -0
- package/src/_deprecated/Select/Select.css +6 -0
- package/src/common/PropsValues/typography.ts +1 -20
- package/src/i18n/en.json +1 -1
- package/src/index.ts +7 -2
- package/src/main.css +864 -1509
- package/src/styles/less/accordion.css +12 -0
- package/src/styles/less/button-groups.css +6 -0
- package/src/styles/less/buttons.css +2 -0
- package/src/styles/less/core/_typography.less +194 -26
- package/src/styles/less/decision.css +12 -0
- package/src/styles/less/dropdowns.css +18 -2
- package/src/styles/less/droppable.css +6 -0
- package/src/styles/less/footer.css +12 -0
- package/src/styles/less/forms/bootstrap-forms.css +24 -2
- package/src/styles/less/input-groups.css +42 -2
- package/src/styles/less/list-group.css +6 -0
- package/src/styles/less/navbar.css +18 -2
- package/src/styles/less/neptune-core.css +512 -4
- package/src/styles/less/neptune.bundle.less +0 -2
- package/src/styles/less/neptune.css +640 -1426
- package/src/styles/less/popovers.css +18 -0
- package/src/styles/less/tooltip.css +6 -0
- package/src/DateInput/_stories/DateInput.rtl.test.story.tsx +0 -36
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import { Card } from '@transferwise/icons';
|
|
2
|
+
|
|
3
|
+
import { act, fireEvent, mockMatchMedia, render, screen, userEvent } from '../test-utils';
|
|
4
|
+
import { WDS_LIVE_REGION_DELAY_MS } from '../common/constants';
|
|
5
|
+
import { resetLiveRegionAnnouncementQueue } from '../common/LiveRegion/LiveRegion';
|
|
6
|
+
|
|
7
|
+
import { SNACKBAR_EXIT_DURATION } from './constants';
|
|
8
|
+
import { Snackbar } from './Snackbar';
|
|
9
|
+
import { SnackbarPortal } from './SnackbarPortal';
|
|
10
|
+
import type { SnackbarProps } from './Snackbar.types';
|
|
11
|
+
|
|
12
|
+
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTimeAsync });
|
|
13
|
+
|
|
14
|
+
mockMatchMedia();
|
|
15
|
+
|
|
16
|
+
const advanceTimers = (duration: number) => {
|
|
17
|
+
act(() => {
|
|
18
|
+
jest.advanceTimersByTime(duration);
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const enableLiveRegion = () => advanceTimers(WDS_LIVE_REGION_DELAY_MS);
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
resetLiveRegionAnnouncementQueue();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const showSnackbar = (props: Partial<SnackbarProps> = {}) =>
|
|
29
|
+
render(<Snackbar {...props} text={props.text ?? 'Changes saved'} />);
|
|
30
|
+
|
|
31
|
+
describe('Snackbar duration', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
jest.useFakeTimers();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
jest.clearAllTimers();
|
|
38
|
+
jest.useRealTimers();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('dismisses after the default four-second duration', () => {
|
|
42
|
+
showSnackbar();
|
|
43
|
+
|
|
44
|
+
advanceTimers(3999);
|
|
45
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
46
|
+
|
|
47
|
+
advanceTimers(1);
|
|
48
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
49
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('dismisses after the extended six-second duration', () => {
|
|
53
|
+
showSnackbar({ duration: 'extended' });
|
|
54
|
+
|
|
55
|
+
advanceTimers(5999);
|
|
56
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
57
|
+
|
|
58
|
+
advanceTimers(1);
|
|
59
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
60
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('retains an explicitly configured legacy timeout during migration', () => {
|
|
64
|
+
showSnackbar({ timeout: 1000 });
|
|
65
|
+
|
|
66
|
+
advanceTimers(999);
|
|
67
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
68
|
+
|
|
69
|
+
advanceTimers(1);
|
|
70
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
71
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('uses semantic duration instead of the deprecated timeout when both are provided', () => {
|
|
75
|
+
showSnackbar({ duration: 'default', timeout: 1 });
|
|
76
|
+
|
|
77
|
+
advanceTimers(3999);
|
|
78
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
79
|
+
|
|
80
|
+
advanceTimers(1);
|
|
81
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
82
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('does not auto-dismiss an indefinite Snackbar', () => {
|
|
86
|
+
showSnackbar({ duration: 'indefinite' });
|
|
87
|
+
|
|
88
|
+
advanceTimers(60_000);
|
|
89
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('does not auto-dismiss a loading Snackbar', () => {
|
|
93
|
+
showSnackbar({ duration: 'extended', loading: true });
|
|
94
|
+
|
|
95
|
+
advanceTimers(60_000);
|
|
96
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('restarts the duration when direct Snackbar text changes', () => {
|
|
100
|
+
const view = showSnackbar();
|
|
101
|
+
|
|
102
|
+
advanceTimers(2000);
|
|
103
|
+
view.rerender(<Snackbar text="Updated changes saved" />);
|
|
104
|
+
|
|
105
|
+
advanceTimers(3999);
|
|
106
|
+
expect(screen.getByText('Updated changes saved')).toBeInTheDocument();
|
|
107
|
+
|
|
108
|
+
advanceTimers(1);
|
|
109
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
110
|
+
expect(screen.queryByText('Updated changes saved')).not.toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('reactivates a direct Snackbar when text changes after dismissal', async () => {
|
|
114
|
+
const view = showSnackbar({ duration: 'indefinite' });
|
|
115
|
+
|
|
116
|
+
await user.click(screen.getByRole('button', { name: 'Close' }));
|
|
117
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
118
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
119
|
+
|
|
120
|
+
view.rerender(<Snackbar text="Updated changes saved" duration="indefinite" />);
|
|
121
|
+
|
|
122
|
+
expect(screen.getByText('Updated changes saved')).toBeInTheDocument();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('pauses and resumes while the Snackbar is hovered', async () => {
|
|
126
|
+
showSnackbar();
|
|
127
|
+
const snackbar = screen.getByText('Changes saved').closest('.wds-Snackbar-container');
|
|
128
|
+
|
|
129
|
+
if (!snackbar) {
|
|
130
|
+
throw new Error('Expected the Snackbar surface to be rendered');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
advanceTimers(2000);
|
|
134
|
+
await user.hover(snackbar);
|
|
135
|
+
advanceTimers(10_000);
|
|
136
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
137
|
+
|
|
138
|
+
await user.unhover(snackbar);
|
|
139
|
+
advanceTimers(1999);
|
|
140
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
141
|
+
|
|
142
|
+
advanceTimers(1);
|
|
143
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
144
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('pauses and resumes while keyboard focus is within the Snackbar', () => {
|
|
148
|
+
showSnackbar({ action: { label: 'Undo' } });
|
|
149
|
+
const action = screen.getByText('Undo').closest('button');
|
|
150
|
+
|
|
151
|
+
if (!action) {
|
|
152
|
+
throw new Error('Expected the Snackbar action to be rendered');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
advanceTimers(2000);
|
|
156
|
+
fireEvent.focus(action);
|
|
157
|
+
advanceTimers(10_000);
|
|
158
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
159
|
+
|
|
160
|
+
fireEvent.blur(action, { relatedTarget: null });
|
|
161
|
+
advanceTimers(1999);
|
|
162
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
163
|
+
|
|
164
|
+
advanceTimers(1);
|
|
165
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
166
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('stays paused until both hover and focus interruptions end', async () => {
|
|
170
|
+
showSnackbar({ action: { label: 'Undo' } });
|
|
171
|
+
const action = screen.getByRole('button', { name: 'Undo' });
|
|
172
|
+
const snackbar = action.closest('.wds-Snackbar-container');
|
|
173
|
+
|
|
174
|
+
if (!snackbar) {
|
|
175
|
+
throw new Error('Expected the Snackbar surface to be rendered');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
advanceTimers(2000);
|
|
179
|
+
await user.hover(snackbar);
|
|
180
|
+
fireEvent.focus(action);
|
|
181
|
+
await user.unhover(snackbar);
|
|
182
|
+
|
|
183
|
+
advanceTimers(10_000);
|
|
184
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
185
|
+
|
|
186
|
+
fireEvent.blur(action, { relatedTarget: null });
|
|
187
|
+
advanceTimers(1999);
|
|
188
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
189
|
+
|
|
190
|
+
advanceTimers(1);
|
|
191
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
192
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('Snackbar accessibility', () => {
|
|
197
|
+
beforeEach(() => {
|
|
198
|
+
jest.useFakeTimers();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
afterEach(() => {
|
|
202
|
+
jest.clearAllTimers();
|
|
203
|
+
jest.useRealTimers();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('announces every sentiment as a polite status message', () => {
|
|
207
|
+
showSnackbar({ sentiment: 'negative' });
|
|
208
|
+
|
|
209
|
+
const liveRegion = screen.getByRole('status');
|
|
210
|
+
|
|
211
|
+
expect(liveRegion).toHaveAttribute('aria-live', 'polite');
|
|
212
|
+
expect(liveRegion).toHaveAttribute('aria-atomic', 'true');
|
|
213
|
+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('delays live-region exposure until assistive technology can observe the update', () => {
|
|
217
|
+
showSnackbar({ duration: 'indefinite' });
|
|
218
|
+
const liveRegionContent = screen.getByRole('status').firstElementChild;
|
|
219
|
+
|
|
220
|
+
expect(liveRegionContent).toHaveAttribute('aria-hidden', 'true');
|
|
221
|
+
advanceTimers(WDS_LIVE_REGION_DELAY_MS - 1);
|
|
222
|
+
expect(liveRegionContent).toHaveAttribute('aria-hidden', 'true');
|
|
223
|
+
|
|
224
|
+
advanceTimers(1);
|
|
225
|
+
expect(liveRegionContent).not.toHaveAttribute('aria-hidden');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('re-announces changed text for a direct Snackbar', () => {
|
|
229
|
+
const view = showSnackbar({ duration: 'indefinite' });
|
|
230
|
+
enableLiveRegion();
|
|
231
|
+
|
|
232
|
+
view.rerender(<Snackbar text="Updated changes saved" duration="indefinite" />);
|
|
233
|
+
|
|
234
|
+
const liveRegionContent = screen.getByRole('status').firstElementChild;
|
|
235
|
+
expect(liveRegionContent).toHaveAttribute('aria-hidden', 'true');
|
|
236
|
+
|
|
237
|
+
enableLiveRegion();
|
|
238
|
+
expect(liveRegionContent).not.toHaveAttribute('aria-hidden');
|
|
239
|
+
expect(screen.getByRole('status')).toHaveTextContent('Updated changes saved');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('keeps interactive controls outside the delayed announcement subtree', () => {
|
|
243
|
+
showSnackbar({ action: { label: 'Undo' }, duration: 'indefinite' });
|
|
244
|
+
|
|
245
|
+
const liveRegionContent = screen.getByRole('status').firstElementChild;
|
|
246
|
+
const action = screen.queryByRole('button', { name: 'Undo' });
|
|
247
|
+
const dismiss = screen.queryByRole('button', { name: 'Close' });
|
|
248
|
+
expect(liveRegionContent).toHaveAttribute('aria-hidden', 'true');
|
|
249
|
+
expect(action).toBeInTheDocument();
|
|
250
|
+
expect(dismiss).toBeInTheDocument();
|
|
251
|
+
expect(action?.closest('[aria-hidden="true"]')).toBeNull();
|
|
252
|
+
expect(dismiss?.closest('[aria-hidden="true"]')).toBeNull();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('does not steal focus when it appears', () => {
|
|
256
|
+
const view = render(<button type="button">Save changes</button>);
|
|
257
|
+
const trigger = screen.getByRole('button', { name: 'Save changes' });
|
|
258
|
+
|
|
259
|
+
trigger.focus();
|
|
260
|
+
view.rerender(
|
|
261
|
+
<>
|
|
262
|
+
<button type="button">Save changes</button>
|
|
263
|
+
<Snackbar text="Changes saved" />
|
|
264
|
+
</>,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
expect(trigger).toHaveFocus();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('exposes the action with its visible label', () => {
|
|
271
|
+
showSnackbar({ action: { label: 'Undo' } });
|
|
272
|
+
enableLiveRegion();
|
|
273
|
+
|
|
274
|
+
expect(screen.getByRole('button', { name: 'Undo' })).toBeEnabled();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('allows the action to be triggered with the keyboard', async () => {
|
|
278
|
+
const onAction = jest.fn();
|
|
279
|
+
showSnackbar({ action: { label: 'Undo', onClick: onAction }, duration: 'indefinite' });
|
|
280
|
+
enableLiveRegion();
|
|
281
|
+
|
|
282
|
+
screen.getByRole('button', { name: 'Undo' }).focus();
|
|
283
|
+
await user.keyboard('{Enter}');
|
|
284
|
+
|
|
285
|
+
expect(onAction).toHaveBeenCalledTimes(1);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('dismisses after an action callback throws', async () => {
|
|
289
|
+
const onAction = jest.fn(() => {
|
|
290
|
+
throw new Error('Action failed');
|
|
291
|
+
});
|
|
292
|
+
const preventExpectedError = (event: ErrorEvent) => event.preventDefault();
|
|
293
|
+
showSnackbar({ action: { label: 'Undo', onClick: onAction }, duration: 'indefinite' });
|
|
294
|
+
enableLiveRegion();
|
|
295
|
+
|
|
296
|
+
window.addEventListener('error', preventExpectedError);
|
|
297
|
+
try {
|
|
298
|
+
await user.click(screen.getByRole('button', { name: 'Undo' }));
|
|
299
|
+
} finally {
|
|
300
|
+
window.removeEventListener('error', preventExpectedError);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
expect(onAction).toHaveBeenCalledTimes(1);
|
|
304
|
+
|
|
305
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
306
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('announces that Escape dismisses the Snackbar without moving focus', () => {
|
|
310
|
+
showSnackbar({ duration: 'indefinite' });
|
|
311
|
+
enableLiveRegion();
|
|
312
|
+
|
|
313
|
+
expect(screen.getByRole('status')).toHaveTextContent('Changes saved Press Escape to dismiss.');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('gives the desktop dismiss button an accessible name', () => {
|
|
317
|
+
showSnackbar();
|
|
318
|
+
enableLiveRegion();
|
|
319
|
+
|
|
320
|
+
expect(screen.getByRole('button', { name: 'Close' })).toBeEnabled();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('Snackbar media', () => {
|
|
325
|
+
it('uses the shared PrimitivePrompt structure', () => {
|
|
326
|
+
showSnackbar({ duration: 'indefinite' });
|
|
327
|
+
|
|
328
|
+
expect(screen.getByText('Changes saved').closest('.wds-prompt')).toHaveClass(
|
|
329
|
+
'wds-Snackbar-container',
|
|
330
|
+
);
|
|
331
|
+
expect(screen.getByRole('button', { name: 'Close' })).toHaveClass('wds-prompt__dismiss');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('renders custom media in the fixed-size media wrapper', () => {
|
|
335
|
+
showSnackbar({ duration: 'indefinite', media: <Card title="Card" /> });
|
|
336
|
+
|
|
337
|
+
expect(screen.getByTestId('card-icon').parentElement).toHaveClass('wds-Snackbar__media-asset');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('renders proposition media in the fixed-size media wrapper', () => {
|
|
341
|
+
showSnackbar({ duration: 'indefinite', sentiment: 'proposition' });
|
|
342
|
+
const propositionIcon = screen
|
|
343
|
+
.getByText('Changes saved')
|
|
344
|
+
.closest('.wds-prompt')
|
|
345
|
+
?.querySelector('.tw-icon-tags');
|
|
346
|
+
|
|
347
|
+
expect(propositionIcon?.parentElement).toHaveClass('wds-Snackbar__media-asset');
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe('Snackbar swipe dismissal', () => {
|
|
352
|
+
beforeEach(() => {
|
|
353
|
+
jest.useFakeTimers();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
afterEach(() => {
|
|
357
|
+
jest.clearAllTimers();
|
|
358
|
+
jest.useRealTimers();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const getSnackbar = () => {
|
|
362
|
+
const surface = screen.getByText('Changes saved').closest('.wds-Snackbar-container');
|
|
363
|
+
const container = surface?.closest('.wds-Snackbar');
|
|
364
|
+
|
|
365
|
+
if (!surface || !container) {
|
|
366
|
+
throw new Error('Expected the Snackbar to be rendered');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { container, surface };
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const setElementHeight = (element: Element, height = 100) => {
|
|
373
|
+
Object.defineProperty(element, 'offsetHeight', { configurable: true, value: height });
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const mockPointerCapture = (element: Element) => {
|
|
377
|
+
const capturedPointers = new Set<number>();
|
|
378
|
+
const setPointerCapture = jest.fn((pointerId: number) => capturedPointers.add(pointerId));
|
|
379
|
+
const releasePointerCapture = jest.fn((pointerId: number) =>
|
|
380
|
+
capturedPointers.delete(pointerId),
|
|
381
|
+
);
|
|
382
|
+
const hasPointerCapture = jest.fn((pointerId: number) => capturedPointers.has(pointerId));
|
|
383
|
+
|
|
384
|
+
Object.assign(element, { hasPointerCapture, releasePointerCapture, setPointerCapture });
|
|
385
|
+
|
|
386
|
+
return { releasePointerCapture, setPointerCapture };
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
it('dismisses after a downward swipe at half the Snackbar height', async () => {
|
|
390
|
+
showSnackbar({ duration: 'indefinite' });
|
|
391
|
+
const { container, surface } = getSnackbar();
|
|
392
|
+
setElementHeight(surface);
|
|
393
|
+
setElementHeight(container, 140);
|
|
394
|
+
|
|
395
|
+
await user.pointer([
|
|
396
|
+
{ keys: '[MouseLeft>]', target: surface, coords: { clientX: 0, clientY: 0 } },
|
|
397
|
+
{ coords: { clientX: 0, clientY: 50 } },
|
|
398
|
+
'[/MouseLeft]',
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
expect(container).toHaveClass('wds-Snackbar--swipe-dismiss');
|
|
402
|
+
expect(container).toHaveStyle({ transform: 'translateY(140px)' });
|
|
403
|
+
|
|
404
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
405
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('dismisses after a downward mouse drag above the threshold', async () => {
|
|
409
|
+
showSnackbar({ duration: 'indefinite' });
|
|
410
|
+
const { surface } = getSnackbar();
|
|
411
|
+
setElementHeight(surface);
|
|
412
|
+
|
|
413
|
+
await user.pointer([
|
|
414
|
+
{ keys: '[MouseLeft>]', target: surface, coords: { clientX: 0, clientY: 0 } },
|
|
415
|
+
{ coords: { clientX: 0, clientY: 51 } },
|
|
416
|
+
'[/MouseLeft]',
|
|
417
|
+
]);
|
|
418
|
+
|
|
419
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
420
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('captures the pointer until a drag released outside the Snackbar ends', async () => {
|
|
424
|
+
showSnackbar({ duration: 'indefinite' });
|
|
425
|
+
const { container, surface } = getSnackbar();
|
|
426
|
+
setElementHeight(surface);
|
|
427
|
+
const { releasePointerCapture, setPointerCapture } = mockPointerCapture(surface);
|
|
428
|
+
|
|
429
|
+
await user.pointer([
|
|
430
|
+
{ keys: '[MouseLeft>]', target: surface, coords: { clientX: 0, clientY: 0 } },
|
|
431
|
+
{ coords: { clientX: 0, clientY: 30 } },
|
|
432
|
+
]);
|
|
433
|
+
expect(container).toHaveStyle({ transform: 'translateY(30px)' });
|
|
434
|
+
expect(setPointerCapture).toHaveBeenCalledWith(1);
|
|
435
|
+
|
|
436
|
+
await user.pointer({ keys: '[/MouseLeft]' });
|
|
437
|
+
expect(container).toHaveStyle({ transform: '' });
|
|
438
|
+
expect(releasePointerCapture).toHaveBeenCalledWith(1);
|
|
439
|
+
|
|
440
|
+
await user.pointer({ target: surface, coords: { clientX: 0, clientY: 80 } });
|
|
441
|
+
expect(container).toHaveStyle({ transform: '' });
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('clears the swipe position when a Snackbar is reactivated during exit', async () => {
|
|
445
|
+
const view = render(<SnackbarPortal text="Changes saved" duration="indefinite" />);
|
|
446
|
+
const { container, surface } = getSnackbar();
|
|
447
|
+
setElementHeight(surface);
|
|
448
|
+
setElementHeight(container, 140);
|
|
449
|
+
|
|
450
|
+
await user.pointer([
|
|
451
|
+
{ keys: '[MouseLeft>]', target: surface, coords: { clientX: 0, clientY: 0 } },
|
|
452
|
+
{ coords: { clientX: 0, clientY: 50 } },
|
|
453
|
+
'[/MouseLeft]',
|
|
454
|
+
]);
|
|
455
|
+
expect(container).toHaveClass('wds-Snackbar--swipe-dismiss');
|
|
456
|
+
|
|
457
|
+
view.rerender(<SnackbarPortal text="" duration="indefinite" />);
|
|
458
|
+
view.rerender(<SnackbarPortal text="Replacement message" duration="indefinite" />);
|
|
459
|
+
|
|
460
|
+
const reactivatedContainer = screen.getByText('Replacement message').closest('.wds-Snackbar');
|
|
461
|
+
expect(reactivatedContainer).not.toHaveClass('wds-Snackbar--swipe-dismiss');
|
|
462
|
+
expect(reactivatedContainer).toHaveStyle({ transform: '' });
|
|
463
|
+
|
|
464
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
465
|
+
expect(screen.getByText('Replacement message')).toBeInTheDocument();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('does not trigger an action when a swipe starts on it', async () => {
|
|
469
|
+
const onAction = jest.fn();
|
|
470
|
+
showSnackbar({
|
|
471
|
+
action: { label: 'Undo', onClick: onAction },
|
|
472
|
+
duration: 'indefinite',
|
|
473
|
+
});
|
|
474
|
+
const { surface } = getSnackbar();
|
|
475
|
+
const action = screen.getByRole('button', { name: 'Undo' });
|
|
476
|
+
setElementHeight(surface);
|
|
477
|
+
|
|
478
|
+
await user.pointer([
|
|
479
|
+
{ keys: '[MouseLeft>]', target: action, coords: { clientX: 0, clientY: 0 } },
|
|
480
|
+
{ coords: { clientX: 0, clientY: 50 } },
|
|
481
|
+
'[/MouseLeft]',
|
|
482
|
+
]);
|
|
483
|
+
|
|
484
|
+
expect(onAction).not.toHaveBeenCalled();
|
|
485
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
486
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('restores after downward movement below the threshold', async () => {
|
|
490
|
+
showSnackbar({ duration: 'indefinite' });
|
|
491
|
+
const { container, surface } = getSnackbar();
|
|
492
|
+
setElementHeight(surface);
|
|
493
|
+
|
|
494
|
+
await user.pointer([
|
|
495
|
+
{ keys: '[MouseLeft>]', target: surface, coords: { clientX: 0, clientY: 0 } },
|
|
496
|
+
{ coords: { clientX: 0, clientY: 49 } },
|
|
497
|
+
'[/MouseLeft]',
|
|
498
|
+
]);
|
|
499
|
+
|
|
500
|
+
expect(container).toHaveStyle({ transform: '' });
|
|
501
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('ignores upward movement', async () => {
|
|
505
|
+
showSnackbar({ duration: 'indefinite' });
|
|
506
|
+
const { container, surface } = getSnackbar();
|
|
507
|
+
setElementHeight(surface);
|
|
508
|
+
|
|
509
|
+
await user.pointer([
|
|
510
|
+
{ keys: '[MouseLeft>]', target: surface, coords: { clientX: 0, clientY: 60 } },
|
|
511
|
+
{ coords: { clientX: 0, clientY: 0 } },
|
|
512
|
+
'[/MouseLeft]',
|
|
513
|
+
]);
|
|
514
|
+
|
|
515
|
+
expect(container).toHaveStyle({ transform: '' });
|
|
516
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('ignores predominantly horizontal movement', async () => {
|
|
520
|
+
showSnackbar({ duration: 'indefinite' });
|
|
521
|
+
const { container, surface } = getSnackbar();
|
|
522
|
+
setElementHeight(surface);
|
|
523
|
+
|
|
524
|
+
await user.pointer([
|
|
525
|
+
{ keys: '[MouseLeft>]', target: surface, coords: { clientX: 0, clientY: 0 } },
|
|
526
|
+
{ coords: { clientX: 80, clientY: 60 } },
|
|
527
|
+
'[/MouseLeft]',
|
|
528
|
+
]);
|
|
529
|
+
|
|
530
|
+
expect(container).toHaveStyle({ transform: '' });
|
|
531
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('restores its position when the gesture is cancelled', async () => {
|
|
535
|
+
showSnackbar({ duration: 'indefinite' });
|
|
536
|
+
const { container, surface } = getSnackbar();
|
|
537
|
+
setElementHeight(surface);
|
|
538
|
+
|
|
539
|
+
await user.pointer([
|
|
540
|
+
{ keys: '[MouseLeft>]', target: surface, coords: { clientX: 0, clientY: 0 } },
|
|
541
|
+
{ coords: { clientX: 0, clientY: 80 } },
|
|
542
|
+
]);
|
|
543
|
+
expect(container).toHaveStyle({ transform: 'translateY(80px)' });
|
|
544
|
+
|
|
545
|
+
const cancelEvent = new Event('pointercancel', { bubbles: true });
|
|
546
|
+
Object.defineProperty(cancelEvent, 'pointerId', { value: 1 });
|
|
547
|
+
fireEvent(surface, cancelEvent);
|
|
548
|
+
|
|
549
|
+
expect(container).toHaveStyle({ transform: '' });
|
|
550
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
describe('Snackbar dismissal', () => {
|
|
555
|
+
beforeEach(() => {
|
|
556
|
+
jest.useFakeTimers();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
afterEach(() => {
|
|
560
|
+
jest.clearAllTimers();
|
|
561
|
+
jest.useRealTimers();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('calls onDismiss after the exit transition completes', async () => {
|
|
565
|
+
const onDismiss = jest.fn();
|
|
566
|
+
showSnackbar({ duration: 'indefinite', onDismiss });
|
|
567
|
+
enableLiveRegion();
|
|
568
|
+
|
|
569
|
+
await user.click(screen.getByRole('button', { name: 'Close' }));
|
|
570
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
571
|
+
|
|
572
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
573
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('unmounts the fixed wrapper after the exit transition completes', async () => {
|
|
577
|
+
showSnackbar({ duration: 'indefinite' });
|
|
578
|
+
enableLiveRegion();
|
|
579
|
+
const snackbar = screen.getByRole('status');
|
|
580
|
+
|
|
581
|
+
await user.click(screen.getByRole('button', { name: 'Close' }));
|
|
582
|
+
expect(snackbar).toBeInTheDocument();
|
|
583
|
+
|
|
584
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
585
|
+
expect(snackbar).not.toBeInTheDocument();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('dismisses when Escape is pressed without moving focus', async () => {
|
|
589
|
+
const onDismiss = jest.fn();
|
|
590
|
+
const trigger = document.createElement('button');
|
|
591
|
+
document.body.appendChild(trigger);
|
|
592
|
+
trigger.focus();
|
|
593
|
+
showSnackbar({ duration: 'indefinite', onDismiss });
|
|
594
|
+
|
|
595
|
+
await user.keyboard('{Escape}');
|
|
596
|
+
|
|
597
|
+
expect(trigger).toHaveFocus();
|
|
598
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
599
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
600
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
601
|
+
trigger.remove();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('runs the action before dismissing the Snackbar', async () => {
|
|
605
|
+
const onAction = jest.fn();
|
|
606
|
+
const onDismiss = jest.fn();
|
|
607
|
+
showSnackbar({
|
|
608
|
+
action: { label: 'Undo', onClick: onAction },
|
|
609
|
+
duration: 'indefinite',
|
|
610
|
+
onDismiss,
|
|
611
|
+
});
|
|
612
|
+
enableLiveRegion();
|
|
613
|
+
|
|
614
|
+
await user.click(screen.getByRole('button', { name: 'Undo' }));
|
|
615
|
+
expect(onAction).toHaveBeenCalledTimes(1);
|
|
616
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
617
|
+
|
|
618
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
619
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
describe('SnackbarPortal compatibility', () => {
|
|
624
|
+
beforeEach(() => {
|
|
625
|
+
jest.useFakeTimers();
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
afterEach(() => {
|
|
629
|
+
jest.clearAllTimers();
|
|
630
|
+
jest.useRealTimers();
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('renders numeric text', () => {
|
|
634
|
+
render(<SnackbarPortal text={0} duration="indefinite" />);
|
|
635
|
+
|
|
636
|
+
expect(screen.getByRole('status')).toHaveTextContent('0');
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('preserves timestamp-based exit-before-enter replacement', () => {
|
|
640
|
+
const view = render(
|
|
641
|
+
<SnackbarPortal text="First message" timestamp={1} duration="indefinite" />,
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
view.rerender(
|
|
645
|
+
<SnackbarPortal text="Replacement message" timestamp={2} duration="indefinite" />,
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
expect(screen.getByText('First message')).toBeInTheDocument();
|
|
649
|
+
expect(screen.queryByText('Replacement message')).not.toBeInTheDocument();
|
|
650
|
+
|
|
651
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
652
|
+
expect(screen.queryByText('First message')).not.toBeInTheDocument();
|
|
653
|
+
expect(screen.getByText('Replacement message')).toBeInTheDocument();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('renders the first present message when its initial timestamp is unchanged', () => {
|
|
657
|
+
const view = render(<SnackbarPortal text="" timestamp={1} duration="indefinite" />);
|
|
658
|
+
|
|
659
|
+
view.rerender(<SnackbarPortal text="First message" timestamp={1} duration="indefinite" />);
|
|
660
|
+
|
|
661
|
+
expect(screen.getByText('First message')).toBeInTheDocument();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('updates changed props in place when timestamp is omitted', () => {
|
|
665
|
+
const view = render(<SnackbarPortal text="First message" duration="indefinite" />);
|
|
666
|
+
|
|
667
|
+
view.rerender(<SnackbarPortal text="Replacement message" duration="indefinite" />);
|
|
668
|
+
|
|
669
|
+
expect(screen.queryByText('First message')).not.toBeInTheDocument();
|
|
670
|
+
expect(screen.getByText('Replacement message')).toBeInTheDocument();
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('does not restart dismissal when an inline action is recreated without timestamp', () => {
|
|
674
|
+
const view = render(
|
|
675
|
+
<SnackbarPortal text="First message" action={{ label: 'Undo' }} timeout={1000} />,
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
advanceTimers(500);
|
|
679
|
+
view.rerender(
|
|
680
|
+
<SnackbarPortal text="First message" action={{ label: 'Undo' }} timeout={1000} />,
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
advanceTimers(500);
|
|
684
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
685
|
+
expect(screen.queryByText('First message')).not.toBeInTheDocument();
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('reactivates a timestamp-less Snackbar when text returns during its exit', () => {
|
|
689
|
+
const view = render(<SnackbarPortal text="First message" duration="indefinite" />);
|
|
690
|
+
|
|
691
|
+
view.rerender(<SnackbarPortal text="" duration="indefinite" />);
|
|
692
|
+
view.rerender(<SnackbarPortal text="Replacement message" duration="indefinite" />);
|
|
693
|
+
|
|
694
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
695
|
+
expect(screen.getByText('Replacement message')).toBeInTheDocument();
|
|
696
|
+
});
|
|
697
|
+
});
|