@transferwise/components 0.0.0-experimental-b3df26d → 0.0.0-experimental-050f154

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 (121) hide show
  1. package/build/expressiveMoneyInput/AmountInput.js +281 -0
  2. package/build/expressiveMoneyInput/AmountInput.js.map +1 -0
  3. package/build/expressiveMoneyInput/AmountInput.mjs +279 -0
  4. package/build/expressiveMoneyInput/AmountInput.mjs.map +1 -0
  5. package/build/expressiveMoneyInput/AnimatedNumber.js +50 -0
  6. package/build/expressiveMoneyInput/AnimatedNumber.js.map +1 -0
  7. package/build/expressiveMoneyInput/AnimatedNumber.mjs +48 -0
  8. package/build/expressiveMoneyInput/AnimatedNumber.mjs.map +1 -0
  9. package/build/expressiveMoneyInput/Chevron.js +33 -0
  10. package/build/expressiveMoneyInput/Chevron.js.map +1 -0
  11. package/build/expressiveMoneyInput/Chevron.mjs +31 -0
  12. package/build/expressiveMoneyInput/Chevron.mjs.map +1 -0
  13. package/build/expressiveMoneyInput/CurrencySelector.js +160 -0
  14. package/build/expressiveMoneyInput/CurrencySelector.js.map +1 -0
  15. package/build/expressiveMoneyInput/CurrencySelector.mjs +157 -0
  16. package/build/expressiveMoneyInput/CurrencySelector.mjs.map +1 -0
  17. package/build/expressiveMoneyInput/ExpressiveMoneyInput.js +114 -0
  18. package/build/expressiveMoneyInput/ExpressiveMoneyInput.js.map +1 -0
  19. package/build/expressiveMoneyInput/ExpressiveMoneyInput.messages.js +17 -0
  20. package/build/expressiveMoneyInput/ExpressiveMoneyInput.messages.js.map +1 -0
  21. package/build/expressiveMoneyInput/ExpressiveMoneyInput.messages.mjs +13 -0
  22. package/build/expressiveMoneyInput/ExpressiveMoneyInput.messages.mjs.map +1 -0
  23. package/build/expressiveMoneyInput/ExpressiveMoneyInput.mjs +110 -0
  24. package/build/expressiveMoneyInput/ExpressiveMoneyInput.mjs.map +1 -0
  25. package/build/expressiveMoneyInput/useFocus.js +37 -0
  26. package/build/expressiveMoneyInput/useFocus.js.map +1 -0
  27. package/build/expressiveMoneyInput/useFocus.mjs +35 -0
  28. package/build/expressiveMoneyInput/useFocus.mjs.map +1 -0
  29. package/build/expressiveMoneyInput/useInputStyle.js +71 -0
  30. package/build/expressiveMoneyInput/useInputStyle.js.map +1 -0
  31. package/build/expressiveMoneyInput/useInputStyle.mjs +69 -0
  32. package/build/expressiveMoneyInput/useInputStyle.mjs.map +1 -0
  33. package/build/expressiveMoneyInput/utils.js +87 -0
  34. package/build/expressiveMoneyInput/utils.js.map +1 -0
  35. package/build/expressiveMoneyInput/utils.mjs +78 -0
  36. package/build/expressiveMoneyInput/utils.mjs.map +1 -0
  37. package/build/i18n/en.json +2 -0
  38. package/build/i18n/en.json.js +2 -0
  39. package/build/i18n/en.json.js.map +1 -1
  40. package/build/i18n/en.json.mjs +2 -0
  41. package/build/i18n/en.json.mjs.map +1 -1
  42. package/build/index.js +2 -0
  43. package/build/index.js.map +1 -1
  44. package/build/index.mjs +1 -0
  45. package/build/index.mjs.map +1 -1
  46. package/build/main.css +65 -7
  47. package/build/prompt/InlinePrompt/InlinePrompt.js +7 -0
  48. package/build/prompt/InlinePrompt/InlinePrompt.js.map +1 -1
  49. package/build/prompt/InlinePrompt/InlinePrompt.mjs +8 -1
  50. package/build/prompt/InlinePrompt/InlinePrompt.mjs.map +1 -1
  51. package/build/styles/expressiveMoneyInput/AmountInput.css +32 -0
  52. package/build/styles/expressiveMoneyInput/Chevron.css +12 -0
  53. package/build/styles/expressiveMoneyInput/CurrencySelector.css +6 -0
  54. package/build/styles/expressiveMoneyInput/ExpressiveMoneyInput.css +58 -0
  55. package/build/styles/main.css +65 -7
  56. package/build/styles/prompt/InlinePrompt/InlinePrompt.css +7 -7
  57. package/build/types/expressiveMoneyInput/AmountInput.d.ts +13 -0
  58. package/build/types/expressiveMoneyInput/AmountInput.d.ts.map +1 -0
  59. package/build/types/expressiveMoneyInput/AnimatedNumber.d.ts +9 -0
  60. package/build/types/expressiveMoneyInput/AnimatedNumber.d.ts.map +1 -0
  61. package/build/types/expressiveMoneyInput/Chevron.d.ts +6 -0
  62. package/build/types/expressiveMoneyInput/Chevron.d.ts.map +1 -0
  63. package/build/types/expressiveMoneyInput/CurrencySelector.d.ts +30 -0
  64. package/build/types/expressiveMoneyInput/CurrencySelector.d.ts.map +1 -0
  65. package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.d.ts +33 -0
  66. package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.d.ts.map +1 -0
  67. package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.messages.d.ts +12 -0
  68. package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.messages.d.ts.map +1 -0
  69. package/build/types/expressiveMoneyInput/index.d.ts +3 -0
  70. package/build/types/expressiveMoneyInput/index.d.ts.map +1 -0
  71. package/build/types/expressiveMoneyInput/useFocus.d.ts +7 -0
  72. package/build/types/expressiveMoneyInput/useFocus.d.ts.map +1 -0
  73. package/build/types/expressiveMoneyInput/useInputStyle.d.ts +10 -0
  74. package/build/types/expressiveMoneyInput/useInputStyle.d.ts.map +1 -0
  75. package/build/types/expressiveMoneyInput/useSelectionRange.d.ts +10 -0
  76. package/build/types/expressiveMoneyInput/useSelectionRange.d.ts.map +1 -0
  77. package/build/types/expressiveMoneyInput/utils.d.ts +22 -0
  78. package/build/types/expressiveMoneyInput/utils.d.ts.map +1 -0
  79. package/build/types/index.d.ts +2 -0
  80. package/build/types/index.d.ts.map +1 -1
  81. package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts +3 -2
  82. package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts.map +1 -1
  83. package/build/types/test-utils/index.d.ts +4 -0
  84. package/build/types/test-utils/index.d.ts.map +1 -1
  85. package/build/types/withDisplayFormat/WithDisplayFormat.d.ts.map +1 -1
  86. package/build/withDisplayFormat/WithDisplayFormat.js +1 -0
  87. package/build/withDisplayFormat/WithDisplayFormat.js.map +1 -1
  88. package/build/withDisplayFormat/WithDisplayFormat.mjs +1 -0
  89. package/build/withDisplayFormat/WithDisplayFormat.mjs.map +1 -1
  90. package/package.json +1 -1
  91. package/src/expressiveMoneyInput/AmountInput.css +32 -0
  92. package/src/expressiveMoneyInput/AmountInput.less +43 -0
  93. package/src/expressiveMoneyInput/AmountInput.tsx +353 -0
  94. package/src/expressiveMoneyInput/AnimatedNumber.tsx +40 -0
  95. package/src/expressiveMoneyInput/Chevron.css +12 -0
  96. package/src/expressiveMoneyInput/Chevron.less +13 -0
  97. package/src/expressiveMoneyInput/Chevron.tsx +35 -0
  98. package/src/expressiveMoneyInput/CurrencySelector.css +6 -0
  99. package/src/expressiveMoneyInput/CurrencySelector.less +7 -0
  100. package/src/expressiveMoneyInput/CurrencySelector.tsx +218 -0
  101. package/src/expressiveMoneyInput/ExpressiveMoneyInput.css +58 -0
  102. package/src/expressiveMoneyInput/ExpressiveMoneyInput.less +13 -0
  103. package/src/expressiveMoneyInput/ExpressiveMoneyInput.messages.ts +13 -0
  104. package/src/expressiveMoneyInput/ExpressiveMoneyInput.story.tsx +290 -0
  105. package/src/expressiveMoneyInput/ExpressiveMoneyInput.tsx +118 -0
  106. package/src/expressiveMoneyInput/index.ts +2 -0
  107. package/src/expressiveMoneyInput/useFocus.ts +35 -0
  108. package/src/expressiveMoneyInput/useInputStyle.ts +85 -0
  109. package/src/expressiveMoneyInput/useSelectionRange.ts +23 -0
  110. package/src/expressiveMoneyInput/utils.spec.ts +114 -0
  111. package/src/expressiveMoneyInput/utils.ts +116 -0
  112. package/src/i18n/en.json +2 -0
  113. package/src/index.ts +2 -0
  114. package/src/main.css +65 -7
  115. package/src/main.less +1 -0
  116. package/src/prompt/InlinePrompt/InlinePrompt.css +7 -7
  117. package/src/prompt/InlinePrompt/InlinePrompt.less +7 -7
  118. package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +49 -0
  119. package/src/prompt/InlinePrompt/InlinePrompt.tsx +12 -2
  120. package/src/ssr.spec.tsx +1 -0
  121. package/src/withDisplayFormat/WithDisplayFormat.tsx +1 -0
@@ -0,0 +1,43 @@
1
+ .wds-amount-input {
2
+ &-container {
3
+ width: 100%;
4
+ }
5
+
6
+ &-input-container {
7
+ display: flex;
8
+ justify-content: right;
9
+ width: 100%;
10
+ transition:
11
+ font-size 0.4s cubic-bezier(0.3, 0, 0.1, 1),
12
+ height 0.4s cubic-bezier(0.3, 0, 0.1, 1),
13
+ margin-top 0.4s cubic-bezier(0.3, 0, 0.1, 1),
14
+ color 0.4s ease;
15
+ color: var(--color-interactive-primary);
16
+ overflow: hidden;
17
+ margin-bottom: 0 !important;
18
+ }
19
+
20
+ @media (prefers-reduced-motion: reduce) {
21
+ &-input-container {
22
+ transition: none;
23
+ }
24
+ }
25
+
26
+ &-input {
27
+ border: none;
28
+ outline: none;
29
+ flex-grow: 1;
30
+ text-align: right;
31
+ background-color: transparent;
32
+
33
+ &:focus-visible {
34
+ outline: none;
35
+ }
36
+ }
37
+
38
+ &-placeholder {
39
+ flex-grow: 0;
40
+ display: flex;
41
+ align-items: center;
42
+ }
43
+ }
@@ -0,0 +1,353 @@
1
+ import { formatAmount } from '@transferwise/formatting';
2
+ import { clsx } from 'clsx';
3
+ import { AnimatePresence } from 'framer-motion';
4
+ import { type ChangeEvent, type KeyboardEvent, useEffect, useMemo, useRef, useState } from 'react';
5
+ import { useIntl } from 'react-intl';
6
+
7
+ import { Props as ExpressiveMoneyInputProps } from './ExpressiveMoneyInput';
8
+ import { AnimatedNumber } from './AnimatedNumber';
9
+ import { useFocus } from './useFocus';
10
+ import { useInputStyle } from './useInputStyle';
11
+ import {
12
+ getDecimalCount,
13
+ getDecimalSeparator,
14
+ getEnteredDecimalsCount,
15
+ getFormattedString,
16
+ getGroupSeparator,
17
+ getUnformattedNumber,
18
+ isAllowedInputKey,
19
+ isInputPossiblyOverflowing,
20
+ } from './utils';
21
+
22
+ type Props = {
23
+ id: string;
24
+ describedById?: string;
25
+ amount?: number | null;
26
+ currency: string;
27
+ autoFocus?: boolean;
28
+ onChange: (amount: number | null) => void;
29
+ onFocusChange?: (focused: boolean) => void;
30
+ } & Pick<ExpressiveMoneyInputProps, 'loading'>;
31
+
32
+ export const AmountInput = ({
33
+ id,
34
+ describedById,
35
+ amount,
36
+ currency,
37
+ autoFocus,
38
+ onChange,
39
+ onFocusChange,
40
+ loading,
41
+ }: Props) => {
42
+ const intl = useIntl();
43
+ const { focus, setFocus, visualFocus, setVisualFocus } = useFocus();
44
+
45
+ const [value, setValue] = useState<string>(
46
+ amount
47
+ ? getFormattedString({
48
+ value: amount,
49
+ currency,
50
+ locale: intl.locale,
51
+ })
52
+ : '',
53
+ );
54
+ const numericValue = useMemo(() => {
55
+ return getUnformattedNumber({
56
+ value,
57
+ currency,
58
+ locale: intl.locale,
59
+ });
60
+ }, [value, currency, intl.locale]);
61
+
62
+ const valueWithFullDecimals = useMemo(() => {
63
+ return getFormattedString({
64
+ value: numericValue ?? 0,
65
+ currency,
66
+ locale: intl.locale,
67
+ alwaysShowDecimals: true,
68
+ });
69
+ }, [numericValue, currency, intl.locale]);
70
+
71
+ const ref = useRef<HTMLInputElement>(null);
72
+
73
+ useEffect(() => {
74
+ if (autoFocus) {
75
+ ref.current?.focus();
76
+ }
77
+ }, []);
78
+
79
+ const placeholder = getPlaceholder(currency, intl.locale);
80
+ const groupSeparator = getGroupSeparator(currency, intl.locale);
81
+ const decimalSeparator = getDecimalSeparator(currency, intl.locale);
82
+ const maxDecimalCount = getDecimalCount(currency, intl.locale);
83
+
84
+ const decimalPart = getDecimalPart(value, decimalSeparator);
85
+ const decimalMode = decimalSeparator && value.includes(decimalSeparator);
86
+
87
+ useEffect(() => {
88
+ if (!focus) {
89
+ setValue(
90
+ amount
91
+ ? getFormattedString({
92
+ value: amount,
93
+ currency,
94
+ locale: intl.locale,
95
+ })
96
+ : '',
97
+ );
98
+ }
99
+ // eslint-disable-next-line react-hooks/exhaustive-deps
100
+ }, [amount]);
101
+
102
+ useEffect(() => {
103
+ onFocusChange?.(visualFocus);
104
+ }, [visualFocus]);
105
+
106
+ const shouldReformatAfterUserInput = (newValue: string) => {
107
+ // don't reformat if formatting would wipe out user's input
108
+ if (reformatValue(newValue) === '') {
109
+ return false;
110
+ }
111
+
112
+ const endsWithDecimalSeparator = decimalSeparator && newValue.endsWith(decimalSeparator);
113
+ const endsWithGroupSeparator = groupSeparator && newValue.endsWith(groupSeparator);
114
+
115
+ // if the user has entered a seperator to the end, formatting would delete it
116
+ if (endsWithDecimalSeparator || endsWithGroupSeparator) {
117
+ return false;
118
+ }
119
+
120
+ const containsDecimalSeparator = decimalSeparator && newValue.includes(decimalSeparator);
121
+
122
+ if (containsDecimalSeparator) {
123
+ const enteredDecimalsCount = getEnteredDecimalsCount(newValue, decimalSeparator);
124
+ // don't reformat until user has entered all the allowed decimals
125
+ // for example, we don't want 1.1 to be reformatted to 1.10 immediately
126
+ if (enteredDecimalsCount < maxDecimalCount) {
127
+ return false;
128
+ }
129
+ }
130
+
131
+ return true;
132
+ };
133
+
134
+ const reformatValue = (newValue: string) => {
135
+ const unformattedValue = getUnformattedNumber({
136
+ value: newValue,
137
+ currency,
138
+ locale: intl.locale,
139
+ });
140
+ const formattedValue = unformattedValue
141
+ ? getFormattedString({
142
+ value: unformattedValue,
143
+ currency,
144
+ locale: intl.locale,
145
+ })
146
+ : '';
147
+ return formattedValue;
148
+ };
149
+
150
+ const handleChange = (newValue: string) => {
151
+ const oldCursorPosition = ref.current?.selectionStart ?? 0;
152
+
153
+ const newFormattedString = shouldReformatAfterUserInput(newValue)
154
+ ? reformatValue(newValue)
155
+ : newValue;
156
+ setValue(newFormattedString);
157
+
158
+ const newNumber = getUnformattedNumber({
159
+ value: newFormattedString,
160
+ currency,
161
+ locale: intl.locale,
162
+ });
163
+
164
+ if (newNumber !== numericValue) {
165
+ if (numericValue || newNumber) {
166
+ onChange(newNumber);
167
+ }
168
+ }
169
+
170
+ const newCursorPosition = oldCursorPosition + (newFormattedString.length - newValue.length);
171
+ requestAnimationFrame(() => {
172
+ ref?.current?.setSelectionRange(newCursorPosition, newCursorPosition);
173
+ });
174
+ };
175
+
176
+ const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
177
+ e.preventDefault();
178
+
179
+ const clipboardData = e.clipboardData?.getData('text/plain');
180
+ if (!clipboardData) {
181
+ return;
182
+ }
183
+
184
+ // need to sanitise the pasted value otherwise other validation logic will ignore the input entirely
185
+ const sanitisedValue = reformatValue(clipboardData);
186
+
187
+ handleChange(sanitisedValue);
188
+ };
189
+
190
+ const handleBlur = () => {
191
+ setFocus(false);
192
+ setValue(reformatValue(value));
193
+ };
194
+
195
+ const handleBackspace = (e: KeyboardEvent<HTMLInputElement>) => {
196
+ const input = e.target as HTMLInputElement;
197
+ // using the updated selection range after the backspace key has been processed, instead of the current selection range in state
198
+ const { value: currentValue, selectionStart, selectionEnd } = input;
199
+
200
+ if (selectionStart === selectionEnd && selectionStart && selectionStart > 0) {
201
+ const charBeforeCursor = currentValue[selectionStart - 1];
202
+
203
+ // if the user deletes a thousands separator, remove the digit before it as well
204
+ if (charBeforeCursor === groupSeparator) {
205
+ e.preventDefault();
206
+ const beforeCursor = currentValue.slice(0, selectionStart - 2);
207
+ const afterCursor = currentValue.slice(selectionStart);
208
+ const newValue = `${beforeCursor}${afterCursor}`;
209
+ input.setSelectionRange(beforeCursor.length, beforeCursor.length);
210
+ handleChange(newValue);
211
+ }
212
+ }
213
+ };
214
+
215
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
216
+ setFocus(true);
217
+ if (!isAllowedInputKey(e)) {
218
+ e.preventDefault();
219
+ }
220
+
221
+ if (e.key === 'Backspace') {
222
+ handleBackspace(e);
223
+ }
224
+ };
225
+
226
+ const isAllowedInput = (e: ChangeEvent<HTMLInputElement>) => {
227
+ const hasMultipleDecimalSeparators =
228
+ decimalSeparator && e.target.value.split(decimalSeparator).length > 2;
229
+ if (hasMultipleDecimalSeparators) {
230
+ return false;
231
+ }
232
+
233
+ const newNumericValue = getUnformattedNumber({
234
+ value: e.target.value,
235
+ currency,
236
+ locale: intl.locale,
237
+ });
238
+ const maxLength = Number.MAX_SAFE_INTEGER.toString().length;
239
+ if (String(newNumericValue).length > maxLength) {
240
+ return false;
241
+ }
242
+
243
+ return true;
244
+ };
245
+
246
+ const addonContent = useMemo((): string | null | undefined => {
247
+ // because we're using a separate "addon" element for the placeholder decimals, there is a possibility that the input itself will become scrollable
248
+ // and the decimals will appear on top of the input. Safest thing to do is to just hide the addon if there is not enough room
249
+ if (isInputPossiblyOverflowing({ ref, value })) {
250
+ return null;
251
+ }
252
+ if (!decimalSeparator || !value) {
253
+ return null;
254
+ }
255
+
256
+ // if the user has typed a decimal separator, show the full decimal part as a placeholder
257
+ // this returns a string even if there is no content, typing should replace the placeholder immediately without animation
258
+ // otherwise there is an ugly animation when going from 1.23 to 1.2 due to AnimatePresence
259
+ if (focus && decimalMode) {
260
+ // reuse getDecimalPart
261
+ const fullDecimalPart = getDecimalPart(valueWithFullDecimals, decimalSeparator);
262
+ // show only the characters that are not already displayed by the input
263
+ return fullDecimalPart?.slice(decimalPart?.length);
264
+ }
265
+
266
+ // in unfocused state, always show the full decimal part unless the user has already entered decimals
267
+ if (!focus && !decimalMode) {
268
+ const [_, decimalPlaceholder] = placeholder.split(decimalSeparator);
269
+ return decimalSeparator + decimalPlaceholder;
270
+ }
271
+
272
+ return null;
273
+ }, [
274
+ decimalMode,
275
+ decimalPart?.length,
276
+ decimalSeparator,
277
+ focus,
278
+ placeholder,
279
+ value,
280
+ valueWithFullDecimals,
281
+ ]);
282
+
283
+ const style = useInputStyle({
284
+ // whenever decimals are shown, we need to account for the full decimal part for the font size calculation
285
+ value: addonContent ? valueWithFullDecimals : value,
286
+ focus: visualFocus,
287
+ inputElement: ref.current,
288
+ loading,
289
+ });
290
+
291
+ return (
292
+ <div className="wds-amount-input-container">
293
+ <div
294
+ className={clsx('wds-amount-input-input-container', 'np-text-display-large')}
295
+ style={style}
296
+ >
297
+ <input
298
+ ref={ref}
299
+ className="wds-amount-input-input"
300
+ id={id}
301
+ autoComplete="off"
302
+ inputMode="decimal"
303
+ value={value}
304
+ type="text"
305
+ placeholder={placeholder}
306
+ aria-describedby={describedById}
307
+ /* without this, the input tries to keep an aspect ratio and doesn't respect CSS width rules */
308
+ size={1}
309
+ onChange={(e) => {
310
+ if (isAllowedInput(e)) {
311
+ handleChange(e.target.value);
312
+ }
313
+ }}
314
+ onBlurCapture={() => handleBlur()}
315
+ onPaste={(e) => handlePaste(e)}
316
+ onFocus={() => {
317
+ setFocus(true);
318
+ }}
319
+ onBlur={() => {
320
+ setTimeout(() => setVisualFocus(false), 30);
321
+ }}
322
+ onKeyDown={(e) => handleKeyDown(e)}
323
+ />
324
+ <AnimatePresence initial={false}>
325
+ {addonContent !== null && (
326
+ <AnimatedNumber
327
+ className={clsx(
328
+ 'wds-amount-input-placeholder',
329
+ visualFocus && 'wds-amount-input-placeholder-focus',
330
+ )}
331
+ onClick={() => ref.current?.focus()}
332
+ >
333
+ {addonContent}
334
+ </AnimatedNumber>
335
+ )}
336
+ </AnimatePresence>
337
+ </div>
338
+ </div>
339
+ );
340
+ };
341
+
342
+ const getPlaceholder = (currency: string, locale: string) => {
343
+ return formatAmount(0, currency, locale, { alwaysShowDecimals: true });
344
+ };
345
+
346
+ const getDecimalPart = (value: string, decimalSeparator: string | null) => {
347
+ if (!value || !decimalSeparator) {
348
+ return undefined;
349
+ }
350
+
351
+ const [_, decimalPart] = value.split(decimalSeparator);
352
+ return decimalPart ?? undefined;
353
+ };
@@ -0,0 +1,40 @@
1
+ import { motion, useReducedMotion } from 'framer-motion';
2
+ import type { ReactNode } from 'react';
3
+
4
+ interface Props {
5
+ children: ReactNode;
6
+ onClick?: () => void;
7
+ className?: string;
8
+ }
9
+
10
+ export const AnimatedNumber = ({ children, onClick, className }: Props) => {
11
+ const reducedMotion = useReducedMotion();
12
+
13
+ return (
14
+ <motion.span
15
+ className={className}
16
+ aria-hidden
17
+ initial={{ zoom: 0.01 }}
18
+ animate={{ zoom: 1 }}
19
+ exit={{ zoom: 0.01 }}
20
+ transition={{
21
+ duration: reducedMotion ? 0 : 0.3,
22
+ type: 'tween',
23
+ ease: [0.3, 0, 0.1, 1],
24
+ }}
25
+ onClick={() => onClick?.()}
26
+ >
27
+ <motion.span
28
+ initial={{ opacity: 0 }}
29
+ animate={{ opacity: [0, 0, 1] }}
30
+ exit={{ opacity: [1, 0, 0] }}
31
+ transition={{
32
+ duration: reducedMotion ? 0 : 0.3,
33
+ times: [0, 0.5, 1],
34
+ }}
35
+ >
36
+ {children}
37
+ </motion.span>
38
+ </motion.span>
39
+ );
40
+ };
@@ -0,0 +1,12 @@
1
+ .wds-chevron-container {
2
+ width: 32px;
3
+ width: var(--size-32);
4
+ overflow: hidden;
5
+ color: var(--color-interactive-primary);
6
+ margin-left: 8px;
7
+ margin-left: var(--size-8);
8
+ transition: width 0.3s ease;
9
+ }
10
+ .wds-chevron-hidden {
11
+ width: 0;
12
+ }
@@ -0,0 +1,13 @@
1
+ .wds-chevron {
2
+ &-container {
3
+ width: var(--size-32);
4
+ overflow: hidden;
5
+ color: var(--color-interactive-primary);
6
+ margin-left: var(--size-8);
7
+ transition: width 0.3s ease;
8
+ }
9
+
10
+ &-hidden {
11
+ width: 0;
12
+ }
13
+ }
@@ -0,0 +1,35 @@
1
+ import { ChevronLeft } from '@transferwise/icons';
2
+ import { clsx } from 'clsx';
3
+ import { motion } from 'framer-motion';
4
+
5
+ interface Props {
6
+ shouldShow: boolean;
7
+ }
8
+
9
+ export const Chevron = ({ shouldShow = true }: Props) => {
10
+ return (
11
+ <div
12
+ className={clsx(
13
+ 'd-flex align-items-center',
14
+ 'wds-chevron-container',
15
+ !shouldShow && 'wds-chevron-hidden',
16
+ )}
17
+ >
18
+ <motion.div
19
+ animate={{
20
+ x: [12, 0, 0, -12],
21
+ opacity: [0, 1, 1, 0],
22
+ }}
23
+ transition={{
24
+ duration: 3,
25
+ ease: [[0.3, 0, 0.1, 1], 'linear', [0.3, 0, 0.1, 1]],
26
+ times: [0, 0.1, 0.9, 1],
27
+ repeat: Infinity,
28
+ repeatType: 'loop',
29
+ }}
30
+ >
31
+ <ChevronLeft size={24} />
32
+ </motion.div>
33
+ </div>
34
+ );
35
+ };
@@ -0,0 +1,6 @@
1
+ .wds-currency-selector:disabled {
2
+ opacity: 1 !important;
3
+ cursor: auto !important;
4
+ cursor: initial !important;
5
+ mix-blend-mode: initial !important;
6
+ }
@@ -0,0 +1,7 @@
1
+ .wds-currency-selector {
2
+ &:disabled {
3
+ opacity: 1 !important;
4
+ cursor: initial !important;
5
+ mix-blend-mode: initial !important;
6
+ }
7
+ }