@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,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,290 @@
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, lorem5 } 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
+ import Button from '../button';
11
+ import Modal from '../modal';
12
+ import ListItem from '../listItem';
13
+ import List from '../list';
14
+ import { Flag } from '@wise/art';
15
+ import Alert from '../alert';
16
+ import Divider from '../divider';
17
+ import { Rewards, Tags } from '@transferwise/icons';
18
+
19
+ export default {
20
+ title: 'Forms/ExpressiveMoneyInput',
21
+ component: ExpressiveMoneyInput,
22
+ } as Meta;
23
+
24
+ const props: ExpressiveMoneyInputProps = {
25
+ label: 'You send',
26
+ currency: 'GBP',
27
+ amount: 1234.56,
28
+ onAmountChange: (amount) => {
29
+ console.log('Amount changed', amount);
30
+ },
31
+ currencySelector: {
32
+ addons: [
33
+ { asset: <Image src="../avatar-square-dude.webp" alt="" /> },
34
+ { profileName: 'Test Test' },
35
+ ],
36
+ options: [
37
+ {
38
+ title: 'Popular',
39
+ currencies: [
40
+ { code: 'USD', label: 'US Dollar', keywords: ['dollar', 'us'] },
41
+ { code: 'AUD', label: 'Australia Dollar', keywords: ['dollar', 'us'] },
42
+ ],
43
+ },
44
+ {
45
+ title: 'Others',
46
+ currencies: [
47
+ { code: 'GBP', label: 'Pound', keywords: ['british'] },
48
+ { code: 'EUR', label: 'Euro', keywords: ['euro'] },
49
+ ],
50
+ },
51
+ ],
52
+ onOpen: () => {
53
+ console.log('Currency selector opened');
54
+ },
55
+ onSearchChange: (payload) => {
56
+ console.log('Search changed', payload);
57
+ },
58
+ onChange: (currency) => {
59
+ console.log('Currency changed', currency);
60
+ },
61
+ },
62
+ loading: false,
63
+ inlinePrompt: { message: lorem10, sentiment: Sentiment.POSITIVE },
64
+ showChevron: true,
65
+ autoFocus: true,
66
+ };
67
+
68
+ export const NullAmount = {
69
+ args: props,
70
+ render: (args: ExpressiveMoneyInputProps) => (
71
+ <ExpressiveMoneyInput
72
+ label={args.label}
73
+ amount={null}
74
+ currency="EUR"
75
+ onAmountChange={args.onAmountChange}
76
+ />
77
+ ),
78
+ };
79
+
80
+ export const WithInitAmount = {
81
+ args: props,
82
+ render: (args: ExpressiveMoneyInputProps) => {
83
+ const availableBalance = 1500;
84
+ return (
85
+ <ExpressiveMoneyInput
86
+ label={args.label}
87
+ amount={availableBalance}
88
+ currency="EUR"
89
+ onAmountChange={args.onAmountChange}
90
+ />
91
+ );
92
+ },
93
+ };
94
+
95
+ export const WithLoading = {
96
+ args: props,
97
+ render: (args: ExpressiveMoneyInputProps) => {
98
+ const availableBalance = 1500;
99
+ return (
100
+ <ExpressiveMoneyInput
101
+ label={args.label}
102
+ amount={availableBalance}
103
+ currency="EUR"
104
+ loading
105
+ onAmountChange={args.onAmountChange}
106
+ />
107
+ );
108
+ },
109
+ };
110
+
111
+ export const WithoutChevron = {
112
+ args: props,
113
+ render: (args: ExpressiveMoneyInputProps) => {
114
+ const availableBalance = 1500;
115
+ return (
116
+ <ExpressiveMoneyInput
117
+ label={args.label}
118
+ amount={availableBalance}
119
+ currency="EUR"
120
+ showChevron={false}
121
+ onAmountChange={args.onAmountChange}
122
+ />
123
+ );
124
+ },
125
+ };
126
+
127
+ export const WithCurrencySelector = {
128
+ args: props,
129
+ render: (args: ExpressiveMoneyInputProps) => {
130
+ const availableBalance = 1500;
131
+ return (
132
+ <ExpressiveMoneyInput
133
+ label={args.label}
134
+ amount={availableBalance}
135
+ currency="EUR"
136
+ currencySelector={args.currencySelector}
137
+ onAmountChange={args.onAmountChange}
138
+ />
139
+ );
140
+ },
141
+ };
142
+
143
+ export const WithInlinePromptNoSentiment = {
144
+ args: props,
145
+ render: (args: ExpressiveMoneyInputProps) => {
146
+ const availableBalance = 1500;
147
+ const [sourceAmount, setSourceAmount] = React.useState(args.amount);
148
+ return (
149
+ <ExpressiveMoneyInput
150
+ label={args.label}
151
+ amount={sourceAmount}
152
+ currency="EUR"
153
+ currencySelector={args.currencySelector}
154
+ inlinePrompt={{
155
+ message: (
156
+ <>
157
+ {`Available balance `}
158
+ <Link
159
+ onClick={() => {
160
+ setSourceAmount(availableBalance);
161
+ }}
162
+ >
163
+ <Money amount={availableBalance} currency={args.currency} />
164
+ </Link>
165
+ </>
166
+ ),
167
+ }}
168
+ onAmountChange={args.onAmountChange}
169
+ />
170
+ );
171
+ },
172
+ };
173
+
174
+ export const WithInlinePromptSentiment = {
175
+ args: props,
176
+ render: (args: ExpressiveMoneyInputProps) => {
177
+ return (
178
+ <>
179
+ <ExpressiveMoneyInput
180
+ label={args.label}
181
+ currency="EUR"
182
+ currencySelector={args.currencySelector}
183
+ inlinePrompt={{ message: lorem5, sentiment: 'negative' }}
184
+ onAmountChange={args.onAmountChange}
185
+ />
186
+ <ExpressiveMoneyInput
187
+ label={args.label}
188
+ currency="EUR"
189
+ currencySelector={args.currencySelector}
190
+ inlinePrompt={{ message: lorem5, sentiment: 'warning' }}
191
+ onAmountChange={args.onAmountChange}
192
+ />
193
+ <ExpressiveMoneyInput
194
+ label={args.label}
195
+ currency="EUR"
196
+ currencySelector={args.currencySelector}
197
+ inlinePrompt={{ message: lorem5, sentiment: 'neutral' }}
198
+ onAmountChange={args.onAmountChange}
199
+ />
200
+ <ExpressiveMoneyInput
201
+ label={args.label}
202
+ currency="EUR"
203
+ currencySelector={args.currencySelector}
204
+ inlinePrompt={{ message: lorem5, sentiment: 'positive' }}
205
+ onAmountChange={args.onAmountChange}
206
+ />
207
+ <ExpressiveMoneyInput
208
+ label={args.label}
209
+ currency="EUR"
210
+ currencySelector={args.currencySelector}
211
+ inlinePrompt={{ message: lorem5, sentiment: 'positive', media: <Tags /> }}
212
+ onAmountChange={args.onAmountChange}
213
+ />
214
+ <ExpressiveMoneyInput
215
+ label={args.label}
216
+ currency="EUR"
217
+ currencySelector={args.currencySelector}
218
+ inlinePrompt={{ message: lorem5, sentiment: 'proposition' }}
219
+ onAmountChange={args.onAmountChange}
220
+ />
221
+ <ExpressiveMoneyInput
222
+ label={args.label}
223
+ currency="EUR"
224
+ currencySelector={args.currencySelector}
225
+ inlinePrompt={{ message: lorem5, sentiment: 'proposition', media: <Rewards /> }}
226
+ onAmountChange={args.onAmountChange}
227
+ />
228
+ </>
229
+ );
230
+ },
231
+ };
232
+
233
+ export const Autofocus = {
234
+ args: props,
235
+ render: (args: ExpressiveMoneyInputProps) => (
236
+ <ExpressiveMoneyInput {...args} currency="MXN" inlinePrompt={undefined} />
237
+ ),
238
+ };
239
+
240
+ export const WithCustomRender = {
241
+ args: props,
242
+ render: (args: ExpressiveMoneyInputProps) => {
243
+ const [open, setOpen] = React.useState(false);
244
+ return (
245
+ <>
246
+ <ExpressiveMoneyInput
247
+ label="Instance with Custom Currency Selector"
248
+ currency="AUD"
249
+ currencySelector={{
250
+ customRender: ({ id, labelId }) => (
251
+ <Button v2 size="sm" priority="primary" onClick={() => setOpen(true)}>
252
+ Custom Render
253
+ </Button>
254
+ ),
255
+ }}
256
+ onAmountChange={(amount) => console.log(amount)}
257
+ />
258
+ <Modal
259
+ open={open}
260
+ title="Custom UX for currency selectors"
261
+ body={
262
+ <List>
263
+ <ListItem
264
+ media={
265
+ <ListItem.AvatarView>
266
+ <Flag code="gb" />
267
+ </ListItem.AvatarView>
268
+ }
269
+ title="GBP"
270
+ control={<ListItem.Radio name="currency-key" checked={false} />}
271
+ />
272
+ <ListItem
273
+ media={
274
+ <ListItem.AvatarView>
275
+ <Flag code="au" />
276
+ </ListItem.AvatarView>
277
+ }
278
+ title="AUD"
279
+ control={<ListItem.Radio name="currency-key" checked />}
280
+ />
281
+ </List>
282
+ }
283
+ onClose={() => setOpen(false)}
284
+ />
285
+ <Divider className="m-y-2" />
286
+ <Alert type="info" message="TODO: add proper message here" />
287
+ </>
288
+ );
289
+ },
290
+ };
@@ -0,0 +1,118 @@
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 = {
21
+ customRender?: (props: { id: string; labelId: string }) => ReactNode;
22
+ };
23
+ type CurrencySelectorType = DefaultCurrencySelectorInstanceType &
24
+ CustomCurrencySelectorInstanceType;
25
+
26
+ export type Props = {
27
+ label?: ReactNode;
28
+ currencySelector?: CurrencySelectorType;
29
+ amount?: AmountType;
30
+ currency: CurrencyType;
31
+ inlinePrompt?: {
32
+ sentiment?: InlinePromptProps['sentiment'];
33
+ message: InlinePromptProps['children'];
34
+ media?: InlinePromptProps['media'];
35
+ };
36
+ showChevron?: boolean;
37
+ autoFocus?: boolean;
38
+ loading?: boolean;
39
+ onAmountChange: (amount: AmountType) => void;
40
+ onFocusChange?: (focused: boolean) => void;
41
+ } & CommonProps;
42
+
43
+ export default function ExpressiveMoneyInput({
44
+ label,
45
+ currency,
46
+ currencySelector = { options: [] } as DefaultCurrencySelectorInstanceType,
47
+ amount,
48
+ onAmountChange,
49
+ className,
50
+ inlinePrompt,
51
+ showChevron,
52
+ autoFocus,
53
+ loading,
54
+ onFocusChange,
55
+ }: Props) {
56
+ const inputId = useId();
57
+ const labelId = useId();
58
+ const customAlertId = useId();
59
+ const currencyId = useId();
60
+
61
+ const selector = currencySelector.customRender?.({ id: currencyId, labelId }) ?? (
62
+ <CurrencySelector id={currencyId} labelId={labelId} currency={currency} {...currencySelector} />
63
+ );
64
+
65
+ return (
66
+ <div className={clsx('wds-expressive-money-input', className)}>
67
+ <Label id={labelId} htmlFor={inputId} className={clsx('m-b-1', 'font-weight-normal')}>
68
+ {label}
69
+ </Label>
70
+ <div
71
+ className={clsx('d-flex')}
72
+ role="group"
73
+ aria-labelledby={labelId}
74
+ {...(inlinePrompt ? { 'aria-describedby': customAlertId } : {})}
75
+ >
76
+ <div className="wds-expressive-money-input-currency-selector">{selector}</div>
77
+ <AmountInput
78
+ id={inputId}
79
+ describedById={currencyId}
80
+ amount={amount}
81
+ currency={currency}
82
+ // eslint-disable-next-line jsx-a11y/no-autofocus
83
+ autoFocus={autoFocus}
84
+ loading={loading}
85
+ onChange={onAmountChange}
86
+ onFocusChange={onFocusChange}
87
+ />
88
+ <div className={clsx('d-flex align-items-center', 'wds-expressive-money-input-chevron')}>
89
+ <Chevron shouldShow={Boolean(showChevron)} />
90
+ </div>
91
+ </div>
92
+ <AnimatePresence initial={false}>
93
+ {inlinePrompt && (
94
+ <div className={clsx('d-flex justify-content-end', inlinePrompt && 'm-t-1')}>
95
+ <motion.div
96
+ key={customAlertId}
97
+ initial={{ opacity: 0, height: 0 }}
98
+ animate={{
99
+ opacity: 1,
100
+ height: 'auto',
101
+ transition: { delay: 0.75, duration: 0.3 },
102
+ }}
103
+ exit={{ opacity: 0, height: 0 }}
104
+ >
105
+ {inlinePrompt.sentiment ? (
106
+ <InlinePrompt id={customAlertId} media={inlinePrompt.media} sentiment={inlinePrompt.sentiment}>
107
+ {inlinePrompt.message}
108
+ </InlinePrompt>
109
+ ) : (
110
+ <Body>{inlinePrompt.message}</Body>
111
+ )}
112
+ </motion.div>
113
+ </div>
114
+ )}
115
+ </AnimatePresence>
116
+ </div>
117
+ );
118
+ }