@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.
Files changed (218) hide show
  1. package/build/Calendar/DateHeader/DateHeader.js +6 -2
  2. package/build/Calendar/DateHeader/DateHeader.js.map +1 -1
  3. package/build/Calendar/DateHeader/DateHeader.mjs +7 -3
  4. package/build/Calendar/DateHeader/DateHeader.mjs.map +1 -1
  5. package/build/Carousel/Carousel.js +2 -2
  6. package/build/Carousel/Carousel.js.map +1 -1
  7. package/build/Carousel/Carousel.mjs +3 -3
  8. package/build/Carousel/Carousel.mjs.map +1 -1
  9. package/build/Inputs/SelectInput/OptionContent/SelectInputOptionContent.js +3 -4
  10. package/build/Inputs/SelectInput/OptionContent/SelectInputOptionContent.js.map +1 -1
  11. package/build/Inputs/SelectInput/OptionContent/SelectInputOptionContent.mjs +3 -4
  12. package/build/Inputs/SelectInput/OptionContent/SelectInputOptionContent.mjs.map +1 -1
  13. package/build/ListItem/Navigation/ListItemNavigation.js +1 -1
  14. package/build/ListItem/Navigation/ListItemNavigation.js.map +1 -1
  15. package/build/ListItem/Navigation/ListItemNavigation.mjs +2 -2
  16. package/build/ListItem/Navigation/ListItemNavigation.mjs.map +1 -1
  17. package/build/PhoneNumberInput/PhoneNumberInput.js +1 -2
  18. package/build/PhoneNumberInput/PhoneNumberInput.js.map +1 -1
  19. package/build/PhoneNumberInput/PhoneNumberInput.mjs +1 -2
  20. package/build/PhoneNumberInput/PhoneNumberInput.mjs.map +1 -1
  21. package/build/PromoCard/Indicator/PromoCardIndicator.js +1 -1
  22. package/build/PromoCard/Indicator/PromoCardIndicator.js.map +1 -1
  23. package/build/PromoCard/Indicator/PromoCardIndicator.mjs +2 -2
  24. package/build/PromoCard/Indicator/PromoCardIndicator.mjs.map +1 -1
  25. package/build/Snackbar/Snackbar.js +212 -108
  26. package/build/Snackbar/Snackbar.js.map +1 -1
  27. package/build/Snackbar/Snackbar.messages.js +14 -0
  28. package/build/Snackbar/Snackbar.messages.js.map +1 -0
  29. package/build/Snackbar/Snackbar.messages.mjs +10 -0
  30. package/build/Snackbar/Snackbar.messages.mjs.map +1 -0
  31. package/build/Snackbar/Snackbar.mjs +213 -107
  32. package/build/Snackbar/Snackbar.mjs.map +1 -1
  33. package/build/Snackbar/SnackbarContext.js.map +1 -1
  34. package/build/Snackbar/SnackbarContext.mjs.map +1 -1
  35. package/build/Snackbar/SnackbarPortal.js +92 -0
  36. package/build/Snackbar/SnackbarPortal.js.map +1 -0
  37. package/build/Snackbar/SnackbarPortal.mjs +89 -0
  38. package/build/Snackbar/SnackbarPortal.mjs.map +1 -0
  39. package/build/Snackbar/SnackbarProvider.js +21 -31
  40. package/build/Snackbar/SnackbarProvider.js.map +1 -1
  41. package/build/Snackbar/SnackbarProvider.mjs +22 -32
  42. package/build/Snackbar/SnackbarProvider.mjs.map +1 -1
  43. package/build/Snackbar/constants.js +14 -0
  44. package/build/Snackbar/constants.js.map +1 -0
  45. package/build/Snackbar/constants.mjs +8 -0
  46. package/build/Snackbar/constants.mjs.map +1 -0
  47. package/build/Snackbar/useSnackbarAutoDismiss.js +135 -0
  48. package/build/Snackbar/useSnackbarAutoDismiss.js.map +1 -0
  49. package/build/Snackbar/useSnackbarAutoDismiss.mjs +131 -0
  50. package/build/Snackbar/useSnackbarAutoDismiss.mjs.map +1 -0
  51. package/build/Snackbar/useSnackbarManager.js +164 -0
  52. package/build/Snackbar/useSnackbarManager.js.map +1 -0
  53. package/build/Snackbar/useSnackbarManager.mjs +160 -0
  54. package/build/Snackbar/useSnackbarManager.mjs.map +1 -0
  55. package/build/Snackbar/useSnackbarSwipeDismiss.js +114 -0
  56. package/build/Snackbar/useSnackbarSwipeDismiss.js.map +1 -0
  57. package/build/Snackbar/useSnackbarSwipeDismiss.mjs +110 -0
  58. package/build/Snackbar/useSnackbarSwipeDismiss.mjs.map +1 -0
  59. package/build/common/PropsValues/typography.js +0 -6
  60. package/build/common/PropsValues/typography.js.map +1 -1
  61. package/build/common/PropsValues/typography.mjs +0 -6
  62. package/build/common/PropsValues/typography.mjs.map +1 -1
  63. package/build/i18n/en.json +1 -0
  64. package/build/i18n/en.json.js +1 -0
  65. package/build/i18n/en.json.js.map +1 -1
  66. package/build/i18n/en.json.mjs +1 -0
  67. package/build/i18n/en.json.mjs.map +1 -1
  68. package/build/index.js +3 -1
  69. package/build/index.js.map +1 -1
  70. package/build/index.mjs +2 -1
  71. package/build/index.mjs.map +1 -1
  72. package/build/main.css +864 -1509
  73. package/build/styles/Button/Button.css +3 -0
  74. package/build/styles/DateInput/DateInput.css +0 -13
  75. package/build/styles/Inputs/Input/Input.css +24 -0
  76. package/build/styles/Inputs/TextArea/TextArea.css +24 -0
  77. package/build/styles/ListItem/ListItem.css +1 -1
  78. package/build/styles/PhoneNumberInput/PhoneNumberInput.css +0 -3
  79. package/build/styles/Popover/Popover.css +6 -0
  80. package/build/styles/PromoCard/PromoCard.css +3 -0
  81. package/build/styles/Snackbar/Snackbar.css +182 -61
  82. package/build/styles/_deprecated/Select/Select.css +6 -0
  83. package/build/styles/css/accordion.css +12 -0
  84. package/build/styles/css/button-groups.css +6 -0
  85. package/build/styles/css/buttons.css +2 -0
  86. package/build/styles/css/decision.css +12 -0
  87. package/build/styles/css/dropdowns.css +18 -2
  88. package/build/styles/css/droppable.css +6 -0
  89. package/build/styles/css/footer.css +12 -0
  90. package/build/styles/css/input-groups.css +42 -2
  91. package/build/styles/css/list-group.css +6 -0
  92. package/build/styles/css/navbar.css +18 -2
  93. package/build/styles/css/neptune-core.css +512 -4
  94. package/build/styles/css/neptune.css +640 -1426
  95. package/build/styles/css/popovers.css +18 -0
  96. package/build/styles/css/tooltip.css +6 -0
  97. package/build/styles/less/neptune-tokens.less +0 -8
  98. package/build/styles/main.css +864 -1509
  99. package/build/styles/props/neptune-tokens.css +0 -8
  100. package/build/styles/styles/less/accordion.css +12 -0
  101. package/build/styles/styles/less/button-groups.css +6 -0
  102. package/build/styles/styles/less/buttons.css +2 -0
  103. package/build/styles/styles/less/decision.css +12 -0
  104. package/build/styles/styles/less/dropdowns.css +18 -2
  105. package/build/styles/styles/less/droppable.css +6 -0
  106. package/build/styles/styles/less/footer.css +12 -0
  107. package/build/styles/styles/less/forms/bootstrap-forms.css +24 -2
  108. package/build/styles/styles/less/input-groups.css +42 -2
  109. package/build/styles/styles/less/list-group.css +6 -0
  110. package/build/styles/styles/less/navbar.css +18 -2
  111. package/build/styles/styles/less/neptune-core.css +512 -4
  112. package/build/styles/styles/less/neptune.css +640 -1426
  113. package/build/styles/styles/less/popovers.css +18 -0
  114. package/build/styles/styles/less/tooltip.css +6 -0
  115. package/build/types/Inputs/SelectInput/OptionContent/SelectInputOptionContent.d.ts +1 -2
  116. package/build/types/Inputs/SelectInput/OptionContent/SelectInputOptionContent.d.ts.map +1 -1
  117. package/build/types/PhoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  118. package/build/types/Snackbar/Snackbar.d.ts +41 -24
  119. package/build/types/Snackbar/Snackbar.d.ts.map +1 -1
  120. package/build/types/Snackbar/Snackbar.messages.d.ts +9 -0
  121. package/build/types/Snackbar/Snackbar.messages.d.ts.map +1 -0
  122. package/build/types/Snackbar/Snackbar.types.d.ts +21 -0
  123. package/build/types/Snackbar/Snackbar.types.d.ts.map +1 -0
  124. package/build/types/Snackbar/SnackbarContext.d.ts +2 -2
  125. package/build/types/Snackbar/SnackbarContext.d.ts.map +1 -1
  126. package/build/types/Snackbar/SnackbarPortal.d.ts +5 -0
  127. package/build/types/Snackbar/SnackbarPortal.d.ts.map +1 -0
  128. package/build/types/Snackbar/SnackbarProvider.d.ts +6 -0
  129. package/build/types/Snackbar/SnackbarProvider.d.ts.map +1 -1
  130. package/build/types/Snackbar/constants.d.ts +6 -0
  131. package/build/types/Snackbar/constants.d.ts.map +1 -0
  132. package/build/types/Snackbar/index.d.ts +3 -1
  133. package/build/types/Snackbar/index.d.ts.map +1 -1
  134. package/build/types/Snackbar/useSnackbar.d.ts +1 -1
  135. package/build/types/Snackbar/useSnackbar.d.ts.map +1 -1
  136. package/build/types/Snackbar/useSnackbarAutoDismiss.d.ts +18 -0
  137. package/build/types/Snackbar/useSnackbarAutoDismiss.d.ts.map +1 -0
  138. package/build/types/Snackbar/useSnackbarManager.d.ts +17 -0
  139. package/build/types/Snackbar/useSnackbarManager.d.ts.map +1 -0
  140. package/build/types/Snackbar/useSnackbarSwipeDismiss.d.ts +11 -0
  141. package/build/types/Snackbar/useSnackbarSwipeDismiss.d.ts.map +1 -0
  142. package/build/types/common/PropsValues/typography.d.ts +1 -13
  143. package/build/types/common/PropsValues/typography.d.ts.map +1 -1
  144. package/build/types/index.d.ts +2 -2
  145. package/build/types/index.d.ts.map +1 -1
  146. package/build/types/test-utils/index.d.ts +2 -0
  147. package/build/types/test-utils/index.d.ts.map +1 -1
  148. package/package.json +2 -2
  149. package/src/Button/Button.css +3 -0
  150. package/src/Button/Button.less +16 -0
  151. package/src/Button/_stories/Button.test.story.tsx +12 -12
  152. package/src/Calendar/DateHeader/DateHeader.tsx +3 -3
  153. package/src/Carousel/Carousel.tsx +3 -3
  154. package/src/DateInput/DateInput.css +0 -13
  155. package/src/DateInput/DateInput.less +2 -10
  156. package/src/DateInput/_stories/DateInput.test.story.tsx +13 -0
  157. package/src/Display/_stories/Display.story.tsx +6 -20
  158. package/src/ExpressiveMoneyInput/_stories/ExpressiveMoneyInput.rtl.test.story.tsx +1 -1
  159. package/src/InputWithDisplayFormat/_stories/InputWithDisplayFormat.test.story.tsx +0 -52
  160. package/src/Inputs/Input/Input.css +24 -0
  161. package/src/Inputs/SelectInput/OptionContent/SelectInputOptionContent.tsx +1 -4
  162. package/src/Inputs/SelectInput/SelectInput.test.tsx +0 -74
  163. package/src/Inputs/TextArea/TextArea.css +24 -0
  164. package/src/ListItem/ListItem.css +1 -1
  165. package/src/ListItem/ListItem.grid.less +4 -4
  166. package/src/ListItem/ListItem.less +1 -1
  167. package/src/ListItem/Navigation/ListItemNavigation.tsx +2 -2
  168. package/src/PhoneNumberInput/PhoneNumberInput.css +0 -3
  169. package/src/PhoneNumberInput/PhoneNumberInput.less +2 -10
  170. package/src/PhoneNumberInput/PhoneNumberInput.tsx +0 -1
  171. package/src/PhoneNumberInput/_stories/PhoneNumberInput.rtl.test.story.tsx +1 -1
  172. package/src/Popover/Popover.css +6 -0
  173. package/src/PromoCard/Indicator/PromoCardIndicator.tsx +2 -2
  174. package/src/PromoCard/PromoCard.css +3 -0
  175. package/src/PromoCard/PromoCard.less +5 -0
  176. package/src/Prompt/CriticalBanner/_stories/CriticalBanner.test.story.tsx +1 -1
  177. package/src/Snackbar/Snackbar.css +182 -61
  178. package/src/Snackbar/Snackbar.less +185 -56
  179. package/src/Snackbar/Snackbar.messages.ts +9 -0
  180. package/src/Snackbar/Snackbar.test.tsx +697 -0
  181. package/src/Snackbar/Snackbar.tsx +227 -96
  182. package/src/Snackbar/Snackbar.types.ts +26 -0
  183. package/src/Snackbar/SnackbarContext.ts +2 -2
  184. package/src/Snackbar/SnackbarPortal.tsx +110 -0
  185. package/src/Snackbar/SnackbarProvider.test.tsx +202 -0
  186. package/src/Snackbar/SnackbarProvider.tsx +25 -25
  187. package/src/Snackbar/_stories/Snackbar.accessibility.docs.mdx +73 -0
  188. package/src/Snackbar/_stories/Snackbar.rtl.test.story.tsx +1 -12
  189. package/src/Snackbar/_stories/Snackbar.story.tsx +447 -62
  190. package/src/Snackbar/_stories/Snackbar.test.story.tsx +91 -16
  191. package/src/Snackbar/constants.ts +5 -0
  192. package/src/Snackbar/index.ts +3 -1
  193. package/src/Snackbar/useSnackbarAutoDismiss.ts +155 -0
  194. package/src/Snackbar/useSnackbarManager.ts +181 -0
  195. package/src/Snackbar/useSnackbarSwipeDismiss.ts +129 -0
  196. package/src/_deprecated/Select/Select.css +6 -0
  197. package/src/common/PropsValues/typography.ts +1 -20
  198. package/src/i18n/en.json +1 -1
  199. package/src/index.ts +7 -2
  200. package/src/main.css +864 -1509
  201. package/src/styles/less/accordion.css +12 -0
  202. package/src/styles/less/button-groups.css +6 -0
  203. package/src/styles/less/buttons.css +2 -0
  204. package/src/styles/less/core/_typography.less +194 -26
  205. package/src/styles/less/decision.css +12 -0
  206. package/src/styles/less/dropdowns.css +18 -2
  207. package/src/styles/less/droppable.css +6 -0
  208. package/src/styles/less/footer.css +12 -0
  209. package/src/styles/less/forms/bootstrap-forms.css +24 -2
  210. package/src/styles/less/input-groups.css +42 -2
  211. package/src/styles/less/list-group.css +6 -0
  212. package/src/styles/less/navbar.css +18 -2
  213. package/src/styles/less/neptune-core.css +512 -4
  214. package/src/styles/less/neptune.bundle.less +0 -2
  215. package/src/styles/less/neptune.css +640 -1426
  216. package/src/styles/less/popovers.css +18 -0
  217. package/src/styles/less/tooltip.css +6 -0
  218. package/src/DateInput/_stories/DateInput.rtl.test.story.tsx +0 -36
@@ -0,0 +1,202 @@
1
+ import { renderHook } from '@testing-library/react';
2
+
3
+ import { act, mockMatchMedia, render, screen, userEvent } from '../test-utils';
4
+ import { resetLiveRegionAnnouncementQueue } from '../common/LiveRegion/LiveRegion';
5
+
6
+ import { SNACKBAR_EXIT_DURATION } from './constants';
7
+ import type { SnackbarCreationProps } from './Snackbar.types';
8
+ import SnackbarProvider from './SnackbarProvider';
9
+ import useSnackbar from './useSnackbar';
10
+ import useSnackbarManager from './useSnackbarManager';
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
+ type SnackbarTrigger = {
23
+ label: string;
24
+ snackbar: SnackbarCreationProps;
25
+ };
26
+
27
+ const SnackbarTriggers = ({ triggers }: { triggers: SnackbarTrigger[] }) => {
28
+ const showSnackbar = useSnackbar();
29
+
30
+ return (
31
+ <>
32
+ {triggers.map(({ label, snackbar }) => (
33
+ <button key={label} type="button" onClick={() => showSnackbar(snackbar)}>
34
+ {label}
35
+ </button>
36
+ ))}
37
+ </>
38
+ );
39
+ };
40
+
41
+ const BatchedSnackbarTrigger = () => {
42
+ const showSnackbar = useSnackbar();
43
+
44
+ return (
45
+ <button
46
+ type="button"
47
+ onClick={() => {
48
+ showSnackbar({ text: 'First message', duration: 'indefinite' });
49
+ showSnackbar({ text: 'Latest message', duration: 'indefinite' });
50
+ }}
51
+ >
52
+ Show latest
53
+ </button>
54
+ );
55
+ };
56
+
57
+ const renderProvider = (triggers: SnackbarTrigger[], timeout?: number) =>
58
+ render(
59
+ <SnackbarProvider timeout={timeout}>
60
+ <SnackbarTriggers triggers={triggers} />
61
+ </SnackbarProvider>,
62
+ );
63
+
64
+ describe('SnackbarProvider orchestration', () => {
65
+ beforeEach(() => {
66
+ jest.useFakeTimers();
67
+ resetLiveRegionAnnouncementQueue();
68
+ });
69
+
70
+ afterEach(() => {
71
+ jest.clearAllTimers();
72
+ jest.useRealTimers();
73
+ });
74
+
75
+ it('creates a Snackbar programmatically', async () => {
76
+ renderProvider([
77
+ {
78
+ label: 'Show confirmation',
79
+ snackbar: { text: 'Changes saved', sentiment: 'success' },
80
+ },
81
+ ]);
82
+
83
+ await user.click(screen.getByRole('button', { name: 'Show confirmation' }));
84
+
85
+ expect(screen.getByRole('status')).toHaveTextContent('Changes saved');
86
+ });
87
+
88
+ it('uses an explicit semantic duration instead of the legacy Provider timeout', async () => {
89
+ renderProvider(
90
+ [
91
+ {
92
+ label: 'Show confirmation',
93
+ snackbar: { text: 'Changes saved', duration: 'default' },
94
+ },
95
+ ],
96
+ 1,
97
+ );
98
+
99
+ await user.click(screen.getByRole('button', { name: 'Show confirmation' }));
100
+ advanceTimers(3999);
101
+ expect(screen.getByText('Changes saved')).toBeInTheDocument();
102
+
103
+ advanceTimers(1);
104
+ advanceTimers(SNACKBAR_EXIT_DURATION);
105
+ expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
106
+ });
107
+
108
+ it('retains an explicitly configured legacy Provider timeout', async () => {
109
+ renderProvider([{ label: 'Show confirmation', snackbar: { text: 'Changes saved' } }], 1000);
110
+
111
+ await user.click(screen.getByRole('button', { name: 'Show confirmation' }));
112
+ advanceTimers(999);
113
+ expect(screen.getByText('Changes saved')).toBeInTheDocument();
114
+
115
+ advanceTimers(1);
116
+ advanceTimers(SNACKBAR_EXIT_DURATION);
117
+ expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
118
+ });
119
+
120
+ it('exits the active Snackbar before showing the latest replacement', async () => {
121
+ renderProvider([
122
+ { label: 'Show first', snackbar: { text: 'First message', duration: 'indefinite' } },
123
+ { label: 'Show second', snackbar: { text: 'Second message', duration: 'indefinite' } },
124
+ { label: 'Show latest', snackbar: { text: 'Latest message', duration: 'indefinite' } },
125
+ ]);
126
+
127
+ await user.click(screen.getByRole('button', { name: 'Show first' }));
128
+ await user.click(screen.getByRole('button', { name: 'Show second' }));
129
+
130
+ expect(screen.getByText('First message')).toBeInTheDocument();
131
+ expect(screen.queryByText('Second message')).not.toBeInTheDocument();
132
+
133
+ await user.click(screen.getByRole('button', { name: 'Show latest' }));
134
+ advanceTimers(SNACKBAR_EXIT_DURATION);
135
+
136
+ expect(screen.queryByText('First message')).not.toBeInTheDocument();
137
+ expect(screen.queryByText('Second message')).not.toBeInTheDocument();
138
+ expect(screen.getByText('Latest message')).toBeInTheDocument();
139
+ });
140
+
141
+ it('shows the latest Snackbar when multiple creations are batched before the first render', async () => {
142
+ render(
143
+ <SnackbarProvider>
144
+ <BatchedSnackbarTrigger />
145
+ </SnackbarProvider>,
146
+ );
147
+
148
+ await user.click(screen.getByRole('button', { name: 'Show latest' }));
149
+
150
+ expect(screen.queryByText('First message')).not.toBeInTheDocument();
151
+ expect(screen.getByText('Latest message')).toBeInTheDocument();
152
+ });
153
+
154
+ it('calls onDismiss after the active Snackbar finishes exiting', async () => {
155
+ const onDismiss = jest.fn();
156
+ renderProvider([
157
+ {
158
+ label: 'Show first',
159
+ snackbar: { text: 'First message', duration: 'indefinite', onDismiss },
160
+ },
161
+ { label: 'Replace', snackbar: { text: 'Replacement', duration: 'indefinite' } },
162
+ ]);
163
+
164
+ await user.click(screen.getByRole('button', { name: 'Show first' }));
165
+ await user.click(screen.getByRole('button', { name: 'Replace' }));
166
+ advanceTimers(SNACKBAR_EXIT_DURATION - 1);
167
+ expect(onDismiss).not.toHaveBeenCalled();
168
+
169
+ advanceTimers(1);
170
+ expect(onDismiss).toHaveBeenCalledTimes(1);
171
+ });
172
+
173
+ it('promotes a pending Snackbar even when onDismiss throws', async () => {
174
+ const onDismiss = jest.fn(() => {
175
+ throw new Error('Dismiss callback failed');
176
+ });
177
+ const { result } = renderHook(() => useSnackbarManager());
178
+
179
+ await act(async () => result.current.showSnackbar({ text: 'First message', onDismiss }));
180
+ await act(async () => result.current.showSnackbar({ text: 'Replacement message' }));
181
+
182
+ await expect(act(async () => result.current.handleExited())).rejects.toThrow(
183
+ 'Dismiss callback failed',
184
+ );
185
+ await act(async () => undefined);
186
+ expect(result.current.active?.props.text).toBe('Replacement message');
187
+ expect(result.current.visible).toBe(true);
188
+ });
189
+
190
+ it('cleans up the active Snackbar timer when the Provider unmounts', async () => {
191
+ const view = renderProvider([
192
+ { label: 'Show confirmation', snackbar: { text: 'Changes saved' } },
193
+ ]);
194
+
195
+ await user.click(screen.getByRole('button', { name: 'Show confirmation' }));
196
+ advanceTimers(300);
197
+ expect(jest.getTimerCount()).toBe(1);
198
+
199
+ view.unmount();
200
+ expect(jest.getTimerCount()).toBe(0);
201
+ });
202
+ });
@@ -1,38 +1,38 @@
1
- import { useMemo, useState } from 'react';
1
+ import { useMemo } from 'react';
2
2
 
3
- import SnackbarPortal, { SnackbarProps } from './Snackbar';
4
3
  import { SnackbarContext } from './SnackbarContext';
4
+ import { PortalledSnackbar } from './SnackbarPortal';
5
+ import useSnackbarManager from './useSnackbarManager';
5
6
 
6
7
  export interface SnackbarProviderProps {
8
+ /**
9
+ * @deprecated Configure each Snackbar with semantic `duration` instead. Existing millisecond
10
+ * values remain supported temporarily while consumers migrate. A Snackbar's `duration` takes
11
+ * precedence when both are provided.
12
+ */
7
13
  timeout?: number;
14
+ /** Application content that can create Snackbars through the Provider context. */
8
15
  children?: React.ReactNode;
9
16
  }
10
17
 
11
- export default function SnackbarProvider({ timeout = 4500, children }: SnackbarProviderProps) {
12
- const [state, setState] = useState<Omit<SnackbarProps, 'timeout'>>({
13
- text: '',
14
- timestamp: 0,
15
- });
16
- const { action, text, theme, timestamp } = state;
18
+ export default function SnackbarProvider({ timeout, children }: SnackbarProviderProps) {
19
+ const { active, announcementKey, dismissSnackbar, handleExited, showSnackbar, visible } =
20
+ useSnackbarManager();
21
+ const contextValue = useMemo(() => ({ createSnackbar: showSnackbar }), [showSnackbar]);
17
22
 
18
23
  return (
19
- <SnackbarContext.Provider
20
- value={useMemo(
21
- () => ({
22
- createSnackbar: ({ action, text, theme }) => {
23
- setState({ action, text, theme, timestamp: Date.now() });
24
- },
25
- }),
26
- [],
27
- )}
28
- >
29
- <SnackbarPortal
30
- action={action}
31
- text={text}
32
- timestamp={timestamp}
33
- timeout={timeout}
34
- theme={theme}
35
- />
24
+ <SnackbarContext.Provider value={contextValue}>
25
+ {active ? (
26
+ <PortalledSnackbar
27
+ key={active.id}
28
+ {...active.props}
29
+ announcementKey={announcementKey}
30
+ timeout={timeout}
31
+ visible={visible}
32
+ onDismissRequest={dismissSnackbar}
33
+ onExited={handleExited}
34
+ />
35
+ ) : null}
36
36
  {children}
37
37
  </SnackbarContext.Provider>
38
38
  );
@@ -0,0 +1,73 @@
1
+ import { Meta, Source } from '@storybook/addon-docs/blocks';
2
+
3
+ <Meta title="Prompts/Snackbar/Accessibility" />
4
+
5
+ # Accessibility
6
+
7
+ Snackbar is a polite live region. It uses `role="status"` and `aria-live="polite"`, so its message
8
+ is announced when the screen reader is idle without interrupting the current task. It does not move
9
+ keyboard focus when it appears.
10
+
11
+ Visual sentiment and announcement urgency are separate. A negative Snackbar remains polite; use a
12
+ different component when an update is urgent enough to interrupt the user.
13
+
14
+ <Source
15
+ dark
16
+ code={`showSnackbar({
17
+ text: 'Transfer failed',
18
+ sentiment: 'negative',
19
+ });`}
20
+ />
21
+
22
+ ## Actions and dismissal
23
+
24
+ The optional action uses its visible label as its accessible name. Keep the label concise and make
25
+ the outcome clear. Timed Snackbars pause while the pointer is hovering over them or while keyboard
26
+ focus is inside.
27
+
28
+ The polite announcement tells screen-reader users that pressing `Escape` dismisses the Snackbar
29
+ without moving focus. On desktop, the close button also has an accessible name and dismisses through
30
+ the normal exit motion. On mobile, the Snackbar can be dismissed with a downward swipe. A swipe
31
+ below 50% of its rendered height, an upward or horizontal gesture, or a cancelled gesture returns it
32
+ to its resting position.
33
+
34
+ <Source
35
+ dark
36
+ code={`showSnackbar({
37
+ text: 'Transfer deleted',
38
+ action: {
39
+ label: 'Undo',
40
+ onClick: handleUndo,
41
+ },
42
+ duration: 'extended',
43
+ onDismiss: handleDismiss,
44
+ });`}
45
+ />
46
+
47
+ ## Media
48
+
49
+ Default sentiment media already includes the appropriate accessible treatment. If custom media
50
+ conveys meaning, add its accessibility attributes directly to the element you pass. Decorative
51
+ custom media should be hidden from assistive technology.
52
+
53
+ <Source
54
+ dark
55
+ code={`showSnackbar({
56
+ text: 'Added to your wallet',
57
+ media: <Card title="Card" />,
58
+ });`}
59
+ />
60
+
61
+ <Source
62
+ dark
63
+ code={`showSnackbar({
64
+ text: 'Offer applied',
65
+ media: <GiftBox aria-hidden />,
66
+ });`}
67
+ />
68
+
69
+ ## Motion and zoom
70
+
71
+ Snackbar removes movement and scaling when `prefers-reduced-motion: reduce` is enabled. Its content
72
+ reflows at high zoom and narrow viewport widths without horizontal scrolling. Keep messages short
73
+ and plain so they remain easy to read when text wraps.
@@ -1,4 +1,3 @@
1
- import { useEffect, useState } from 'react';
2
1
  import type { Meta, StoryObj } from '@storybook/react-webpack5';
3
2
 
4
3
  import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
@@ -12,21 +11,11 @@ export default {
12
11
 
13
12
  type Story = StoryObj<typeof Snackbar>;
14
13
 
15
- const TIMEOUT = 10_000_000;
16
-
17
14
  function SnackbarStory({ action }: { action?: { label: string } }) {
18
- const [timestamp, setTimestamp] = useState(0);
19
-
20
- useEffect(() => {
21
- // eslint-disable-next-line react-hooks/set-state-in-effect
22
- setTimestamp(1);
23
- }, []);
24
-
25
15
  return (
26
16
  <Snackbar
27
17
  text={<span>Transfer completed successfully</span>}
28
- timestamp={timestamp}
29
- timeout={TIMEOUT}
18
+ duration="indefinite"
30
19
  action={action}
31
20
  />
32
21
  );