@transferwise/components 0.0.0-experimental-d3453cf → 0.0.0-experimental-3f5dc0d

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 (104) 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 +113 -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 +109 -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 +58 -0
  47. package/build/styles/expressiveMoneyInput/AmountInput.css +32 -0
  48. package/build/styles/expressiveMoneyInput/Chevron.css +12 -0
  49. package/build/styles/expressiveMoneyInput/CurrencySelector.css +6 -0
  50. package/build/styles/expressiveMoneyInput/ExpressiveMoneyInput.css +58 -0
  51. package/build/styles/main.css +58 -0
  52. package/build/types/expressiveMoneyInput/AmountInput.d.ts +13 -0
  53. package/build/types/expressiveMoneyInput/AmountInput.d.ts.map +1 -0
  54. package/build/types/expressiveMoneyInput/AnimatedNumber.d.ts +9 -0
  55. package/build/types/expressiveMoneyInput/AnimatedNumber.d.ts.map +1 -0
  56. package/build/types/expressiveMoneyInput/Chevron.d.ts +6 -0
  57. package/build/types/expressiveMoneyInput/Chevron.d.ts.map +1 -0
  58. package/build/types/expressiveMoneyInput/CurrencySelector.d.ts +30 -0
  59. package/build/types/expressiveMoneyInput/CurrencySelector.d.ts.map +1 -0
  60. package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.d.ts +30 -0
  61. package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.d.ts.map +1 -0
  62. package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.messages.d.ts +12 -0
  63. package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.messages.d.ts.map +1 -0
  64. package/build/types/expressiveMoneyInput/index.d.ts +3 -0
  65. package/build/types/expressiveMoneyInput/index.d.ts.map +1 -0
  66. package/build/types/expressiveMoneyInput/useFocus.d.ts +7 -0
  67. package/build/types/expressiveMoneyInput/useFocus.d.ts.map +1 -0
  68. package/build/types/expressiveMoneyInput/useInputStyle.d.ts +10 -0
  69. package/build/types/expressiveMoneyInput/useInputStyle.d.ts.map +1 -0
  70. package/build/types/expressiveMoneyInput/useSelectionRange.d.ts +10 -0
  71. package/build/types/expressiveMoneyInput/useSelectionRange.d.ts.map +1 -0
  72. package/build/types/expressiveMoneyInput/utils.d.ts +22 -0
  73. package/build/types/expressiveMoneyInput/utils.d.ts.map +1 -0
  74. package/build/types/index.d.ts +2 -0
  75. package/build/types/index.d.ts.map +1 -1
  76. package/build/types/test-utils/index.d.ts +4 -0
  77. package/build/types/test-utils/index.d.ts.map +1 -1
  78. package/package.json +9 -2
  79. package/src/expressiveMoneyInput/AmountInput.css +32 -0
  80. package/src/expressiveMoneyInput/AmountInput.less +43 -0
  81. package/src/expressiveMoneyInput/AmountInput.tsx +353 -0
  82. package/src/expressiveMoneyInput/AnimatedNumber.tsx +40 -0
  83. package/src/expressiveMoneyInput/Chevron.css +12 -0
  84. package/src/expressiveMoneyInput/Chevron.less +13 -0
  85. package/src/expressiveMoneyInput/Chevron.tsx +35 -0
  86. package/src/expressiveMoneyInput/CurrencySelector.css +6 -0
  87. package/src/expressiveMoneyInput/CurrencySelector.less +7 -0
  88. package/src/expressiveMoneyInput/CurrencySelector.tsx +218 -0
  89. package/src/expressiveMoneyInput/ExpressiveMoneyInput.css +58 -0
  90. package/src/expressiveMoneyInput/ExpressiveMoneyInput.less +13 -0
  91. package/src/expressiveMoneyInput/ExpressiveMoneyInput.messages.ts +13 -0
  92. package/src/expressiveMoneyInput/ExpressiveMoneyInput.story.tsx +188 -0
  93. package/src/expressiveMoneyInput/ExpressiveMoneyInput.tsx +124 -0
  94. package/src/expressiveMoneyInput/index.ts +2 -0
  95. package/src/expressiveMoneyInput/useFocus.ts +35 -0
  96. package/src/expressiveMoneyInput/useInputStyle.ts +85 -0
  97. package/src/expressiveMoneyInput/useSelectionRange.ts +23 -0
  98. package/src/expressiveMoneyInput/utils.spec.ts +114 -0
  99. package/src/expressiveMoneyInput/utils.ts +116 -0
  100. package/src/i18n/en.json +2 -0
  101. package/src/index.ts +2 -0
  102. package/src/main.css +58 -0
  103. package/src/main.less +1 -0
  104. package/src/ssr.spec.tsx +1 -0
@@ -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
+ }
@@ -0,0 +1,218 @@
1
+ import type { AvatarLayoutProps } from '../avatarLayout';
2
+ import Button from '../button';
3
+ import {
4
+ SelectInput,
5
+ SelectInputOptionContent,
6
+ SelectInputTriggerButton,
7
+ } from '../inputs/SelectInput';
8
+ import { CurrencyType, Props as ExpressiveMoneyInputProps } from './ExpressiveMoneyInput';
9
+ import { ChevronDown } from '@transferwise/icons';
10
+ import { Flag } from '@wise/art';
11
+ import {
12
+ type ButtonHTMLAttributes,
13
+ forwardRef,
14
+ type MouseEventHandler,
15
+ useMemo,
16
+ useState,
17
+ } from 'react';
18
+ import { useIntl } from 'react-intl';
19
+
20
+ import messages from './ExpressiveMoneyInput.messages';
21
+
22
+ export interface CurrencyOption {
23
+ label?: string;
24
+ code: string;
25
+ keywords: string[] | undefined;
26
+ }
27
+
28
+ export interface CurrencySection {
29
+ title: string;
30
+ currencies: CurrencyOption[];
31
+ }
32
+
33
+ export type CurrencyOptions = CurrencySection[];
34
+
35
+ export type Props = {
36
+ id: string;
37
+ labelId: string;
38
+ options?: CurrencyOptions;
39
+ onChange?: (currency: CurrencyType) => void;
40
+ onOpen?: () => void;
41
+ addons?: AvatarLayoutProps['avatars'];
42
+ onSearchChange?: (payload: { query: string; resultCount: number }) => void;
43
+ } & Pick<ExpressiveMoneyInputProps, 'currency'>;
44
+
45
+ export const CurrencySelector = ({
46
+ id,
47
+ currency,
48
+ options = [],
49
+ labelId,
50
+ onChange,
51
+ addons,
52
+ onOpen,
53
+ onSearchChange,
54
+ }: Props) => {
55
+ const intl = useIntl();
56
+
57
+ const allCurrencyOptions = useMemo(() => getUniqueCurrencies(options), [options]);
58
+
59
+ const activeCurrencyOption = useMemo(() => {
60
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
61
+ return allCurrencyOptions.find((option) => option.code === currency)!;
62
+ }, [currency, allCurrencyOptions]);
63
+
64
+ const disabled =
65
+ !onChange ||
66
+ options.length === 0 ||
67
+ (options.length === 1 && options[0].currencies.length <= 1);
68
+
69
+ const [searchQuery, setSearchQuery] = useState<string>('');
70
+
71
+ const handleTriggerClick: MouseEventHandler = (event) => {
72
+ const triggerEl = event.currentTarget;
73
+ if (triggerEl?.getAttribute('aria-expanded') === 'false') {
74
+ onOpen?.();
75
+ }
76
+ };
77
+
78
+ const items = searchQuery
79
+ ? filterAndSortCurrenciesForQuery(allCurrencyOptions, searchQuery).map(getCurrencySelectOption)
80
+ : options.map(getCurrencyGroup);
81
+
82
+ return (
83
+ <SelectInput
84
+ compareValues="code"
85
+ disabled={disabled}
86
+ id={id}
87
+ value={activeCurrencyOption}
88
+ filterable
89
+ filterPlaceholder={intl.formatMessage(messages.currencySelectorSearchPlaceholder)}
90
+ UNSAFE_triggerButtonProps={{
91
+ id: undefined,
92
+ 'aria-labelledby': undefined,
93
+ 'aria-describedby': labelId,
94
+ 'aria-invalid': undefined,
95
+ 'aria-label': intl.formatMessage(messages.currencySelectorSelectCurrency),
96
+ }}
97
+ items={items}
98
+ renderValue={({ code, label }) => {
99
+ return (
100
+ <SelectInputOptionContent
101
+ title={code}
102
+ note={label}
103
+ icon={<Flag code={code} intrinsicSize={24} />}
104
+ />
105
+ );
106
+ }}
107
+ renderTrigger={() => (
108
+ <SelectInputTriggerButton
109
+ as={ButtonInput}
110
+ // @ts-expect-error new (v2) ButtonProps
111
+ addonStart={{
112
+ type: 'avatar',
113
+ value: [
114
+ addons ? addons[0] : null,
115
+ {
116
+ ...(addons && addons.length > 1
117
+ ? { ...addons[1] }
118
+ : {
119
+ asset: <Flag code={currency} />,
120
+ }),
121
+ },
122
+ ].filter((avatar) => Boolean(avatar)),
123
+ }}
124
+ addonEnd={disabled ? undefined : { type: 'icon', value: <ChevronDown /> }}
125
+ onClick={(event) => handleTriggerClick(event)}
126
+ >
127
+ {currency}
128
+ </SelectInputTriggerButton>
129
+ )}
130
+ onChange={(newValue) => {
131
+ onChange?.(newValue.code);
132
+ }}
133
+ onFilterChange={({ queryNormalized }) => {
134
+ setSearchQuery(queryNormalized ?? '');
135
+ if (queryNormalized) {
136
+ onSearchChange?.({
137
+ query: queryNormalized,
138
+ resultCount: filterAndSortCurrenciesForQuery(allCurrencyOptions, queryNormalized)
139
+ .length,
140
+ });
141
+ }
142
+ }}
143
+ />
144
+ );
145
+ };
146
+
147
+ export const ButtonInput = forwardRef(function ButtonInput(
148
+ { children, ...rest }: React.PropsWithChildren<ButtonHTMLAttributes<HTMLButtonElement>>,
149
+ ref: React.ForwardedRef<HTMLButtonElement | null>,
150
+ ) {
151
+ return (
152
+ <Button
153
+ ref={ref}
154
+ size="md"
155
+ v2
156
+ className="wds-currency-selector"
157
+ priority="secondary-neutral"
158
+ {...rest}
159
+ >
160
+ {children}
161
+ </Button>
162
+ );
163
+ });
164
+
165
+ const getCurrencySelectOption = (currency: CurrencyOption) => {
166
+ return {
167
+ type: 'option' as const,
168
+ value: currency,
169
+ filterMatchers: currency.keywords,
170
+ };
171
+ };
172
+
173
+ const getCurrencyGroup = (section: CurrencySection) => {
174
+ return {
175
+ type: 'group' as const,
176
+ label: section.title,
177
+ options: section.currencies.map(getCurrencySelectOption),
178
+ };
179
+ };
180
+
181
+ const getUniqueCurrencies = (options: CurrencyOptions) => {
182
+ const allCurrencyOptions = options.flatMap((section) => section.currencies);
183
+ const uniqueCurrencies = new Map<string, CurrencyOption>();
184
+
185
+ allCurrencyOptions.forEach((currencyObj) => {
186
+ uniqueCurrencies.set(currencyObj.code, currencyObj);
187
+ });
188
+
189
+ return Array.from(uniqueCurrencies.values());
190
+ };
191
+
192
+ const filterAndSortCurrenciesForQuery = (
193
+ currencies: CurrencyOption[],
194
+ query: string,
195
+ ): CurrencyOption[] => {
196
+ return (
197
+ currencies
198
+ .filter((currency) => {
199
+ return (
200
+ currency.code.toLowerCase().includes(query) ||
201
+ (currency.label ?? '').toLowerCase().includes(query) ||
202
+ currency.keywords?.some((keyword) => keyword.toLowerCase().includes(query))
203
+ );
204
+ })
205
+ // prefer exact matches, then sort alphabetically by code
206
+ .sort((a, b) => {
207
+ const aCode = a.code.toLowerCase();
208
+ const bCode = b.code.toLowerCase();
209
+ if (aCode === query) {
210
+ return -1;
211
+ }
212
+ if (bCode === query) {
213
+ return 1;
214
+ }
215
+ return aCode.localeCompare(bCode);
216
+ })
217
+ );
218
+ };
@@ -0,0 +1,58 @@
1
+ .wds-amount-input-container {
2
+ width: 100%;
3
+ }
4
+ .wds-amount-input-input-container {
5
+ display: flex;
6
+ justify-content: right;
7
+ width: 100%;
8
+ transition: font-size 0.4s cubic-bezier(0.3, 0, 0.1, 1), height 0.4s cubic-bezier(0.3, 0, 0.1, 1), margin-top 0.4s cubic-bezier(0.3, 0, 0.1, 1), color 0.4s ease;
9
+ color: var(--color-interactive-primary);
10
+ overflow: hidden;
11
+ margin-bottom: 0 !important;
12
+ }
13
+ @media (prefers-reduced-motion: reduce) {
14
+ .wds-amount-input-input-container {
15
+ transition: none;
16
+ }
17
+ }
18
+ .wds-amount-input-input {
19
+ border: none;
20
+ outline: none;
21
+ flex-grow: 1;
22
+ text-align: right;
23
+ background-color: transparent;
24
+ }
25
+ .wds-amount-input-input:focus-visible {
26
+ outline: none;
27
+ }
28
+ .wds-amount-input-placeholder {
29
+ flex-grow: 0;
30
+ display: flex;
31
+ align-items: center;
32
+ }
33
+ .wds-currency-selector:disabled {
34
+ opacity: 1 !important;
35
+ cursor: auto !important;
36
+ cursor: initial !important;
37
+ mix-blend-mode: initial !important;
38
+ }
39
+ .wds-chevron-container {
40
+ width: 32px;
41
+ width: var(--size-32);
42
+ overflow: hidden;
43
+ color: var(--color-interactive-primary);
44
+ margin-left: 8px;
45
+ margin-left: var(--size-8);
46
+ transition: width 0.3s ease;
47
+ }
48
+ .wds-chevron-hidden {
49
+ width: 0;
50
+ }
51
+ .wds-expressive-money-input-currency-selector {
52
+ flex-shrink: 0;
53
+ margin-right: 24px;
54
+ margin-right: var(--size-24);
55
+ }
56
+ .wds-expressive-money-input-chevron {
57
+ transform: translateY(-5%);
58
+ }
@@ -0,0 +1,13 @@
1
+ @import "./AmountInput.less";
2
+ @import "./CurrencySelector.less";
3
+ @import "./Chevron.less";
4
+
5
+ .wds-expressive-money-input {
6
+ &-currency-selector {
7
+ flex-shrink: 0;
8
+ margin-right: var(--size-24);
9
+ }
10
+ &-chevron {
11
+ transform: translateY(-5%);
12
+ }
13
+ }
@@ -0,0 +1,13 @@
1
+ import { defineMessages } from 'react-intl';
2
+
3
+ export default defineMessages({
4
+ currencySelectorSearchPlaceholder: {
5
+ id: 'neptune.ExpressiveMoneyInput.currency.search.placeholder',
6
+ defaultMessage: 'Type a currency / country',
7
+ },
8
+
9
+ currencySelectorSelectCurrency: {
10
+ id: 'neptune.ExpressiveMoneyInput.currency.select.currency',
11
+ defaultMessage: 'Select currency',
12
+ },
13
+ });
@@ -0,0 +1,188 @@
1
+ /* eslint-disable jsx-a11y/anchor-is-valid, no-console, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
2
+ import { Meta } from '@storybook/react-webpack5';
3
+ import ExpressiveMoneyInput, { Props as ExpressiveMoneyInputProps } from './ExpressiveMoneyInput';
4
+ import { lorem10 } from '../test-utils';
5
+ import { Sentiment } from '../common';
6
+ import Image from '../image';
7
+ import Link from '../link';
8
+ import Money from '../money';
9
+ import React from 'react';
10
+
11
+ export default {
12
+ title: 'Forms/ExpressiveMoneyInput',
13
+ component: ExpressiveMoneyInput,
14
+ } as Meta;
15
+
16
+ const props: ExpressiveMoneyInputProps = {
17
+ label: 'You send',
18
+ currency: 'GBP',
19
+ amount: 1234.56,
20
+ onAmountChange: (amount) => {
21
+ console.log('Amount changed', amount);
22
+ },
23
+ currencySelector: {
24
+ addons: [
25
+ { asset: <Image src="../avatar-square-dude.webp" alt="" /> },
26
+ { profileName: 'Test Test' },
27
+ ],
28
+ options: [
29
+ {
30
+ title: 'Popular',
31
+ currencies: [
32
+ { code: 'USD', label: 'US Dollar', keywords: ['dollar', 'us'] },
33
+ { code: 'AUD', label: 'Australia Dollar', keywords: ['dollar', 'us'] },
34
+ ],
35
+ },
36
+ {
37
+ title: 'Others',
38
+ currencies: [
39
+ { code: 'GBP', label: 'Pound', keywords: ['british'] },
40
+ { code: 'EUR', label: 'Euro', keywords: ['euro'] },
41
+ ],
42
+ },
43
+ ],
44
+ onOpen: () => {
45
+ console.log('Currency selector opened');
46
+ },
47
+ onSearchChange: (payload) => {
48
+ console.log('Search changed', payload);
49
+ },
50
+ onChange: (currency) => {
51
+ console.log('Currency changed', currency);
52
+ },
53
+ },
54
+ loading: false,
55
+ inlinePrompt: { message: lorem10, sentiment: Sentiment.POSITIVE },
56
+ showChevron: true,
57
+ autoFocus: true,
58
+ };
59
+
60
+ export const NullAmount = {
61
+ args: props,
62
+ render: (args: ExpressiveMoneyInputProps) => (
63
+ <ExpressiveMoneyInput
64
+ label={args.label}
65
+ amount={null}
66
+ currency="EUR"
67
+ onAmountChange={args.onAmountChange}
68
+ />
69
+ ),
70
+ };
71
+
72
+ export const WithInitAmount = {
73
+ args: props,
74
+ render: (args: ExpressiveMoneyInputProps) => {
75
+ const availableBalance = 1500;
76
+ return (
77
+ <ExpressiveMoneyInput
78
+ label={args.label}
79
+ amount={availableBalance}
80
+ currency="EUR"
81
+ onAmountChange={args.onAmountChange}
82
+ />
83
+ );
84
+ },
85
+ };
86
+
87
+ export const WithLoading = {
88
+ args: props,
89
+ render: (args: ExpressiveMoneyInputProps) => {
90
+ const availableBalance = 1500;
91
+ return (
92
+ <ExpressiveMoneyInput
93
+ label={args.label}
94
+ amount={availableBalance}
95
+ currency="EUR"
96
+ loading
97
+ onAmountChange={args.onAmountChange}
98
+ />
99
+ );
100
+ },
101
+ };
102
+
103
+ export const WithoutChevron = {
104
+ args: props,
105
+ render: (args: ExpressiveMoneyInputProps) => {
106
+ const availableBalance = 1500;
107
+ return (
108
+ <ExpressiveMoneyInput
109
+ label={args.label}
110
+ amount={availableBalance}
111
+ currency="EUR"
112
+ showChevron={false}
113
+ onAmountChange={args.onAmountChange}
114
+ />
115
+ );
116
+ },
117
+ };
118
+
119
+ export const WithCurrencySelector = {
120
+ args: props,
121
+ render: (args: ExpressiveMoneyInputProps) => {
122
+ const availableBalance = 1500;
123
+ return (
124
+ <ExpressiveMoneyInput
125
+ label={args.label}
126
+ amount={availableBalance}
127
+ currency="EUR"
128
+ currencySelector={args.currencySelector}
129
+ onAmountChange={args.onAmountChange}
130
+ />
131
+ );
132
+ },
133
+ };
134
+
135
+ export const WithInlinePromptNoSentiment = {
136
+ args: props,
137
+ render: (args: ExpressiveMoneyInputProps) => {
138
+ const availableBalance = 1500;
139
+ const [sourceAmount, setSourceAmount] = React.useState(args.amount);
140
+ return (
141
+ <ExpressiveMoneyInput
142
+ label={args.label}
143
+ amount={sourceAmount}
144
+ currency="EUR"
145
+ currencySelector={args.currencySelector}
146
+ inlinePrompt={{
147
+ message: (
148
+ <>
149
+ {`Available balance `}
150
+ <Link
151
+ onClick={() => {
152
+ setSourceAmount(availableBalance);
153
+ }}
154
+ >
155
+ <Money amount={availableBalance} currency={args.currency} />
156
+ </Link>
157
+ </>
158
+ ),
159
+ }}
160
+ onAmountChange={args.onAmountChange}
161
+ />
162
+ );
163
+ },
164
+ };
165
+
166
+ export const WithInlinePromptSentiment = {
167
+ args: props,
168
+ render: (args: ExpressiveMoneyInputProps) => {
169
+ const availableBalance = 1500;
170
+ return (
171
+ <ExpressiveMoneyInput
172
+ label={args.label}
173
+ amount={availableBalance}
174
+ currency="EUR"
175
+ currencySelector={args.currencySelector}
176
+ inlinePrompt={args.inlinePrompt}
177
+ onAmountChange={args.onAmountChange}
178
+ />
179
+ );
180
+ },
181
+ };
182
+
183
+ export const Autofocus = {
184
+ args: props,
185
+ render: (args: ExpressiveMoneyInputProps) => (
186
+ <ExpressiveMoneyInput {...args} currency="MXN" inlinePrompt={undefined} />
187
+ ),
188
+ };
@@ -0,0 +1,124 @@
1
+ import Body from '../body';
2
+ import { Label } from '../label/Label';
3
+ import { clsx } from 'clsx';
4
+ import { AnimatePresence, motion } from 'framer-motion';
5
+ import { useId, type ReactNode } from 'react';
6
+
7
+ import { type Props as CurrencySelectorProps, CurrencySelector } from './CurrencySelector';
8
+ import { CommonProps } from '../common';
9
+ import { AmountInput } from './AmountInput';
10
+ import { Chevron } from './Chevron';
11
+ import { InlinePrompt, type InlinePromptProps } from '../prompt/InlinePrompt';
12
+
13
+ type AmountType = number | null;
14
+ export type CurrencyType = string;
15
+
16
+ type DefaultCurrencySelectorInstanceType = Pick<
17
+ CurrencySelectorProps,
18
+ 'addons' | 'options' | 'onChange' | 'onOpen' | 'onSearchChange'
19
+ >;
20
+ type CustomCurrencySelectorInstanceType = (props: { id: string; labelId: string }) => ReactNode;
21
+ type CurrencySelectorType =
22
+ | DefaultCurrencySelectorInstanceType
23
+ | CustomCurrencySelectorInstanceType;
24
+
25
+ export type Props = {
26
+ label?: ReactNode;
27
+ currencySelector?: CurrencySelectorType;
28
+ amount?: AmountType;
29
+ currency: CurrencyType;
30
+ inlinePrompt?: {
31
+ sentiment?: InlinePromptProps['sentiment'];
32
+ message: InlinePromptProps['children'];
33
+ };
34
+ showChevron?: boolean;
35
+ autoFocus?: boolean;
36
+ loading?: boolean;
37
+ onAmountChange: (amount: AmountType) => void;
38
+ onFocusChange?: (focused: boolean) => void;
39
+ } & CommonProps;
40
+
41
+ export default function ExpressiveMoneyInput({
42
+ label,
43
+ currency,
44
+ currencySelector = { options: [] } as DefaultCurrencySelectorInstanceType,
45
+ amount,
46
+ onAmountChange,
47
+ className,
48
+ inlinePrompt,
49
+ showChevron,
50
+ autoFocus,
51
+ loading,
52
+ onFocusChange,
53
+ }: Props) {
54
+ const inputId = useId();
55
+ const labelId = useId();
56
+ const customAlertId = useId();
57
+ const currencyId = useId();
58
+
59
+ const selector =
60
+ typeof currencySelector === 'function' ? (
61
+ currencySelector({ id: currencyId, labelId })
62
+ ) : (
63
+ <CurrencySelector
64
+ id={currencyId}
65
+ labelId={labelId}
66
+ currency={currency}
67
+ {...currencySelector}
68
+ />
69
+ );
70
+
71
+ return (
72
+ <div className={clsx('wds-expressive-money-input', className)}>
73
+ <Label id={labelId} htmlFor={inputId} className={clsx('m-b-1', 'font-weight-normal')}>
74
+ {label}
75
+ </Label>
76
+ <div
77
+ className={clsx('d-flex')}
78
+ role="group"
79
+ aria-labelledby={labelId}
80
+ {...(inlinePrompt ? { 'aria-describedby': customAlertId } : {})}
81
+ >
82
+ <div className="wds-expressive-money-input-currency-selector">{selector}</div>
83
+ <AmountInput
84
+ id={inputId}
85
+ describedById={currencyId}
86
+ amount={amount}
87
+ currency={currency}
88
+ // eslint-disable-next-line jsx-a11y/no-autofocus
89
+ autoFocus={autoFocus}
90
+ loading={loading}
91
+ onChange={onAmountChange}
92
+ onFocusChange={onFocusChange}
93
+ />
94
+ <div className={clsx('d-flex align-items-center', 'wds-expressive-money-input-chevron')}>
95
+ <Chevron shouldShow={Boolean(showChevron)} />
96
+ </div>
97
+ </div>
98
+ <AnimatePresence initial={false}>
99
+ {inlinePrompt && (
100
+ <div className={clsx('d-flex justify-content-end', inlinePrompt && 'm-t-1')}>
101
+ <motion.div
102
+ key={customAlertId}
103
+ initial={{ opacity: 0, height: 0 }}
104
+ animate={{
105
+ opacity: 1,
106
+ height: 'auto',
107
+ transition: { delay: 0.75, duration: 0.3 },
108
+ }}
109
+ exit={{ opacity: 0, height: 0 }}
110
+ >
111
+ {inlinePrompt.sentiment ? (
112
+ <InlinePrompt id={customAlertId} sentiment={inlinePrompt.sentiment}>
113
+ {inlinePrompt.message}
114
+ </InlinePrompt>
115
+ ) : (
116
+ <Body>{inlinePrompt.message}</Body>
117
+ )}
118
+ </motion.div>
119
+ </div>
120
+ )}
121
+ </AnimatePresence>
122
+ </div>
123
+ );
124
+ }
@@ -0,0 +1,2 @@
1
+ export type { Props as ExpressiveMoneyInputProps } from './ExpressiveMoneyInput';
2
+ export { default } from './ExpressiveMoneyInput';
@@ -0,0 +1,35 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ const focusTimeout = 5 * 1000;
4
+
5
+ export const useFocus = () => {
6
+ const [focus, setFocus] = useState(false);
7
+ const [visualFocus, setVisualFocus] = useState(false);
8
+ const [counter, setCounter] = useState(0);
9
+
10
+ useEffect(() => {
11
+ if (focus) {
12
+ const timeout = setTimeout(() => {
13
+ setVisualFocus(false);
14
+ }, focusTimeout);
15
+ return () => clearTimeout(timeout);
16
+ }
17
+ }, [focus, counter]);
18
+
19
+ return {
20
+ focus,
21
+ setFocus: (value: boolean) => {
22
+ setFocus(value);
23
+ if (value) {
24
+ setVisualFocus(true);
25
+ }
26
+ // any call to setFocus should reset the timeout, even if the input was already in focus
27
+ // updating the counter will trigger the useEffect to reset the timeout
28
+ setCounter((prev) => prev + 1);
29
+ },
30
+ visualFocus,
31
+ setVisualFocus: (value: boolean) => {
32
+ setVisualFocus(value);
33
+ },
34
+ };
35
+ };