@transferwise/components 46.115.1 → 46.116.1

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 (77) hide show
  1. package/build/alert/Alert.js +2 -1
  2. package/build/alert/Alert.js.map +1 -1
  3. package/build/alert/Alert.mjs +2 -1
  4. package/build/alert/Alert.mjs.map +1 -1
  5. package/build/criticalBanner/CriticalCommsBanner.js +1 -0
  6. package/build/criticalBanner/CriticalCommsBanner.js.map +1 -1
  7. package/build/criticalBanner/CriticalCommsBanner.mjs +1 -0
  8. package/build/criticalBanner/CriticalCommsBanner.mjs.map +1 -1
  9. package/build/main.css +428 -44
  10. package/build/mocks.js +7 -0
  11. package/build/mocks.js.map +1 -1
  12. package/build/mocks.mjs +7 -1
  13. package/build/mocks.mjs.map +1 -1
  14. package/build/sentimentSurface/SentimentSurface.js +43 -0
  15. package/build/sentimentSurface/SentimentSurface.js.map +1 -0
  16. package/build/sentimentSurface/SentimentSurface.mjs +39 -0
  17. package/build/sentimentSurface/SentimentSurface.mjs.map +1 -0
  18. package/build/sentimentSurface/classMap.js +17 -0
  19. package/build/sentimentSurface/classMap.js.map +1 -0
  20. package/build/sentimentSurface/classMap.mjs +14 -0
  21. package/build/sentimentSurface/classMap.mjs.map +1 -0
  22. package/build/statusIcon/StatusIcon.js +10 -1
  23. package/build/statusIcon/StatusIcon.js.map +1 -1
  24. package/build/statusIcon/StatusIcon.mjs +10 -1
  25. package/build/statusIcon/StatusIcon.mjs.map +1 -1
  26. package/build/styles/inputs/Input.css +2 -4
  27. package/build/styles/inputs/TextArea.css +2 -4
  28. package/build/styles/main.css +428 -44
  29. package/build/styles/popover/Popover.css +2 -4
  30. package/build/styles/sentimentSurface/SentimentSurface.css +420 -0
  31. package/build/styles/statusIcon/StatusIcon.css +4 -36
  32. package/build/types/alert/Alert.d.ts.map +1 -1
  33. package/build/types/criticalBanner/CriticalCommsBanner.d.ts +2 -1
  34. package/build/types/criticalBanner/CriticalCommsBanner.d.ts.map +1 -1
  35. package/build/types/mocks.d.ts +1 -0
  36. package/build/types/mocks.d.ts.map +1 -1
  37. package/build/types/sentimentSurface/SentimentSurface.d.ts +30 -0
  38. package/build/types/sentimentSurface/SentimentSurface.d.ts.map +1 -0
  39. package/build/types/sentimentSurface/SentimentSurface.types.d.ts +80 -0
  40. package/build/types/sentimentSurface/SentimentSurface.types.d.ts.map +1 -0
  41. package/build/types/sentimentSurface/classMap.d.ts +4 -0
  42. package/build/types/sentimentSurface/classMap.d.ts.map +1 -0
  43. package/build/types/sentimentSurface/index.d.ts +3 -0
  44. package/build/types/sentimentSurface/index.d.ts.map +1 -0
  45. package/build/types/statusIcon/StatusIcon.d.ts.map +1 -1
  46. package/build/types/test-utils/window-mock.d.ts +1 -0
  47. package/build/types/test-utils/window-mock.d.ts.map +1 -1
  48. package/package.json +2 -2
  49. package/src/alert/Alert.tsx +3 -1
  50. package/src/criticalBanner/CriticalCommsBanner.tsx +3 -2
  51. package/src/expressiveMoneyInput/ExpressiveMoneyInput.spec.tsx +229 -0
  52. package/src/expressiveMoneyInput/amountInput/AmountInput.spec.tsx +282 -0
  53. package/src/expressiveMoneyInput/currencySelector/CurrencySelector.spec.tsx +160 -0
  54. package/src/inputs/Input.css +2 -4
  55. package/src/inputs/SelectInput.spec.tsx +7 -1
  56. package/src/inputs/TextArea.css +2 -4
  57. package/src/main.css +428 -44
  58. package/src/main.less +2 -0
  59. package/src/mocks.ts +7 -0
  60. package/src/moneyInput/MoneyInput.spec.tsx +9 -1
  61. package/src/popover/Popover.css +2 -4
  62. package/src/provider/theme/ThemeProvider.story.tsx +78 -11
  63. package/src/sentimentSurface/SentimentSurface.css +420 -0
  64. package/src/sentimentSurface/SentimentSurface.docs.mdx +549 -0
  65. package/src/sentimentSurface/SentimentSurface.less +293 -0
  66. package/src/sentimentSurface/SentimentSurface.spec.tsx +140 -0
  67. package/src/sentimentSurface/SentimentSurface.story.tsx +303 -0
  68. package/src/sentimentSurface/SentimentSurface.tests.story.tsx +72 -0
  69. package/src/sentimentSurface/SentimentSurface.tsx +72 -0
  70. package/src/sentimentSurface/SentimentSurface.types.ts +104 -0
  71. package/src/sentimentSurface/classMap.ts +15 -0
  72. package/src/sentimentSurface/index.ts +8 -0
  73. package/src/statusIcon/StatusIcon.css +4 -36
  74. package/src/statusIcon/StatusIcon.less +3 -41
  75. package/src/statusIcon/StatusIcon.tsx +14 -1
  76. package/src/test-utils/jest.setup.ts +0 -5
  77. package/src/test-utils/window-mock.ts +5 -0
@@ -1 +1 @@
1
- {"version":3,"file":"window-mock.d.ts","sourceRoot":"","sources":["../../../src/test-utils/window-mock.ts"],"names":[],"mappings":"AAKA,wBAAgB,cAAc,SAE7B;AAED,wBAAgB,kBAAkB,SAIjC"}
1
+ {"version":3,"file":"window-mock.d.ts","sourceRoot":"","sources":["../../../src/test-utils/window-mock.ts"],"names":[],"mappings":"AAMA,wBAAgB,cAAc,SAE7B;AAED,wBAAgB,kBAAkB,SAIjC;AAED,wBAAgB,yBAAyB,SAExC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transferwise/components",
3
- "version": "46.115.1",
3
+ "version": "46.116.1",
4
4
  "description": "Neptune React components",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -85,7 +85,7 @@
85
85
  "storybook-addon-test-codegen": "^2.0.1",
86
86
  "@transferwise/less-config": "3.1.2",
87
87
  "@transferwise/neptune-css": "14.25.2",
88
- "@wise/components-theming": "1.8.0",
88
+ "@wise/components-theming": "1.9.1",
89
89
  "@wise/wds-configs": "0.0.0"
90
90
  },
91
91
  "peerDependencies": {
@@ -147,9 +147,11 @@ export default function Alert({
147
147
 
148
148
  const [shouldAnnounce, setShouldAnnounce] = useState<boolean>(false);
149
149
  useEffect(() => {
150
- setTimeout(() => {
150
+ const timeoutId = setTimeout(() => {
151
151
  setShouldAnnounce(true);
152
152
  }, WDS_LIVE_REGION_DELAY_MS);
153
+
154
+ return () => clearTimeout(timeoutId);
153
155
  }, []);
154
156
 
155
157
  const closeButtonReference = useRef<HTMLButtonElement>(null);
@@ -6,7 +6,8 @@ export type CriticalCommsBannerProps = {
6
6
  subtitle?: string;
7
7
  action?: {
8
8
  label: string;
9
- href: string;
9
+ href?: string;
10
+ onClick?: () => void;
10
11
  };
11
12
  className?: string;
12
13
  };
@@ -17,7 +18,7 @@ function CriticalCommsBanner({ title, subtitle, action, className }: CriticalCom
17
18
  <Alert
18
19
  title={title}
19
20
  message={subtitle}
20
- action={{ target: action?.href, text: action?.label }}
21
+ action={{ onClick: action?.onClick, target: action?.href, text: action?.label }}
21
22
  className={className}
22
23
  type="warning"
23
24
  />
@@ -0,0 +1,229 @@
1
+ import { screen, render, mockMatchMedia, mockResizeObserver, waitFor } from '../test-utils';
2
+ import { Sentiment } from '../common';
3
+
4
+ import ExpressiveMoneyInput from './ExpressiveMoneyInput';
5
+ import type { Props as ExpressiveMoneyInputProps } from './ExpressiveMoneyInput';
6
+ import { Confetti } from '@transferwise/icons';
7
+
8
+ const defaultProps: ExpressiveMoneyInputProps = {
9
+ label: 'You send',
10
+ currency: 'GBP',
11
+ amount: 1234.56,
12
+ onAmountChange: jest.fn(),
13
+ };
14
+
15
+ const currencySelectorOptions = [
16
+ {
17
+ title: 'Popular',
18
+ currencies: [
19
+ { code: 'USD', label: 'US Dollar', keywords: ['dollar', 'us'] },
20
+ { code: 'AUD', label: 'Australia Dollar', keywords: ['dollar', 'au'] },
21
+ ],
22
+ },
23
+ {
24
+ title: 'Others',
25
+ currencies: [
26
+ { code: 'GBP', label: 'Pound', keywords: ['british'] },
27
+ { code: 'EUR', label: 'Euro', keywords: ['euro'] },
28
+ ],
29
+ },
30
+ ];
31
+
32
+ mockMatchMedia();
33
+ mockResizeObserver();
34
+
35
+ describe('ExpressiveMoneyInput', () => {
36
+ beforeEach(() => {
37
+ jest.clearAllMocks();
38
+ });
39
+
40
+ describe('Basic rendering', () => {
41
+ it('renders with basic props', () => {
42
+ render(<ExpressiveMoneyInput {...defaultProps} />);
43
+
44
+ expect(screen.getByText('You send')).toBeInTheDocument();
45
+ expect(screen.getByDisplayValue('1,234.56')).toBeInTheDocument();
46
+ });
47
+
48
+ it('renders with custom label', () => {
49
+ render(<ExpressiveMoneyInput {...defaultProps} label="Amount to transfer" />);
50
+
51
+ expect(screen.getByText('Amount to transfer')).toBeInTheDocument();
52
+ });
53
+
54
+ it('renders without label', () => {
55
+ render(<ExpressiveMoneyInput {...defaultProps} label={undefined} />);
56
+
57
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
58
+ expect(screen.queryByText('You send')).not.toBeInTheDocument();
59
+ });
60
+
61
+ it('renders with different currencies', () => {
62
+ render(<ExpressiveMoneyInput {...defaultProps} currency="USD" />);
63
+
64
+ expect(screen.getByText('USD')).toBeInTheDocument();
65
+ });
66
+
67
+ it('renders with null amount', () => {
68
+ render(<ExpressiveMoneyInput {...defaultProps} amount={null} />);
69
+
70
+ const input = screen.getByRole('textbox');
71
+ expect(input).toHaveValue('');
72
+ });
73
+ });
74
+
75
+ describe('Inline prompt', () => {
76
+ it('renders basic message without sentiment', () => {
77
+ render(
78
+ <ExpressiveMoneyInput
79
+ {...defaultProps}
80
+ inlinePrompt={{ message: 'This is a basic message' }}
81
+ />,
82
+ );
83
+
84
+ expect(screen.getByText('This is a basic message')).toBeInTheDocument();
85
+ });
86
+
87
+ it('renders inline prompt with sentiment', () => {
88
+ render(
89
+ <ExpressiveMoneyInput
90
+ {...defaultProps}
91
+ inlinePrompt={{
92
+ message: 'Success message',
93
+ sentiment: Sentiment.POSITIVE,
94
+ }}
95
+ />,
96
+ );
97
+
98
+ expect(screen.getByText('Success message')).toBeInTheDocument();
99
+ });
100
+
101
+ it('renders inline prompt with media', () => {
102
+ render(
103
+ <ExpressiveMoneyInput
104
+ {...defaultProps}
105
+ inlinePrompt={{
106
+ message: 'Message with icon',
107
+ sentiment: Sentiment.POSITIVE,
108
+ media: <Confetti data-testid="test-icon" />,
109
+ }}
110
+ />,
111
+ );
112
+
113
+ expect(screen.getByText('Message with icon')).toBeInTheDocument();
114
+ expect(screen.getByTestId('test-icon')).toBeInTheDocument();
115
+ });
116
+
117
+ it('does not render inline prompt when not provided', () => {
118
+ render(<ExpressiveMoneyInput {...defaultProps} />);
119
+
120
+ expect(document.querySelector('.wds-inline-prompt')).not.toBeInTheDocument();
121
+ });
122
+
123
+ it('animates inline prompt appearance', async () => {
124
+ const { rerender } = render(<ExpressiveMoneyInput {...defaultProps} />);
125
+
126
+ expect(screen.queryByText('New message')).not.toBeInTheDocument();
127
+
128
+ rerender(
129
+ <ExpressiveMoneyInput {...defaultProps} inlinePrompt={{ message: 'New message' }} />,
130
+ );
131
+
132
+ await waitFor(() => {
133
+ expect(screen.getByText('New message')).toBeInTheDocument();
134
+ });
135
+ });
136
+
137
+ it('sets correct aria-describedby when inline prompt is present', () => {
138
+ render(
139
+ <ExpressiveMoneyInput
140
+ {...defaultProps}
141
+ inlinePrompt={{ message: 'Descriptive message' }}
142
+ />,
143
+ );
144
+
145
+ const group = screen.getByRole('group');
146
+ expect(group).toHaveAttribute('aria-describedby');
147
+ });
148
+ });
149
+
150
+ describe('Chevron', () => {
151
+ it('shows chevron when showChevron is true', () => {
152
+ render(<ExpressiveMoneyInput {...defaultProps} showChevron />);
153
+
154
+ const chevronContainer = document.querySelector('.wds-expressive-money-input-chevron');
155
+ expect(chevronContainer).toBeInTheDocument();
156
+ });
157
+
158
+ it('hides chevron when showChevron is false', () => {
159
+ render(<ExpressiveMoneyInput {...defaultProps} showChevron={false} />);
160
+
161
+ const chevronContainer = document.querySelector('.wds-expressive-money-input-chevron');
162
+ expect(chevronContainer).toBeInTheDocument();
163
+ });
164
+
165
+ it('does not show chevron by default', () => {
166
+ render(<ExpressiveMoneyInput {...defaultProps} />);
167
+
168
+ const chevronContainer = document.querySelector('.wds-expressive-money-input-chevron');
169
+ expect(chevronContainer).toBeInTheDocument();
170
+ });
171
+ });
172
+
173
+ describe('Accessibility', () => {
174
+ it('has proper ARIA structure', () => {
175
+ render(<ExpressiveMoneyInput {...defaultProps} />);
176
+
177
+ const group = screen.getByRole('group');
178
+ const label = screen.getByText('You send');
179
+ const input = screen.getByRole('textbox');
180
+
181
+ expect(group).toHaveAttribute('aria-labelledby');
182
+ expect(label).toHaveAttribute('for');
183
+ expect(input).toHaveAttribute('id');
184
+ });
185
+
186
+ it('associates label with input correctly', () => {
187
+ render(<ExpressiveMoneyInput {...defaultProps} />);
188
+
189
+ const label = screen.getByText('You send');
190
+ const input = screen.getByRole('textbox');
191
+
192
+ expect(label).toHaveAttribute('for', input.getAttribute('id'));
193
+ });
194
+
195
+ it('links currency selector to main label', () => {
196
+ render(
197
+ <ExpressiveMoneyInput
198
+ {...defaultProps}
199
+ currencySelector={{
200
+ options: currencySelectorOptions,
201
+ onChange: jest.fn(),
202
+ }}
203
+ />,
204
+ );
205
+
206
+ const currencySelector = screen.getByRole('combobox');
207
+ const mainLabel = screen.getByText('You send');
208
+
209
+ expect(currencySelector).toHaveAttribute('aria-describedby', mainLabel.getAttribute('id'));
210
+ });
211
+
212
+ it('describes amount input with currency selector', () => {
213
+ render(
214
+ <ExpressiveMoneyInput
215
+ {...defaultProps}
216
+ currencySelector={{
217
+ options: currencySelectorOptions,
218
+ onChange: jest.fn(),
219
+ }}
220
+ />,
221
+ );
222
+
223
+ const input = screen.getByRole('textbox');
224
+ const currencySelector = screen.getByRole('combobox');
225
+
226
+ expect(input).toHaveAttribute('aria-describedby', currencySelector.getAttribute('id'));
227
+ });
228
+ });
229
+ });
@@ -0,0 +1,282 @@
1
+ import { userEvent, fireEvent, render, screen, waitFor } from '../../test-utils';
2
+
3
+ import { AmountInput } from './AmountInput';
4
+
5
+ describe('AmountInput', () => {
6
+ // TODO: "allows to delete the thousands separator and the preceding digit" test is flaky
7
+ jest.retryTimes(3, { logErrorsBeforeRetry: true });
8
+
9
+ it('formats the passed in value', () => {
10
+ const onChange = jest.fn();
11
+ const { rerender } = render(
12
+ <AmountInput id="foo" currency="GBP" amount={123456.78} onChange={onChange} />,
13
+ );
14
+
15
+ const input = screen.getByRole<HTMLInputElement>('textbox');
16
+
17
+ expect(input).toHaveValue('123,456.78');
18
+ expect(onChange).not.toHaveBeenCalled();
19
+
20
+ rerender(<AmountInput id="foo" currency="GBP" amount={4} onChange={onChange} />);
21
+ expect(input).toHaveValue('4');
22
+ expect(onChange).not.toHaveBeenCalled();
23
+
24
+ rerender(<AmountInput id="foo" currency="GBP" amount={null} onChange={onChange} />);
25
+ expect(input).toHaveValue('');
26
+ expect(onChange).not.toHaveBeenCalled();
27
+ });
28
+
29
+ it('formats the value while the user is typing', async () => {
30
+ const onChange = jest.fn();
31
+ render(<AmountInput id="foo" currency="GBP" onChange={onChange} />);
32
+
33
+ expect(onChange).not.toHaveBeenCalled();
34
+
35
+ const input = screen.getByRole<HTMLInputElement>('textbox');
36
+
37
+ await userEvent.type(input, '123');
38
+ expect(input).toHaveValue('123');
39
+ expect(onChange).toHaveBeenLastCalledWith(123);
40
+
41
+ await userEvent.type(input, '4');
42
+ expect(input).toHaveValue('1,234');
43
+ expect(onChange).toHaveBeenLastCalledWith(1234);
44
+
45
+ onChange.mockClear();
46
+ expect(onChange).not.toHaveBeenCalled();
47
+ await userEvent.type(input, '.');
48
+ expect(input).toHaveValue('1,234.');
49
+ expect(onChange).not.toHaveBeenCalled();
50
+
51
+ await userEvent.type(input, '5');
52
+ expect(input).toHaveValue('1,234.5');
53
+ expect(onChange).toHaveBeenLastCalledWith(1234.5);
54
+
55
+ await userEvent.type(input, '6');
56
+ expect(input).toHaveValue('1,234.56');
57
+ expect(onChange).toHaveBeenLastCalledWith(1234.56);
58
+
59
+ await userEvent.type(input, '{backspace}');
60
+ expect(input).toHaveValue('1,234.5');
61
+ expect(onChange).toHaveBeenLastCalledWith(1234.5);
62
+
63
+ onChange.mockClear();
64
+ fireEvent.blur(input);
65
+ expect(input).toHaveValue('1,234.50');
66
+ expect(onChange).not.toHaveBeenCalled();
67
+ });
68
+
69
+ it('allows to delete the thousands separator and the preceding digit', async () => {
70
+ const onChange = jest.fn();
71
+ render(<AmountInput id="foo" currency="GBP" onChange={onChange} />);
72
+
73
+ const input = screen.getByRole<HTMLInputElement>('textbox');
74
+
75
+ await userEvent.type(input, '1234');
76
+ expect(input).toHaveValue('1,234');
77
+ expect(onChange).toHaveBeenLastCalledWith(1234);
78
+
79
+ await userEvent.type(input, '{backspace}', {
80
+ initialSelectionStart: 2,
81
+ });
82
+ expect(input).toHaveValue('234');
83
+ expect(onChange).toHaveBeenLastCalledWith(234);
84
+
85
+ await waitFor(() => {
86
+ expect(input.selectionStart).toBe(0);
87
+ });
88
+ });
89
+
90
+ it('allows to paste poorly formatted values', async () => {
91
+ const onChange = jest.fn();
92
+ render(<AmountInput id="foo" currency="GBP" onChange={onChange} />);
93
+
94
+ const input = screen.getByRole<HTMLInputElement>('textbox');
95
+
96
+ await userEvent.click(input);
97
+ await userEvent.paste('1234.5678');
98
+ expect(input).toHaveValue('1,234.56');
99
+ expect(onChange).toHaveBeenLastCalledWith(1234.56);
100
+ });
101
+
102
+ it('allows to paste values', async () => {
103
+ const onChange = jest.fn();
104
+ render(<AmountInput id="foo" currency="GBP" onChange={onChange} />);
105
+
106
+ const input = screen.getByRole<HTMLInputElement>('textbox');
107
+
108
+ await userEvent.click(input);
109
+ await userEvent.paste('1000');
110
+ expect(input).toHaveValue('1,000');
111
+ expect(onChange).toHaveBeenLastCalledWith(1000);
112
+ });
113
+
114
+ it('only allows specific characters to be entered', async () => {
115
+ const onChange = jest.fn();
116
+ render(<AmountInput id="foo" currency="GBP" onChange={onChange} />);
117
+
118
+ const input = screen.getByRole<HTMLInputElement>('textbox');
119
+
120
+ await userEvent.type(input, 'abc');
121
+ expect(input).toHaveValue('');
122
+ expect(onChange).not.toHaveBeenCalled();
123
+
124
+ await userEvent.type(input, '123');
125
+ expect(input).toHaveValue('123');
126
+ expect(onChange).toHaveBeenLastCalledWith(123);
127
+
128
+ await userEvent.type(input, 'def');
129
+ expect(input).toHaveValue('123');
130
+ expect(onChange).toHaveBeenLastCalledWith(123);
131
+
132
+ await userEvent.type(input, '{backspace}');
133
+ expect(input).toHaveValue('12');
134
+ expect(onChange).toHaveBeenLastCalledWith(12);
135
+ });
136
+
137
+ it('does not allow to enter too many decimals', async () => {
138
+ const onChange = jest.fn();
139
+ render(<AmountInput id="foo" currency="GBP" onChange={onChange} />);
140
+
141
+ const input = screen.getByRole<HTMLInputElement>('textbox');
142
+
143
+ await userEvent.type(input, '123.45');
144
+ expect(input).toHaveValue('123.45');
145
+ expect(onChange).toHaveBeenLastCalledWith(123.45);
146
+ onChange.mockClear();
147
+
148
+ await userEvent.type(input, '6');
149
+ expect(input).toHaveValue('123.45');
150
+ expect(onChange).not.toHaveBeenCalled();
151
+ });
152
+
153
+ it('does not allow to enter too many decimal separators', async () => {
154
+ const onChange = jest.fn();
155
+ render(<AmountInput id="foo" currency="GBP" onChange={onChange} />);
156
+
157
+ const input = screen.getByRole<HTMLInputElement>('textbox');
158
+
159
+ await userEvent.type(input, '123.');
160
+ expect(input).toHaveValue('123.');
161
+ expect(onChange).toHaveBeenLastCalledWith(123);
162
+ onChange.mockClear();
163
+
164
+ await userEvent.type(input, '.');
165
+ expect(input).toHaveValue('123.');
166
+ expect(onChange).not.toHaveBeenCalled();
167
+
168
+ await userEvent.type(input, '45');
169
+ expect(input).toHaveValue('123.45');
170
+ expect(onChange).toHaveBeenLastCalledWith(123.45);
171
+ onChange.mockClear();
172
+
173
+ await userEvent.type(input, '.');
174
+ expect(input).toHaveValue('123.45');
175
+ expect(onChange).not.toHaveBeenCalled();
176
+ });
177
+
178
+ it('maintains cursor position after formatting', async () => {
179
+ const onChange = jest.fn();
180
+ render(<AmountInput id="foo" currency="GBP" onChange={onChange} />);
181
+
182
+ const input = screen.getByRole<HTMLInputElement>('textbox');
183
+ await userEvent.type(input, '1234567');
184
+ expect(input).toHaveValue('1,234,567');
185
+ expect(onChange).toHaveBeenLastCalledWith(1234567);
186
+ expect(input.selectionStart).toBe(9);
187
+
188
+ await userEvent.type(input, '9', {
189
+ initialSelectionStart: 4, // Place the cursor between "3" and "4"
190
+ });
191
+ expect(input).toHaveValue('12,394,567');
192
+ expect(onChange).toHaveBeenLastCalledWith(12394567);
193
+
194
+ await waitFor(() => {
195
+ expect(input.selectionStart).toBe(5);
196
+ });
197
+ });
198
+
199
+ it('adds decimal placeholders when the user stars typing decimals', async () => {
200
+ const onChange = jest.fn();
201
+ const { container } = render(<AmountInput id="foo" currency="GBP" onChange={onChange} />);
202
+ const input = screen.getByRole<HTMLInputElement>('textbox');
203
+ await userEvent.type(input, '1234.');
204
+ expect(input).toHaveValue('1,234.');
205
+
206
+ expect(getInputAndAddonContents(container)).toBe('1,234.00');
207
+
208
+ await userEvent.type(input, '5');
209
+ expect(input).toHaveValue('1,234.5');
210
+ expect(getInputAndAddonContents(container)).toBe('1,234.50');
211
+
212
+ await userEvent.type(input, '6');
213
+ expect(input).toHaveValue('1,234.56');
214
+ expect(getInputAndAddonContents(container)).toBe('1,234.56');
215
+ });
216
+
217
+ it('adds decimal placeholders when the user stars typing decimals in Spansh', async () => {
218
+ const onChange = jest.fn();
219
+ const { container } = render(<AmountInput id="foo" currency="GBP" onChange={onChange} />, {
220
+ locale: 'es',
221
+ });
222
+ const input = screen.getByRole<HTMLInputElement>('textbox');
223
+ await userEvent.type(input, '1234,');
224
+ expect(input).toHaveValue('1234,');
225
+
226
+ expect(getInputAndAddonContents(container)).toBe('1234,00');
227
+
228
+ await userEvent.type(input, '5');
229
+ expect(input).toHaveValue('1234,5');
230
+ expect(getInputAndAddonContents(container)).toBe('1234,50');
231
+
232
+ await userEvent.type(input, '6');
233
+ expect(input).toHaveValue('1234,56');
234
+ expect(getInputAndAddonContents(container)).toBe('1234,56');
235
+ });
236
+
237
+ it('adds decimal placeholders when the input is blurred', async () => {
238
+ const onChange = jest.fn();
239
+ const { container } = render(<AmountInput id="foo" currency="GBP" onChange={onChange} />);
240
+ const input = screen.getByRole<HTMLInputElement>('textbox');
241
+ await userEvent.type(input, '1234');
242
+ expect(input).toHaveValue('1,234');
243
+
244
+ fireEvent.blur(input);
245
+ expect(getInputAndAddonContents(container)).toBe('1,234.00');
246
+ });
247
+
248
+ it('adds decimal placeholders when the input is blurred in Spanish', async () => {
249
+ const onChange = jest.fn();
250
+ const { container } = render(<AmountInput id="foo" currency="GBP" onChange={onChange} />, {
251
+ locale: 'es',
252
+ });
253
+ const input = screen.getByRole<HTMLInputElement>('textbox');
254
+ await userEvent.type(input, '1234');
255
+ expect(input).toHaveValue('1234');
256
+
257
+ fireEvent.blur(input);
258
+ expect(getInputAndAddonContents(container)).toBe('1234,00');
259
+ });
260
+
261
+ it('does not allow to enter an amount greater than the maximum', async () => {
262
+ const maxAmount = Number.MAX_SAFE_INTEGER - 1;
263
+ const onChange = jest.fn();
264
+ render(<AmountInput id="foo" currency="GBP" onChange={onChange} />);
265
+
266
+ const input = screen.getByRole<HTMLInputElement>('textbox');
267
+
268
+ await userEvent.type(input, String(maxAmount));
269
+ expect(input).toHaveValue('9,007,199,254,740,990');
270
+ expect(onChange).toHaveBeenLastCalledWith(maxAmount);
271
+
272
+ onChange.mockClear();
273
+ await userEvent.type(input, '1');
274
+ expect(input).toHaveValue('9,007,199,254,740,990');
275
+ expect(onChange).not.toHaveBeenCalled();
276
+ });
277
+ });
278
+
279
+ const getInputAndAddonContents = (container: HTMLElement) => {
280
+ const input = screen.getByRole<HTMLInputElement>('textbox');
281
+ return `${input.value}${container.textContent}`;
282
+ };