@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
@@ -1,17 +1,18 @@
1
- import { userEvent, within, expect } from 'storybook/test';
1
+ import { userEvent, within, expect, waitFor } from 'storybook/test';
2
2
  import { action } from 'storybook/actions';
3
+ import { Card } from '@transferwise/icons';
3
4
  import { withVariantConfig } from '../../../.storybook/helpers';
4
5
  import { lorem20 } from '../../test-utils';
5
6
  import { wait } from '../../test-utils/wait';
6
7
  import Button from '../../Button';
7
- import { Snackbar, type SnackbarProps } from '../Snackbar';
8
+ import { Snackbar } from '../Snackbar';
8
9
  import { SnackbarConsumer } from '../SnackbarContext';
9
10
  import SnackbarProvider from '../SnackbarProvider';
11
+ import type { SnackbarProps } from '../Snackbar.types';
10
12
 
11
13
  export default {
12
14
  title: 'Prompts/Snackbar/Tests',
13
15
  component: Snackbar,
14
- argTypes: {},
15
16
  tags: ['!autodocs', '!manifest'],
16
17
  };
17
18
 
@@ -23,21 +24,18 @@ const launchSnackbar = async (canvasElement: HTMLElement) => {
23
24
 
24
25
  type Args = Partial<SnackbarProps> & {
25
26
  extraContext: React.ReactNode;
26
- timeout: number;
27
27
  };
28
28
 
29
- const TIMEOUT = 10000000;
30
-
31
29
  const Default = {
32
30
  args: {
33
- timeout: TIMEOUT,
31
+ duration: 'indefinite',
34
32
  } as Args,
35
33
  play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
36
34
  await launchSnackbar(canvasElement);
37
35
  },
38
36
  render: (args: Args) => {
39
37
  return (
40
- <SnackbarProvider timeout={args.timeout}>
38
+ <SnackbarProvider>
41
39
  <SnackbarConsumer>
42
40
  {({ createSnackbar }) => (
43
41
  <>
@@ -46,11 +44,18 @@ const Default = {
46
44
  block
47
45
  onClick={() => {
48
46
  createSnackbar({
49
- action: {
50
- label: 'View',
51
- onClick: () => action('View button clicked.'),
52
- },
53
- text: <span>Some text</span>,
47
+ action:
48
+ args.action ??
49
+ ({
50
+ label: 'View',
51
+ onClick: () => action('View button clicked.'),
52
+ } as const),
53
+ duration: args.duration,
54
+ loading: args.loading,
55
+ media: args.media,
56
+ onDismiss: args.onDismiss,
57
+ sentiment: args.sentiment,
58
+ text: args.text ?? <span>Some text</span>,
54
59
  });
55
60
  }}
56
61
  >
@@ -72,10 +77,60 @@ export const Basic = {
72
77
  },
73
78
  };
74
79
 
80
+ export const CustomMedia = {
81
+ ...Default,
82
+ args: {
83
+ ...Default.args,
84
+ media: <Card title="Card" />,
85
+ sentiment: 'success',
86
+ },
87
+ };
88
+
89
+ export const Loading = {
90
+ ...Default,
91
+ args: {
92
+ ...Default.args,
93
+ loading: true,
94
+ sentiment: 'negative',
95
+ },
96
+ };
97
+
98
+ export const Proposition = {
99
+ ...Default,
100
+ args: {
101
+ ...Default.args,
102
+ sentiment: 'proposition',
103
+ loading: false,
104
+ media: null,
105
+ },
106
+ };
107
+
108
+ export const NavigationAction = {
109
+ ...Default,
110
+ args: {
111
+ ...Default.args,
112
+ action: {
113
+ label: 'View transfer',
114
+ href: '/transfers/123',
115
+ target: '_blank',
116
+ },
117
+ },
118
+ play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
119
+ await launchSnackbar(canvasElement);
120
+
121
+ const link = await waitFor(() =>
122
+ within(document.body).getByRole('link', { name: 'View transfer' }),
123
+ );
124
+ await expect(link).toHaveAttribute('href', '/transfers/123');
125
+ await expect(link).toHaveAttribute('target', '_blank');
126
+ },
127
+ };
128
+
75
129
  export const Mobile = {
76
130
  ...Default,
77
131
  args: {
78
132
  ...Default.args,
133
+ media: null,
79
134
  },
80
135
  ...withVariantConfig(['mobile'], Default),
81
136
  };
@@ -88,9 +143,9 @@ const switchToDarkMode = async (context: { canvasElement: HTMLElement }) => {
88
143
  if (body) {
89
144
  body.setAttribute('class', 'np-theme-personal--dark');
90
145
  }
91
- const snackbar = document.querySelector('div.snackbar');
146
+ const snackbar = document.querySelector('div.wds-Snackbar');
92
147
  if (snackbar) {
93
- snackbar.setAttribute('class', 'snackbar np-theme-personal--dark');
148
+ snackbar.setAttribute('class', 'wds-Snackbar np-theme-personal--dark');
94
149
  }
95
150
  };
96
151
 
@@ -98,6 +153,7 @@ export const DarkMode = {
98
153
  ...Default,
99
154
  args: {
100
155
  ...Default.args,
156
+ sentiment: 'warning',
101
157
  },
102
158
  play: switchToDarkMode,
103
159
  ...withVariantConfig(['dark'], Default),
@@ -107,6 +163,14 @@ export const Zoom400 = {
107
163
  ...Default,
108
164
  args: {
109
165
  ...Default.args,
166
+ action: {
167
+ label: 'View details',
168
+ onClick: () => action('View details button clicked.'),
169
+ },
170
+ sentiment: 'success',
171
+ text: (
172
+ <span>Your transfer has been completed successfully and is ready for you to review.</span>
173
+ ),
110
174
  extraContext: (
111
175
  <div className="p-t-3">
112
176
  Snackbar isn&#39;t transparent (I&#39;m going to make this text very long so that it will
@@ -120,6 +184,17 @@ export const Zoom400 = {
120
184
  ...withVariantConfig(['400%'], Default),
121
185
  };
122
186
 
187
+ export const Zoom400ShortMessage = {
188
+ ...Default,
189
+ args: {
190
+ ...Default.args,
191
+ media: <Card title="Card" />,
192
+ sentiment: 'success',
193
+ text: <span>Some text</span>,
194
+ },
195
+ ...withVariantConfig(['400%'], Default),
196
+ };
197
+
123
198
  /**
124
199
  * Snackbar is only shown once even if trigger is clicked many times
125
200
  */
@@ -134,6 +209,6 @@ export const MultipleClicks = {
134
209
  await userEvent.click(canvas.getByRole('button'));
135
210
  }
136
211
 
137
- await expect(within(document.body).getAllByRole('alert')).toHaveLength(1);
212
+ await expect(within(document.body).getAllByRole('status')).toHaveLength(1);
138
213
  },
139
214
  };
@@ -0,0 +1,5 @@
1
+ export const SNACKBAR_ENTER_DURATION = 300;
2
+ export const SNACKBAR_EXIT_DURATION = 150;
3
+ export const SNACKBAR_DEFAULT_DURATION = 4000;
4
+ export const SNACKBAR_EXTENDED_DURATION = 6000;
5
+ export const SWIPE_DISMISS_THRESHOLD_RATIO = 0.5;
@@ -1,4 +1,6 @@
1
- export { default } from './Snackbar';
1
+ export { Snackbar } from './Snackbar';
2
+ export { default, SnackbarPortal } from './SnackbarPortal';
2
3
 
3
4
  export type { SnackbarProps } from './Snackbar';
5
+ export type { SnackbarAction, SnackbarDuration } from './Snackbar.types';
4
6
  export type { SnackbarProviderProps } from './SnackbarProvider';
@@ -0,0 +1,155 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+
3
+ import { SNACKBAR_DEFAULT_DURATION, SNACKBAR_EXTENDED_DURATION } from './constants';
4
+ import type { SnackbarDuration } from './Snackbar.types';
5
+
6
+ type UseSnackbarAutoDismissOptions = {
7
+ duration?: SnackbarDuration;
8
+ loading?: boolean;
9
+ onDismissRequest: () => void;
10
+ resetKey: string | number;
11
+ timeout?: number;
12
+ visible: boolean;
13
+ };
14
+
15
+ type InteractionState = {
16
+ focusWithin: boolean;
17
+ hovered: boolean;
18
+ };
19
+
20
+ const getAutoDismissDuration = ({
21
+ duration,
22
+ loading,
23
+ timeout,
24
+ }: Pick<UseSnackbarAutoDismissOptions, 'duration' | 'loading' | 'timeout'>) => {
25
+ if (loading || duration === 'indefinite') {
26
+ return null;
27
+ }
28
+
29
+ if (duration === 'extended') {
30
+ return SNACKBAR_EXTENDED_DURATION;
31
+ }
32
+
33
+ if (duration === 'default') {
34
+ return SNACKBAR_DEFAULT_DURATION;
35
+ }
36
+
37
+ return timeout ?? SNACKBAR_DEFAULT_DURATION;
38
+ };
39
+
40
+ const useSnackbarAutoDismiss = ({
41
+ duration,
42
+ loading,
43
+ onDismissRequest,
44
+ resetKey,
45
+ timeout,
46
+ visible,
47
+ }: UseSnackbarAutoDismissOptions) => {
48
+ const surfaceRef = useRef<HTMLDivElement>(null);
49
+ const timeoutRef = useRef(0);
50
+ const remainingDurationRef = useRef<number | null>(null);
51
+ const timerStartedAtRef = useRef(0);
52
+ const interactionStateRef = useRef<InteractionState>({ focusWithin: false, hovered: false });
53
+
54
+ const isInterrupted = useCallback(() => {
55
+ const { focusWithin, hovered } = interactionStateRef.current;
56
+ return focusWithin || hovered;
57
+ }, []);
58
+
59
+ const clearTimer = useCallback(() => {
60
+ window.clearTimeout(timeoutRef.current);
61
+ timeoutRef.current = 0;
62
+ }, []);
63
+
64
+ const startTimer = useCallback(
65
+ (remainingDuration = getAutoDismissDuration({ duration, loading, timeout })) => {
66
+ clearTimer();
67
+ remainingDurationRef.current = remainingDuration;
68
+
69
+ if (!visible || remainingDuration === null || isInterrupted()) {
70
+ return;
71
+ }
72
+
73
+ timerStartedAtRef.current = Date.now();
74
+ timeoutRef.current = window.setTimeout(() => {
75
+ timeoutRef.current = 0;
76
+ remainingDurationRef.current = 0;
77
+ onDismissRequest();
78
+ }, remainingDuration);
79
+ },
80
+ [clearTimer, duration, isInterrupted, loading, onDismissRequest, timeout, visible],
81
+ );
82
+
83
+ const pauseTimer = useCallback(() => {
84
+ if (!timeoutRef.current || remainingDurationRef.current === null) {
85
+ return;
86
+ }
87
+
88
+ clearTimer();
89
+ remainingDurationRef.current = Math.max(
90
+ 0,
91
+ remainingDurationRef.current - (Date.now() - timerStartedAtRef.current),
92
+ );
93
+ }, [clearTimer]);
94
+
95
+ const resumeTimer = useCallback(() => {
96
+ if (isInterrupted() || remainingDurationRef.current === null) {
97
+ return;
98
+ }
99
+
100
+ startTimer(remainingDurationRef.current);
101
+ }, [isInterrupted, startTimer]);
102
+
103
+ const handleMouseEnter = useCallback(() => {
104
+ interactionStateRef.current = { ...interactionStateRef.current, hovered: true };
105
+ pauseTimer();
106
+ }, [pauseTimer]);
107
+
108
+ const handleMouseLeave = useCallback(() => {
109
+ interactionStateRef.current = { ...interactionStateRef.current, hovered: false };
110
+ resumeTimer();
111
+ }, [resumeTimer]);
112
+
113
+ const handleFocus = useCallback(() => {
114
+ interactionStateRef.current = { ...interactionStateRef.current, focusWithin: true };
115
+ pauseTimer();
116
+ }, [pauseTimer]);
117
+
118
+ const handleBlur = useCallback(
119
+ (event: React.FocusEvent<HTMLDivElement>) => {
120
+ if (event.currentTarget.contains(event.relatedTarget)) {
121
+ return;
122
+ }
123
+
124
+ interactionStateRef.current = { ...interactionStateRef.current, focusWithin: false };
125
+ resumeTimer();
126
+ },
127
+ [resumeTimer],
128
+ );
129
+
130
+ useEffect(() => {
131
+ if (!visible) {
132
+ clearTimer();
133
+ return undefined;
134
+ }
135
+
136
+ const snackbar = surfaceRef.current;
137
+ interactionStateRef.current = {
138
+ focusWithin: Boolean(snackbar?.contains(document.activeElement)),
139
+ hovered: snackbar?.matches(':hover') ?? false,
140
+ };
141
+ startTimer();
142
+
143
+ return clearTimer;
144
+ }, [clearTimer, resetKey, startTimer, visible]);
145
+
146
+ return {
147
+ handleBlur,
148
+ handleFocus,
149
+ handleMouseEnter,
150
+ handleMouseLeave,
151
+ surfaceRef,
152
+ };
153
+ };
154
+
155
+ export default useSnackbarAutoDismiss;
@@ -0,0 +1,181 @@
1
+ import { useCallback, useEffect, useReducer, useRef } from 'react';
2
+
3
+ import type { SnackbarProps } from './Snackbar.types';
4
+
5
+ type SnackbarInstance = {
6
+ id: number;
7
+ props: SnackbarProps;
8
+ };
9
+
10
+ type SnackbarManagerState = {
11
+ active: SnackbarInstance | null;
12
+ activeHasRendered: boolean;
13
+ announcementKey: number;
14
+ pending: SnackbarInstance | null;
15
+ visible: boolean;
16
+ };
17
+
18
+ type SnackbarManagerAction =
19
+ | { type: 'show'; instance: SnackbarInstance }
20
+ | { type: 'update'; instance: SnackbarInstance; reactivate: boolean }
21
+ | { type: 'rendered'; instanceId: number }
22
+ | { type: 'dismiss' }
23
+ | { type: 'exited'; instanceId: number };
24
+
25
+ const emptyState: SnackbarManagerState = {
26
+ active: null,
27
+ activeHasRendered: false,
28
+ announcementKey: 0,
29
+ pending: null,
30
+ visible: false,
31
+ };
32
+
33
+ const createInitialState = (initialSnackbar?: SnackbarProps): SnackbarManagerState =>
34
+ initialSnackbar
35
+ ? {
36
+ active: { id: 1, props: initialSnackbar },
37
+ activeHasRendered: false,
38
+ announcementKey: 1,
39
+ pending: null,
40
+ visible: true,
41
+ }
42
+ : emptyState;
43
+
44
+ const snackbarManagerReducer = (
45
+ state: SnackbarManagerState,
46
+ action: SnackbarManagerAction,
47
+ ): SnackbarManagerState => {
48
+ switch (action.type) {
49
+ case 'show':
50
+ if (!state.active || !state.activeHasRendered) {
51
+ // Nothing has rendered yet, so consecutive creates from the same React batch collapse to
52
+ // the latest Snackbar instead of trying to animate out an instance that never entered.
53
+ return {
54
+ active: action.instance,
55
+ activeHasRendered: false,
56
+ announcementKey: state.announcementKey + 1,
57
+ pending: null,
58
+ visible: true,
59
+ };
60
+ }
61
+
62
+ // Latest wins: replace any pending Snackbar while the active one completes its exit.
63
+ return {
64
+ ...state,
65
+ pending: action.instance,
66
+ visible: false,
67
+ };
68
+ case 'rendered':
69
+ if (state.active?.id !== action.instanceId || state.activeHasRendered) {
70
+ return state;
71
+ }
72
+
73
+ return { ...state, activeHasRendered: true };
74
+ case 'update':
75
+ if (!state.active) {
76
+ return {
77
+ active: action.instance,
78
+ activeHasRendered: false,
79
+ announcementKey: state.announcementKey + 1,
80
+ pending: null,
81
+ visible: true,
82
+ };
83
+ }
84
+
85
+ return {
86
+ ...state,
87
+ active: { ...state.active, props: action.instance.props },
88
+ pending: action.reactivate ? null : state.pending,
89
+ visible: action.reactivate ? true : state.visible,
90
+ announcementKey:
91
+ state.active.props.text === action.instance.props.text
92
+ ? state.announcementKey
93
+ : state.announcementKey + 1,
94
+ };
95
+ case 'dismiss':
96
+ if (!state.active) {
97
+ return state;
98
+ }
99
+
100
+ return state.activeHasRendered
101
+ ? { ...state, visible: false }
102
+ : { ...emptyState, announcementKey: state.announcementKey };
103
+ case 'exited':
104
+ if (state.active?.id !== action.instanceId) {
105
+ return state;
106
+ }
107
+
108
+ if (state.pending) {
109
+ return {
110
+ active: state.pending,
111
+ activeHasRendered: false,
112
+ announcementKey: state.announcementKey + 1,
113
+ pending: null,
114
+ visible: true,
115
+ };
116
+ }
117
+
118
+ return { ...emptyState, announcementKey: state.announcementKey };
119
+ default:
120
+ return state;
121
+ }
122
+ };
123
+
124
+ /** Owns the shared latest-wins and exit-before-enter lifecycle used by Snackbar portals. */
125
+ const useSnackbarManager = (initialSnackbar?: SnackbarProps) => {
126
+ const [state, dispatch] = useReducer(snackbarManagerReducer, initialSnackbar, createInitialState);
127
+ const nextInstanceId = useRef(initialSnackbar ? 1 : 0);
128
+
129
+ const createInstance = useCallback((props: SnackbarProps): SnackbarInstance => {
130
+ nextInstanceId.current += 1;
131
+ return { id: nextInstanceId.current, props };
132
+ }, []);
133
+
134
+ const showSnackbar = useCallback(
135
+ (props: SnackbarProps) => dispatch({ type: 'show', instance: createInstance(props) }),
136
+ [createInstance],
137
+ );
138
+
139
+ const updateSnackbar = useCallback(
140
+ (props: SnackbarProps, reactivate = false) =>
141
+ dispatch({ type: 'update', instance: createInstance(props), reactivate }),
142
+ [createInstance],
143
+ );
144
+
145
+ const dismissSnackbar = useCallback(() => dispatch({ type: 'dismiss' }), []);
146
+
147
+ const handleExited = useCallback(() => {
148
+ if (!state.active) {
149
+ return;
150
+ }
151
+
152
+ try {
153
+ state.active.props.onDismiss?.();
154
+ } finally {
155
+ // Consumer callbacks must not prevent a pending Snackbar from being promoted.
156
+ dispatch({ type: 'exited', instanceId: state.active.id });
157
+ }
158
+ }, [state.active]);
159
+
160
+ useEffect(() => {
161
+ if (!state.active || !state.visible || state.activeHasRendered) {
162
+ return;
163
+ }
164
+
165
+ // This runs after React commits the active Snackbar. Later creates can now safely wait for its
166
+ // exit transition; creates dispatched before this point continue to coalesce to the latest.
167
+ dispatch({ type: 'rendered', instanceId: state.active.id });
168
+ }, [state.active, state.activeHasRendered, state.visible]);
169
+
170
+ return {
171
+ active: state.active,
172
+ announcementKey: state.announcementKey,
173
+ dismissSnackbar,
174
+ handleExited,
175
+ showSnackbar,
176
+ updateSnackbar,
177
+ visible: state.visible,
178
+ };
179
+ };
180
+
181
+ export default useSnackbarManager;
@@ -0,0 +1,129 @@
1
+ import { useCallback, useRef } from 'react';
2
+
3
+ import { SWIPE_DISMISS_THRESHOLD_RATIO } from './constants';
4
+
5
+ const SWIPE_DISMISS_CLASS_NAME = 'wds-Snackbar--swipe-dismiss';
6
+
7
+ const useSnackbarSwipeDismiss = (onDismissRequest: () => void) => {
8
+ const containerRef = useRef<HTMLDivElement>(null);
9
+ const initialXCoordinate = useRef(0);
10
+ const initialYCoordinate = useRef(0);
11
+ const moveOffsetX = useRef(0);
12
+ const moveOffsetY = useRef(0);
13
+ const dismissThreshold = useRef(0);
14
+ const activePointerId = useRef<number | null>(null);
15
+ const captureTarget = useRef<Element | null>(null);
16
+ const suppressClick = useRef(false);
17
+
18
+ const clearSwipePosition = useCallback(() => {
19
+ activePointerId.current = null;
20
+ captureTarget.current = null;
21
+ suppressClick.current = false;
22
+ moveOffsetX.current = 0;
23
+ moveOffsetY.current = 0;
24
+ containerRef.current?.classList.remove(SWIPE_DISMISS_CLASS_NAME);
25
+ containerRef.current?.style.removeProperty('transform');
26
+ }, []);
27
+
28
+ const updateMoveOffset = ({ clientX, clientY }: React.PointerEvent<HTMLDivElement>) => {
29
+ moveOffsetX.current = clientX - initialXCoordinate.current;
30
+ moveOffsetY.current = clientY - initialYCoordinate.current;
31
+ };
32
+
33
+ const releasePointerCapture = (pointerId: number) => {
34
+ const target = captureTarget.current;
35
+ if (target?.hasPointerCapture?.(pointerId)) {
36
+ target.releasePointerCapture(pointerId);
37
+ }
38
+ captureTarget.current = null;
39
+ };
40
+
41
+ const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
42
+ if (event.button !== 0 || !event.isPrimary || activePointerId.current !== null) {
43
+ return;
44
+ }
45
+
46
+ const target = event.target as Element;
47
+ target.setPointerCapture?.(event.pointerId);
48
+ captureTarget.current = target;
49
+ activePointerId.current = event.pointerId;
50
+ initialXCoordinate.current = event.clientX;
51
+ initialYCoordinate.current = event.clientY;
52
+ moveOffsetX.current = 0;
53
+ moveOffsetY.current = 0;
54
+ dismissThreshold.current = event.currentTarget.offsetHeight * SWIPE_DISMISS_THRESHOLD_RATIO;
55
+ };
56
+
57
+ const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
58
+ if (activePointerId.current !== event.pointerId) {
59
+ return;
60
+ }
61
+
62
+ updateMoveOffset(event);
63
+
64
+ if (moveOffsetY.current <= 0 || moveOffsetY.current <= Math.abs(moveOffsetX.current)) {
65
+ containerRef.current?.style.removeProperty('transform');
66
+ return;
67
+ }
68
+
69
+ containerRef.current?.style.setProperty('transform', `translateY(${moveOffsetY.current}px)`);
70
+ };
71
+
72
+ const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
73
+ if (activePointerId.current !== event.pointerId) {
74
+ return;
75
+ }
76
+
77
+ updateMoveOffset(event);
78
+ releasePointerCapture(event.pointerId);
79
+ activePointerId.current = null;
80
+
81
+ if (
82
+ moveOffsetY.current >= dismissThreshold.current &&
83
+ moveOffsetY.current > Math.abs(moveOffsetX.current)
84
+ ) {
85
+ const container = containerRef.current;
86
+ if (container) {
87
+ const dismissOffset = Math.max(moveOffsetY.current, container.offsetHeight);
88
+ container.classList.add(SWIPE_DISMISS_CLASS_NAME);
89
+ container.style.setProperty('transform', `translateY(${dismissOffset}px)`);
90
+ }
91
+ suppressClick.current = true;
92
+ onDismissRequest();
93
+ return;
94
+ }
95
+
96
+ clearSwipePosition();
97
+ };
98
+
99
+ const handlePointerCancel = (event: React.PointerEvent<HTMLDivElement>) => {
100
+ if (activePointerId.current !== event.pointerId) {
101
+ return;
102
+ }
103
+
104
+ releasePointerCapture(event.pointerId);
105
+ clearSwipePosition();
106
+ };
107
+
108
+ const handleClickCapture = (event: React.MouseEvent<HTMLDivElement>) => {
109
+ if (!suppressClick.current) {
110
+ return;
111
+ }
112
+
113
+ suppressClick.current = false;
114
+ event.preventDefault();
115
+ event.stopPropagation();
116
+ };
117
+
118
+ return {
119
+ clearSwipePosition,
120
+ containerRef,
121
+ handleClickCapture,
122
+ handlePointerCancel,
123
+ handlePointerDown,
124
+ handlePointerMove,
125
+ handlePointerUp,
126
+ };
127
+ };
128
+
129
+ export default useSnackbarSwipeDismiss;
@@ -146,6 +146,12 @@
146
146
  border-radius: var(--radius-small);
147
147
  color: #37517e;
148
148
  color: var(--color-content-primary);
149
+ font-size: 0.875rem;
150
+ font-size: var(--font-size-14);
151
+ line-height: 155%;
152
+ letter-spacing: -0.006em;
153
+ font-weight: 400;
154
+ font-weight: var(--font-weight-regular);
149
155
  word-wrap: break-word;
150
156
  }
151
157
  .np-theme-personal .np-dropdown-menu > li > a {