@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,126 +1,257 @@
1
- import { Component, createRef } from 'react';
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { CSSTransition } from 'react-transition-group';
3
+ import { useIntl } from 'react-intl';
4
+ import { Tags } from '@transferwise/icons';
3
5
 
4
6
  import Button from '../Button';
7
+ import type { ButtonReferenceType } from '../Button/Button.types';
5
8
  import Body from '../Body';
9
+ import { LiveRegion } from '../common';
6
10
  import type { ThemeDark, ThemeLight } from '../common';
7
- import { DirectionContext } from '../Provider/Direction';
8
- import withNextPortal from '../WithNextPortal/WithNextPortal';
9
-
10
- const CSS_TRANSITION_DURATION = 400;
11
+ import ProcessIndicator from '../ProcessIndicator';
12
+ import { PrimitivePrompt } from '../Prompt/PrimitivePrompt';
13
+ import type { Sentiment } from '../SentimentSurface';
14
+ import StatusIcon from '../StatusIcon';
15
+ import { SNACKBAR_ENTER_DURATION, SNACKBAR_EXIT_DURATION } from './constants';
16
+ import messages from './Snackbar.messages';
17
+ import type { SnackbarAction, SnackbarDuration, SnackbarInternalProps } from './Snackbar.types';
18
+ import useSnackbarAutoDismiss from './useSnackbarAutoDismiss';
19
+ import useSnackbarSwipeDismiss from './useSnackbarSwipeDismiss';
11
20
 
12
21
  export interface SnackbarProps {
13
- action?: {
14
- label: string;
15
- onClick?: React.MouseEventHandler<HTMLButtonElement>;
16
- };
22
+ /** One optional action. Supports callbacks and navigation through the Button API. */
23
+ action?: SnackbarAction;
24
+ /**
25
+ * Controls auto-dismiss using the design-defined duration presets.
26
+ * @default 'default'
27
+ */
28
+ duration?: SnackbarDuration;
29
+ /**
30
+ * Replaces the sentiment-derived default media.
31
+ * Custom media should fit the 24x24px media slot.
32
+ * The asset should include its own accessibility attributes if it conveys meaning.
33
+ */
34
+ media?: React.ReactNode;
35
+ /**
36
+ * Replaces the media with a progress indicator and uses neutral sentiment styling.
37
+ * @default false
38
+ */
39
+ loading?: boolean;
40
+ /** Called after the Snackbar completes dismissal. */
41
+ onDismiss?: () => void;
42
+ /**
43
+ * Determines the colour scheme and default media.
44
+ * @default 'neutral'
45
+ */
46
+ sentiment?: Sentiment;
47
+ /** Short Snackbar message. Plain text is recommended. */
17
48
  text: React.ReactNode;
18
- /** @deprecated */
49
+ /**
50
+ * @deprecated Theme no longer changes Snackbar appearance. Use `sentiment` for the semantic
51
+ * colour scheme. This prop remains available temporarily for existing implementations.
52
+ */
19
53
  theme?: ThemeLight | ThemeDark;
20
- timeout: number;
21
- timestamp: number;
54
+ /**
55
+ * @deprecated Use `duration` instead. Existing millisecond values remain supported during
56
+ * migration: use `default` for 0–5000ms, `extended` above 5000ms, and `indefinite` only when
57
+ * the existing usage is intentionally persistent. `duration` takes precedence when both are
58
+ * provided.
59
+ */
60
+ timeout?: number;
22
61
  }
23
62
 
24
- interface SnackbarState extends Pick<SnackbarProps, 'action' | 'text' | 'theme'> {
25
- visible: boolean;
26
- }
63
+ export const SnackbarInternal = ({
64
+ action,
65
+ announcementKey,
66
+ duration,
67
+ loading = false,
68
+ media,
69
+ onDismissRequest,
70
+ onExited,
71
+ sentiment = 'neutral',
72
+ text,
73
+ timeout,
74
+ visible,
75
+ }: SnackbarInternalProps) => {
76
+ const { formatMessage } = useIntl();
77
+ const {
78
+ clearSwipePosition,
79
+ containerRef,
80
+ handleClickCapture,
81
+ handlePointerCancel,
82
+ handlePointerDown,
83
+ handlePointerMove,
84
+ handlePointerUp,
85
+ } = useSnackbarSwipeDismiss(onDismissRequest);
86
+ const { handleBlur, handleFocus, handleMouseEnter, handleMouseLeave, surfaceRef } =
87
+ useSnackbarAutoDismiss({
88
+ duration,
89
+ loading,
90
+ onDismissRequest,
91
+ timeout,
92
+ visible,
93
+ resetKey: announcementKey,
94
+ });
27
95
 
28
- export class Snackbar extends Component<SnackbarProps, SnackbarState> {
29
- bodyRef = createRef<HTMLSpanElement>();
30
- timeout = 0;
31
- transitionTimeout = 0;
96
+ const handleExited = () => {
97
+ clearSwipePosition();
98
+ onExited();
99
+ };
32
100
 
33
- constructor(props: SnackbarProps) {
34
- super(props);
35
- this.state = {
36
- visible: false,
37
- text: '',
38
- };
39
- }
101
+ const handleActionClick: React.MouseEventHandler<ButtonReferenceType> = (event) => {
102
+ const handleClick: React.MouseEventHandler<ButtonReferenceType> | undefined = action?.onClick;
103
+ try {
104
+ handleClick?.(event);
105
+ } finally {
106
+ onDismissRequest();
107
+ }
108
+ };
40
109
 
41
- componentWillUnmount() {
42
- window.clearTimeout(this.timeout);
43
- window.clearTimeout(this.transitionTimeout);
44
- }
110
+ const renderMedia = () => {
111
+ if (loading) {
112
+ return (
113
+ <ProcessIndicator
114
+ className="wds-Snackbar__process-indicator"
115
+ data-testid="Snackbar_ProcessIndicator"
116
+ size="xs"
117
+ />
118
+ );
119
+ }
45
120
 
46
- shouldComponentUpdate(nextProps: SnackbarProps, nextState: SnackbarState) {
47
- if (!nextProps.text) {
48
- return false;
121
+ if (media) {
122
+ return <span className="wds-Snackbar__media-asset">{media}</span>;
49
123
  }
50
- if (
51
- nextProps.timestamp === this.props.timestamp &&
52
- nextState.visible === this.state.visible &&
53
- nextState.text === this.state.text
54
- ) {
55
- return false;
124
+
125
+ if (sentiment === 'proposition') {
126
+ return (
127
+ <span className="wds-Snackbar__media-asset">
128
+ <Tags aria-hidden size={24} />
129
+ </span>
130
+ );
56
131
  }
57
132
 
58
- return true;
59
- }
133
+ return <StatusIcon sentiment={sentiment} size={24} />;
134
+ };
60
135
 
61
- setLeaveTimeout = () => {
62
- const { timeout } = this.props;
136
+ useEffect(() => {
137
+ if (visible) {
138
+ clearSwipePosition();
139
+ }
140
+ }, [clearSwipePosition, visible]);
63
141
 
64
- this.timeout = window.setTimeout(() => {
65
- this.setState({ visible: false });
66
- }, timeout);
67
- };
142
+ useEffect(() => {
143
+ if (!visible) {
144
+ return undefined;
145
+ }
68
146
 
69
- componentDidUpdate(previousProps: SnackbarProps) {
70
- const { action, text, timestamp } = this.props;
71
-
72
- if (!previousProps.text) {
73
- this.setState({ visible: true, action, text }, () => {
74
- this.setLeaveTimeout();
75
- });
76
- } else if (previousProps.timestamp !== timestamp) {
77
- window.clearTimeout(this.timeout);
78
-
79
- if (this.state.visible) {
80
- this.setState({ visible: false }, () => {
81
- this.transitionTimeout = window.setTimeout(() => {
82
- this.setState({ visible: true, action, text });
83
- this.setLeaveTimeout();
84
- }, CSS_TRANSITION_DURATION);
85
- });
86
- } else {
87
- this.setState({ visible: true, action, text });
88
- this.setLeaveTimeout();
147
+ const handleKeyDown = (event: KeyboardEvent) => {
148
+ if (event.key === 'Escape' && !event.defaultPrevented) {
149
+ onDismissRequest();
89
150
  }
90
- }
91
- }
151
+ };
152
+
153
+ document.addEventListener('keydown', handleKeyDown);
154
+ return () => document.removeEventListener('keydown', handleKeyDown);
155
+ }, [onDismissRequest, visible]);
92
156
 
93
- render() {
94
- const { action, text, visible } = this.state;
95
- const { timeout } = this.props;
96
-
97
- return (
98
- <div className="snackbar" aria-live="assertive" role="alert">
99
- <CSSTransition
100
- nodeRef={this.bodyRef}
101
- in={visible}
102
- classNames="snackbar__text-container"
103
- timeout={{
104
- appear: 0,
105
- enter: timeout,
106
- exit: CSS_TRANSITION_DURATION,
107
- }}
108
- unmountOnExit
157
+ return (
158
+ <div ref={containerRef} className="wds-Snackbar">
159
+ <CSSTransition
160
+ appear
161
+ nodeRef={surfaceRef}
162
+ in={visible}
163
+ classNames="wds-Snackbar-container"
164
+ timeout={{
165
+ appear: SNACKBAR_ENTER_DURATION,
166
+ enter: SNACKBAR_ENTER_DURATION,
167
+ exit: SNACKBAR_EXIT_DURATION,
168
+ }}
169
+ unmountOnExit
170
+ onExited={handleExited}
171
+ >
172
+ <PrimitivePrompt
173
+ ref={surfaceRef}
174
+ className="wds-Snackbar-container"
175
+ sentiment={loading ? 'neutral' : sentiment}
176
+ emphasis="elevated"
177
+ media={renderMedia()}
178
+ onDismiss={onDismissRequest}
179
+ onBlur={handleBlur}
180
+ onFocus={handleFocus}
181
+ onMouseEnter={handleMouseEnter}
182
+ onMouseLeave={handleMouseLeave}
183
+ onClickCapture={handleClickCapture}
184
+ onPointerCancel={handlePointerCancel}
185
+ onPointerDown={handlePointerDown}
186
+ onPointerMove={handlePointerMove}
187
+ onPointerUp={handlePointerUp}
109
188
  >
110
- <Body ref={this.bodyRef} as="span" className="snackbar__text">
111
- {text}
189
+ <div className="wds-Snackbar__content">
190
+ <LiveRegion
191
+ aria-live="polite"
192
+ announceOnChange={announcementKey}
193
+ className="wds-Snackbar__message"
194
+ >
195
+ <Body as="span" className="wds-Snackbar__body">
196
+ {text}
197
+ <span className="sr-only"> {formatMessage(messages.escapeDismissHint)}</span>
198
+ </Body>
199
+ </LiveRegion>
112
200
  {action ? (
113
- <Button className="snackbar__text__action" v2 size="sm" onClick={action.onClick}>
201
+ <Button
202
+ className="wds-Snackbar__action"
203
+ v2
204
+ size="sm"
205
+ priority="secondary"
206
+ href={action.href}
207
+ target={action.target}
208
+ onClick={handleActionClick}
209
+ >
114
210
  {action.label}
115
211
  </Button>
116
212
  ) : null}
117
- </Body>
118
- </CSSTransition>
119
- </div>
120
- );
121
- }
122
- }
213
+ </div>
214
+ </PrimitivePrompt>
215
+ </CSSTransition>
216
+ </div>
217
+ );
218
+ };
219
+
220
+ export const Snackbar = ({ onDismiss, ...props }: SnackbarProps) => {
221
+ const [mounted, setMounted] = useState(true);
222
+ const [visible, setVisible] = useState(true);
223
+ const [announcementKey, setAnnouncementKey] = useState(0);
224
+ const previousText = useRef(props.text);
225
+ const handleDismissRequest = useCallback(() => setVisible(false), []);
226
+ const handleExited = useCallback(() => {
227
+ setMounted(false);
228
+ onDismiss?.();
229
+ }, [onDismiss]);
230
+
231
+ useEffect(() => {
232
+ if (previousText.current === props.text) {
233
+ return;
234
+ }
235
+
236
+ previousText.current = props.text;
237
+ // Direct Snackbars do not have a Provider-generated instance id, so text changes advance this
238
+ // key to schedule a fresh announcement and restart the semantic duration.
239
+ setMounted(true);
240
+ setVisible(true);
241
+ setAnnouncementKey((key) => key + 1);
242
+ }, [props.text]);
123
243
 
124
- Snackbar.contextType = DirectionContext;
244
+ if (!mounted) {
245
+ return null;
246
+ }
125
247
 
126
- export default withNextPortal(Snackbar);
248
+ return (
249
+ <SnackbarInternal
250
+ {...props}
251
+ announcementKey={announcementKey}
252
+ visible={visible}
253
+ onDismissRequest={handleDismissRequest}
254
+ onExited={handleExited}
255
+ />
256
+ );
257
+ };
@@ -0,0 +1,26 @@
1
+ import type { ButtonProps } from '../Button/Button.types';
2
+ import type { SnackbarProps } from './Snackbar';
3
+
4
+ export type { SnackbarProps } from './Snackbar';
5
+
6
+ export type SnackbarAction = Pick<ButtonProps, 'onClick' | 'href' | 'target'> & {
7
+ label: ButtonProps['children'];
8
+ };
9
+
10
+ export type SnackbarDuration = 'default' | 'extended' | 'indefinite';
11
+
12
+ /** Props accepted when creating a Snackbar through SnackbarProvider. */
13
+ export type SnackbarCreationProps = Omit<SnackbarProps, 'timeout'>;
14
+
15
+ export interface SnackbarInternalProps extends Omit<SnackbarProps, 'onDismiss'> {
16
+ announcementKey: string | number;
17
+ onDismissRequest: () => void;
18
+ onExited: () => void;
19
+ visible: boolean;
20
+ }
21
+
22
+ /** Compatibility props for the existing SnackbarPortal export. */
23
+ export interface SnackbarPortalProps extends SnackbarProps {
24
+ /** @internal Legacy notification identity retained only for migration compatibility. */
25
+ timestamp?: number;
26
+ }
@@ -1,9 +1,9 @@
1
1
  import { createContext } from 'react';
2
2
 
3
- import { SnackbarProps } from './Snackbar';
3
+ import type { SnackbarCreationProps } from './Snackbar.types';
4
4
 
5
5
  export type SnackbarContextType = {
6
- createSnackbar: (props: Omit<SnackbarProps, 'timeout' | 'timestamp'>) => void;
6
+ createSnackbar: (props: SnackbarCreationProps) => void;
7
7
  };
8
8
 
9
9
  export const SnackbarContext = createContext<SnackbarContextType>({ createSnackbar: () => {} });
@@ -0,0 +1,110 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ import withNextPortal from '../WithNextPortal/WithNextPortal';
4
+ import { SnackbarInternal } from './Snackbar';
5
+ import type { SnackbarPortalProps } from './Snackbar.types';
6
+ import useSnackbarManager from './useSnackbarManager';
7
+
8
+ export const PortalledSnackbar = withNextPortal(SnackbarInternal);
9
+
10
+ const hasSnackbarText = (text: SnackbarPortalProps['text']) =>
11
+ text !== null && text !== undefined && text !== '';
12
+
13
+ export const SnackbarPortal = ({
14
+ action,
15
+ duration,
16
+ loading,
17
+ media,
18
+ onDismiss,
19
+ sentiment,
20
+ text,
21
+ theme,
22
+ timeout,
23
+ timestamp,
24
+ }: SnackbarPortalProps) => {
25
+ const hasText = hasSnackbarText(text);
26
+ const previousTimestamp = useRef(timestamp);
27
+ const previousText = useRef(text);
28
+ const isInitialRender = useRef(true);
29
+ const {
30
+ active,
31
+ announcementKey,
32
+ dismissSnackbar,
33
+ handleExited,
34
+ showSnackbar,
35
+ updateSnackbar,
36
+ visible,
37
+ } = useSnackbarManager(
38
+ hasText
39
+ ? { action, duration, loading, media, onDismiss, sentiment, text, theme, timeout }
40
+ : undefined,
41
+ );
42
+
43
+ useEffect(() => {
44
+ if (isInitialRender.current) {
45
+ isInitialRender.current = false;
46
+ return;
47
+ }
48
+
49
+ if (!hasText) {
50
+ dismissSnackbar();
51
+ previousTimestamp.current = timestamp;
52
+ previousText.current = text;
53
+ return;
54
+ }
55
+
56
+ const snackbar = {
57
+ action,
58
+ duration,
59
+ loading,
60
+ media,
61
+ onDismiss,
62
+ sentiment,
63
+ text,
64
+ theme,
65
+ timeout,
66
+ };
67
+
68
+ if (timestamp === undefined) {
69
+ // Without the legacy identity prop, update in place so recreated callbacks/media do not
70
+ // repeatedly restart the exit animation or auto-dismiss timer.
71
+ updateSnackbar(snackbar, !hasSnackbarText(previousText.current));
72
+ } else if (!hasSnackbarText(previousText.current) || previousTimestamp.current !== timestamp) {
73
+ showSnackbar(snackbar);
74
+ }
75
+
76
+ previousTimestamp.current = timestamp;
77
+ previousText.current = text;
78
+ }, [
79
+ dismissSnackbar,
80
+ showSnackbar,
81
+ action,
82
+ duration,
83
+ hasText,
84
+ loading,
85
+ media,
86
+ onDismiss,
87
+ sentiment,
88
+ text,
89
+ theme,
90
+ timeout,
91
+ timestamp,
92
+ updateSnackbar,
93
+ ]);
94
+
95
+ if (!active) {
96
+ return null;
97
+ }
98
+
99
+ return (
100
+ <PortalledSnackbar
101
+ {...active.props}
102
+ announcementKey={announcementKey}
103
+ visible={visible}
104
+ onDismissRequest={dismissSnackbar}
105
+ onExited={handleExited}
106
+ />
107
+ );
108
+ };
109
+
110
+ export default SnackbarPortal;