@transferwise/components 0.0.0-experimental-536d239 → 0.0.0-experimental-6e04f56

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 (95) hide show
  1. package/build/Snackbar/Snackbar.js +98 -101
  2. package/build/Snackbar/Snackbar.js.map +1 -1
  3. package/build/Snackbar/Snackbar.mjs +99 -102
  4. package/build/Snackbar/Snackbar.mjs.map +1 -1
  5. package/build/main.css +1530 -2883
  6. package/build/styles/Inputs/Input/Input.css +28 -62
  7. package/build/styles/Inputs/TextArea/TextArea.css +28 -62
  8. package/build/styles/Popover/Popover.css +9 -3
  9. package/build/styles/_deprecated/Select/Select.css +7 -27
  10. package/build/styles/css/accordion.css +52 -23
  11. package/build/styles/css/button-groups.css +8 -29
  12. package/build/styles/css/buttons.css +2 -26
  13. package/build/styles/css/decision.css +52 -23
  14. package/build/styles/css/dropdowns.css +62 -39
  15. package/build/styles/css/droppable.css +8 -28
  16. package/build/styles/css/footer.css +14 -41
  17. package/build/styles/css/input-groups.css +165 -172
  18. package/build/styles/css/list-group.css +45 -0
  19. package/build/styles/css/navbar.css +62 -39
  20. package/build/styles/css/neptune-addons.css +0 -15
  21. package/build/styles/css/neptune-core.css +526 -250
  22. package/build/styles/css/neptune.css +1486 -2823
  23. package/build/styles/css/popovers.css +60 -35
  24. package/build/styles/css/sequences.css +1 -6
  25. package/build/styles/css/tooltip.css +7 -27
  26. package/build/styles/less/neptune-tokens.less +48 -0
  27. package/build/styles/main.css +1530 -2883
  28. package/build/styles/props/neptune-tokens.css +1 -793
  29. package/build/styles/styles/less/accordion.css +52 -23
  30. package/build/styles/styles/less/button-groups.css +8 -29
  31. package/build/styles/styles/less/buttons.css +2 -26
  32. package/build/styles/styles/less/decision.css +52 -23
  33. package/build/styles/styles/less/dropdowns.css +62 -39
  34. package/build/styles/styles/less/droppable.css +8 -28
  35. package/build/styles/styles/less/footer.css +14 -41
  36. package/build/styles/styles/less/forms/bootstrap-forms.css +91 -137
  37. package/build/styles/styles/less/input-groups.css +165 -172
  38. package/build/styles/styles/less/list-group.css +45 -0
  39. package/build/styles/styles/less/navbar.css +62 -39
  40. package/build/styles/styles/less/neptune-addons.css +0 -15
  41. package/build/styles/styles/less/neptune-core.css +526 -250
  42. package/build/styles/styles/less/neptune.css +1486 -2823
  43. package/build/styles/styles/less/popovers.css +60 -35
  44. package/build/styles/styles/less/sequences.css +1 -6
  45. package/build/styles/styles/less/tooltip.css +7 -27
  46. package/build/types/Body/Body.d.ts +2 -2
  47. package/build/types/IconButton/IconButton.d.ts +1 -1
  48. package/build/types/MoneyInput/MoneyInput.d.ts +1 -1
  49. package/build/types/MoneyInput/MoneyInput.d.ts.map +1 -1
  50. package/build/types/Snackbar/Snackbar.d.ts +1 -15
  51. package/build/types/Snackbar/Snackbar.d.ts.map +1 -1
  52. package/build/types/Title/Title.d.ts +2 -2
  53. package/build/types/Upload/Steps/UploadImageStep/UploadImageStep.d.ts +1 -1
  54. package/build/types/UploadInput/UploadItem/UploadItemLink.d.ts +1 -1
  55. package/package.json +1 -1
  56. package/src/Inputs/Input/Input.css +28 -62
  57. package/src/Inputs/TextArea/TextArea.css +28 -62
  58. package/src/Popover/Popover.css +9 -3
  59. package/src/Snackbar/Snackbar.test.tsx +190 -0
  60. package/src/Snackbar/Snackbar.tsx +90 -88
  61. package/src/_deprecated/Select/Select.css +7 -27
  62. package/src/main.css +1530 -2883
  63. package/src/styles/less/accordion.css +52 -23
  64. package/src/styles/less/addons/_background-utilities.less +0 -5
  65. package/src/styles/less/button-groups.css +8 -29
  66. package/src/styles/less/button-groups.less +2 -3
  67. package/src/styles/less/buttons.css +2 -26
  68. package/src/styles/less/buttons.less +1 -1
  69. package/src/styles/less/core/_typography.less +221 -67
  70. package/src/styles/less/decision.css +52 -23
  71. package/src/styles/less/decision.less +2 -2
  72. package/src/styles/less/dropdowns.css +62 -39
  73. package/src/styles/less/dropdowns.less +5 -6
  74. package/src/styles/less/droppable.css +8 -28
  75. package/src/styles/less/droppable.less +4 -4
  76. package/src/styles/less/footer.css +14 -41
  77. package/src/styles/less/footer.less +3 -4
  78. package/src/styles/less/forms/bootstrap-forms.css +91 -137
  79. package/src/styles/less/forms/bootstrap-forms.less +57 -70
  80. package/src/styles/less/input-groups.css +165 -172
  81. package/src/styles/less/input-groups.less +5 -5
  82. package/src/styles/less/list-group.css +45 -0
  83. package/src/styles/less/list-group.less +4 -7
  84. package/src/styles/less/mixins/_text-emphasis.less +1 -7
  85. package/src/styles/less/navbar.css +62 -39
  86. package/src/styles/less/neptune-addons.css +0 -15
  87. package/src/styles/less/neptune-core.css +526 -250
  88. package/src/styles/less/neptune.bundle.less +0 -4
  89. package/src/styles/less/neptune.css +1486 -2823
  90. package/src/styles/less/popovers.css +60 -35
  91. package/src/styles/less/popovers.less +3 -3
  92. package/src/styles/less/sequences.css +1 -6
  93. package/src/styles/less/sequences.less +2 -7
  94. package/src/styles/less/tooltip.css +7 -27
  95. package/src/styles/less/tooltip.less +1 -1
@@ -0,0 +1,190 @@
1
+ import { act, render, screen, userEvent } from '../test-utils';
2
+ import Button from '../Button';
3
+
4
+ import { Snackbar, type SnackbarProps } from './Snackbar';
5
+ import { SnackbarConsumer } from './SnackbarContext';
6
+ import SnackbarProvider from './SnackbarProvider';
7
+
8
+ const CSS_TRANSITION_DURATION = 400;
9
+ const TIMEOUT = 4000;
10
+
11
+ const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTimeAsync });
12
+
13
+ const advanceTimers = (duration: number) => {
14
+ act(() => {
15
+ jest.advanceTimersByTime(duration);
16
+ });
17
+ };
18
+
19
+ const renderSnackbar = (props: Partial<SnackbarProps> = {}) =>
20
+ render(<Snackbar text="" timeout={TIMEOUT} timestamp={0} {...props} />);
21
+
22
+ beforeEach(() => {
23
+ jest.useFakeTimers();
24
+ });
25
+
26
+ afterEach(() => {
27
+ jest.clearAllTimers();
28
+ jest.useRealTimers();
29
+ });
30
+
31
+ describe('Snackbar', () => {
32
+ it('renders the live region wrapper even while no notification is shown', () => {
33
+ renderSnackbar();
34
+
35
+ expect(screen.getByRole('alert')).toHaveAttribute('aria-live', 'assertive');
36
+ expect(screen.getByRole('alert')).toHaveClass('snackbar');
37
+ });
38
+
39
+ it('does not show a notification that is already present on mount', () => {
40
+ renderSnackbar({ text: 'Changes saved', timestamp: 1 });
41
+
42
+ expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
43
+ });
44
+
45
+ it('shows the notification once text arrives after mount', () => {
46
+ const { rerender } = renderSnackbar();
47
+
48
+ rerender(<Snackbar text="Changes saved" timeout={TIMEOUT} timestamp={1} />);
49
+
50
+ expect(screen.getByText('Changes saved')).toBeInTheDocument();
51
+ });
52
+
53
+ it('renders an action and calls its onClick handler', async () => {
54
+ const onClick = jest.fn();
55
+ const { rerender } = renderSnackbar();
56
+
57
+ rerender(
58
+ <Snackbar
59
+ action={{ label: 'View', onClick }}
60
+ text="Transfer completed"
61
+ timeout={TIMEOUT}
62
+ timestamp={1}
63
+ />,
64
+ );
65
+ await user.click(screen.getByRole('button', { name: 'View' }));
66
+
67
+ expect(onClick).toHaveBeenCalledTimes(1);
68
+ });
69
+
70
+ it('dismisses the notification once the timeout elapses', () => {
71
+ const { rerender } = renderSnackbar();
72
+
73
+ rerender(<Snackbar text="Changes saved" timeout={TIMEOUT} timestamp={1} />);
74
+
75
+ advanceTimers(TIMEOUT - 1);
76
+ expect(screen.getByText('Changes saved')).toBeInTheDocument();
77
+
78
+ advanceTimers(1);
79
+ advanceTimers(CSS_TRANSITION_DURATION);
80
+ expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
81
+ });
82
+
83
+ it('exits the current notification before entering the next one', () => {
84
+ const { rerender } = renderSnackbar();
85
+
86
+ rerender(<Snackbar text="First message" timeout={TIMEOUT} timestamp={1} />);
87
+ rerender(<Snackbar text="Second message" timeout={TIMEOUT} timestamp={2} />);
88
+
89
+ advanceTimers(CSS_TRANSITION_DURATION - 1);
90
+ expect(screen.queryByText('Second message')).not.toBeInTheDocument();
91
+
92
+ advanceTimers(1);
93
+ expect(screen.getByText('Second message')).toBeInTheDocument();
94
+ expect(screen.queryByText('First message')).not.toBeInTheDocument();
95
+ });
96
+
97
+ it('shows the next notification immediately when the previous one has already been dismissed', () => {
98
+ const { rerender } = renderSnackbar();
99
+
100
+ rerender(<Snackbar text="First message" timeout={TIMEOUT} timestamp={1} />);
101
+ advanceTimers(TIMEOUT + CSS_TRANSITION_DURATION);
102
+
103
+ rerender(<Snackbar text="Second message" timeout={TIMEOUT} timestamp={2} />);
104
+ expect(screen.getByText('Second message')).toBeInTheDocument();
105
+ });
106
+
107
+ it('restarts the timeout for a replacing notification', () => {
108
+ const { rerender } = renderSnackbar();
109
+
110
+ rerender(<Snackbar text="First message" timeout={TIMEOUT} timestamp={1} />);
111
+ advanceTimers(TIMEOUT - 100);
112
+
113
+ rerender(<Snackbar text="Second message" timeout={TIMEOUT} timestamp={2} />);
114
+ advanceTimers(CSS_TRANSITION_DURATION);
115
+ expect(screen.getByText('Second message')).toBeInTheDocument();
116
+
117
+ advanceTimers(TIMEOUT - 1);
118
+ expect(screen.getByText('Second message')).toBeInTheDocument();
119
+
120
+ advanceTimers(1);
121
+ advanceTimers(CSS_TRANSITION_DURATION);
122
+ expect(screen.queryByText('Second message')).not.toBeInTheDocument();
123
+ });
124
+
125
+ // Documents existing behaviour rather than endorsing it: clearing the text leaves the notification
126
+ // on screen indefinitely, because the previous shouldComponentUpdate blocked the dismissal too.
127
+ it('keeps the notification visible for as long as text is empty', () => {
128
+ const { rerender } = renderSnackbar();
129
+
130
+ rerender(<Snackbar text="Changes saved" timeout={TIMEOUT} timestamp={1} />);
131
+ rerender(<Snackbar text="" timeout={TIMEOUT} timestamp={2} />);
132
+
133
+ advanceTimers(TIMEOUT + CSS_TRANSITION_DURATION);
134
+ expect(screen.getByText('Changes saved')).toBeInTheDocument();
135
+ });
136
+
137
+ it('ignores a repeated timestamp', () => {
138
+ const { rerender } = renderSnackbar();
139
+
140
+ rerender(<Snackbar text="First message" timeout={TIMEOUT} timestamp={1} />);
141
+ rerender(<Snackbar text="Second message" timeout={TIMEOUT} timestamp={1} />);
142
+
143
+ advanceTimers(CSS_TRANSITION_DURATION);
144
+ expect(screen.getByText('First message')).toBeInTheDocument();
145
+ expect(screen.queryByText('Second message')).not.toBeInTheDocument();
146
+ });
147
+
148
+ it('clears its pending dismissal timer on unmount', () => {
149
+ const { rerender, unmount } = renderSnackbar();
150
+
151
+ rerender(<Snackbar text="Changes saved" timeout={TIMEOUT} timestamp={1} />);
152
+ const pendingTimers = jest.getTimerCount();
153
+ unmount();
154
+
155
+ // The timer left behind belongs to CSSTransition's enter timeout, not to Snackbar.
156
+ expect(jest.getTimerCount()).toBe(pendingTimers - 1);
157
+ });
158
+ });
159
+
160
+ describe('Snackbar created through SnackbarProvider', () => {
161
+ const renderProvider = () =>
162
+ render(
163
+ <SnackbarProvider timeout={TIMEOUT}>
164
+ <SnackbarConsumer>
165
+ {({ createSnackbar }) => (
166
+ <Button onClick={() => createSnackbar({ text: 'Changes saved' })}>Show</Button>
167
+ )}
168
+ </SnackbarConsumer>
169
+ </SnackbarProvider>,
170
+ );
171
+
172
+ it('shows a notification created through the context', async () => {
173
+ renderProvider();
174
+
175
+ await user.click(screen.getByRole('button', { name: 'Show' }));
176
+
177
+ expect(screen.getByText('Changes saved')).toBeInTheDocument();
178
+ });
179
+
180
+ it('shows only one notification when the trigger is clicked repeatedly', async () => {
181
+ renderProvider();
182
+
183
+ const trigger = screen.getByRole('button', { name: 'Show' });
184
+ await user.click(trigger);
185
+ await user.click(trigger);
186
+ await user.click(trigger);
187
+
188
+ expect(screen.getAllByRole('alert')).toHaveLength(1);
189
+ });
190
+ });
@@ -1,10 +1,9 @@
1
- import { Component, createRef } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
2
  import { CSSTransition } from 'react-transition-group';
3
3
 
4
4
  import Button from '../Button';
5
5
  import Body from '../Body';
6
6
  import type { ThemeDark, ThemeLight } from '../common';
7
- import { DirectionContext } from '../Provider/Direction';
8
7
  import withNextPortal from '../WithNextPortal/WithNextPortal';
9
8
 
10
9
  const CSS_TRANSITION_DURATION = 400;
@@ -25,102 +24,105 @@ interface SnackbarState extends Pick<SnackbarProps, 'action' | 'text' | 'theme'>
25
24
  visible: boolean;
26
25
  }
27
26
 
28
- export class Snackbar extends Component<SnackbarProps, SnackbarState> {
29
- bodyRef = createRef<HTMLSpanElement>();
30
- timeout = 0;
31
- transitionTimeout = 0;
27
+ export const Snackbar = (props: SnackbarProps) => {
28
+ const bodyRef = useRef<HTMLSpanElement>(null);
29
+ const leaveTimeout = useRef(0);
30
+ const transitionTimeout = useRef(0);
31
+ /** The props of the last committed render, as `this.props` was in the class component. */
32
+ const committedProps = useRef(props);
33
+
34
+ const [state, setState] = useState<SnackbarState>({
35
+ visible: false,
36
+ text: '',
37
+ });
38
+
39
+ useEffect(
40
+ () => () => {
41
+ window.clearTimeout(leaveTimeout.current);
42
+ window.clearTimeout(transitionTimeout.current);
43
+ },
44
+ [],
45
+ );
46
+
47
+ // Runs after every render to mirror the componentDidUpdate lifecycle this component previously
48
+ // relied on, including the shouldComponentUpdate guard that ignored every update without text.
49
+ // Both rules below are disabled on purpose: the dependency-less effect and its synchronous state
50
+ // updates are what keep this conversion behaviour-identical to the class component.
51
+ /* eslint-disable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */
52
+ useEffect(() => {
53
+ const previousProps = committedProps.current;
54
+ committedProps.current = props;
55
+
56
+ const { action, text, timeout, timestamp } = props;
57
+
58
+ // shouldComponentUpdate bailed out whenever the incoming text was empty. That also blocked the
59
+ // updates scheduled below, so a Snackbar whose text is cleared stays on screen until it is
60
+ // given text again. Preserved here deliberately to keep this refactor behaviour-identical.
61
+ const updateWhileTextIsPresent = (update: () => void) => {
62
+ if (committedProps.current.text) {
63
+ update();
64
+ }
65
+ };
32
66
 
33
- constructor(props: SnackbarProps) {
34
- super(props);
35
- this.state = {
36
- visible: false,
37
- text: '',
67
+ const setLeaveTimeout = () => {
68
+ leaveTimeout.current = window.setTimeout(() => {
69
+ updateWhileTextIsPresent(() => setState((current) => ({ ...current, visible: false })));
70
+ }, timeout);
38
71
  };
39
- }
40
72
 
41
- componentWillUnmount() {
42
- window.clearTimeout(this.timeout);
43
- window.clearTimeout(this.transitionTimeout);
44
- }
73
+ const show = () => {
74
+ setState((current) => ({ ...current, visible: true, action, text }));
75
+ setLeaveTimeout();
76
+ };
45
77
 
46
- shouldComponentUpdate(nextProps: SnackbarProps, nextState: SnackbarState) {
47
- if (!nextProps.text) {
48
- return false;
78
+ if (!text) {
79
+ return;
49
80
  }
50
- if (
51
- nextProps.timestamp === this.props.timestamp &&
52
- nextState.visible === this.state.visible &&
53
- nextState.text === this.state.text
54
- ) {
55
- return false;
56
- }
57
-
58
- return true;
59
- }
60
-
61
- setLeaveTimeout = () => {
62
- const { timeout } = this.props;
63
-
64
- this.timeout = window.setTimeout(() => {
65
- this.setState({ visible: false });
66
- }, timeout);
67
- };
68
-
69
- componentDidUpdate(previousProps: SnackbarProps) {
70
- const { action, text, timestamp } = this.props;
71
81
 
72
82
  if (!previousProps.text) {
73
- this.setState({ visible: true, action, text }, () => {
74
- this.setLeaveTimeout();
75
- });
83
+ show();
76
84
  } 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
- });
85
+ window.clearTimeout(leaveTimeout.current);
86
+
87
+ if (state.visible) {
88
+ setState((current) => ({ ...current, visible: false }));
89
+ transitionTimeout.current = window.setTimeout(() => {
90
+ updateWhileTextIsPresent(show);
91
+ }, CSS_TRANSITION_DURATION);
86
92
  } else {
87
- this.setState({ visible: true, action, text });
88
- this.setLeaveTimeout();
93
+ show();
89
94
  }
90
95
  }
91
- }
92
-
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
109
- >
110
- <Body ref={this.bodyRef} as="span" className="snackbar__text">
111
- {text}
112
- {action ? (
113
- <Button className="snackbar__text__action" v2 size="sm" onClick={action.onClick}>
114
- {action.label}
115
- </Button>
116
- ) : null}
117
- </Body>
118
- </CSSTransition>
119
- </div>
120
- );
121
- }
122
- }
123
-
124
- Snackbar.contextType = DirectionContext;
96
+ });
97
+ /* eslint-enable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */
98
+
99
+ const { action, text, visible } = state;
100
+ const { timeout } = props;
101
+
102
+ return (
103
+ <div className="snackbar" aria-live="assertive" role="alert">
104
+ <CSSTransition
105
+ nodeRef={bodyRef}
106
+ in={visible}
107
+ classNames="snackbar__text-container"
108
+ timeout={{
109
+ appear: 0,
110
+ enter: timeout,
111
+ exit: CSS_TRANSITION_DURATION,
112
+ }}
113
+ unmountOnExit
114
+ >
115
+ <Body ref={bodyRef} as="span" className="snackbar__text">
116
+ {text}
117
+ {action ? (
118
+ <Button className="snackbar__text__action" v2 size="sm" onClick={action.onClick}>
119
+ {action.label}
120
+ </Button>
121
+ ) : null}
122
+ </Body>
123
+ </CSSTransition>
124
+ </div>
125
+ );
126
+ };
125
127
 
126
128
  export default withNextPortal(Snackbar);
@@ -1,30 +1,4 @@
1
- .np-dropdown-menu > li > a {
2
- word-wrap: break-word;
3
- }
4
- @supports (hyphenate-limit-chars: 1) {
5
- .np-dropdown-menu > li > a {
6
- hyphens: auto;hyphenate-limit-chars: 7 3;
7
- }
8
- @media (min-width: 840px) {
9
- .np-dropdown-menu > li > a {
10
- hyphenate-limit-chars: 8 3;
11
- }
12
- }
13
- @media (min-width: 1160px) {
14
- .np-dropdown-menu > li > a {
15
- hyphenate-limit-chars: 10 4 3;
16
- }
17
- }
18
- }@supports (not (hyphenate-limit-chars: 1)) and (-webkit-hyphenate-limit-before: 1) {
19
- .np-dropdown-menu > li > a {
20
- hyphens: auto;-webkit-hyphenate-limit-before: 3;-webkit-hyphenate-limit-after: 3;
21
- }
22
- @media (min-width: 1160px) {
23
- .np-dropdown-menu > li > a {
24
- -webkit-hyphenate-limit-before: 4;
25
- }
26
- }
27
- }.np-select .np-dropdown-toggle {
1
+ .np-select .np-dropdown-toggle {
28
2
  background-color: #ffffff;
29
3
  background-color: var(--color-background-screen);
30
4
  border: 1px solid #c9cbce;
@@ -172,6 +146,12 @@
172
146
  border-radius: var(--radius-small);
173
147
  color: #37517e;
174
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);
175
155
  word-wrap: break-word;
176
156
  }
177
157
  .np-theme-personal .np-dropdown-menu > li > a {