@transferwise/components 46.4.0 → 46.6.0
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.
- package/build/i18n/th.json +3 -3
- package/build/index.esm.js +177 -239
- package/build/index.esm.js.map +1 -1
- package/build/index.js +178 -240
- package/build/index.js.map +1 -1
- package/build/types/common/textFormat/formatWithPattern/formatWithPattern.d.ts +1 -1
- package/build/types/common/textFormat/formatWithPattern/formatWithPattern.d.ts.map +1 -1
- package/build/types/common/textFormat/getCursorPositionAfterKeystroke/getCursorPositionAfterKeystroke.d.ts +2 -2
- package/build/types/common/textFormat/getCursorPositionAfterKeystroke/getCursorPositionAfterKeystroke.d.ts.map +1 -1
- package/build/types/common/textFormat/getSymbolsInPatternWithPosition/getSymbolsInPatternWithPosition.d.ts +5 -1
- package/build/types/common/textFormat/getSymbolsInPatternWithPosition/getSymbolsInPatternWithPosition.d.ts.map +1 -1
- package/build/types/common/textFormat/unformatWithPattern/unformatWithPattern.d.ts +1 -1
- package/build/types/common/textFormat/unformatWithPattern/unformatWithPattern.d.ts.map +1 -1
- package/build/types/index.d.ts +3 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/inputWithDisplayFormat/InputWithDisplayFormat.d.ts +7 -11
- package/build/types/inputWithDisplayFormat/InputWithDisplayFormat.d.ts.map +1 -1
- package/build/types/inputWithDisplayFormat/index.d.ts +2 -1
- package/build/types/inputWithDisplayFormat/index.d.ts.map +1 -1
- package/build/types/inputs/SelectInput.d.ts +2 -2
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/moneyInput/MoneyInput.d.ts +45 -31
- package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
- package/build/types/moneyInput/MoneyInput.messages.d.ts +6 -6
- package/build/types/moneyInput/MoneyInput.messages.d.ts.map +1 -1
- package/build/types/moneyInput/currencyFormatting.d.ts +2 -2
- package/build/types/moneyInput/currencyFormatting.d.ts.map +1 -1
- package/build/types/moneyInput/index.d.ts +2 -1
- package/build/types/moneyInput/index.d.ts.map +1 -1
- package/build/types/textareaWithDisplayFormat/TextareaWithDisplayFormat.d.ts +7 -11
- package/build/types/textareaWithDisplayFormat/TextareaWithDisplayFormat.d.ts.map +1 -1
- package/build/types/textareaWithDisplayFormat/index.d.ts +2 -1
- package/build/types/textareaWithDisplayFormat/index.d.ts.map +1 -1
- package/build/types/withDisplayFormat/WithDisplayFormat.d.ts +55 -83
- package/build/types/withDisplayFormat/WithDisplayFormat.d.ts.map +1 -1
- package/build/types/withDisplayFormat/index.d.ts +2 -1
- package/build/types/withDisplayFormat/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/textFormat/formatWithPattern/{formatWithPattern.js → formatWithPattern.ts} +8 -4
- package/src/common/textFormat/getCursorPositionAfterKeystroke/{getCursorPositionAfterKeystroke.js → getCursorPositionAfterKeystroke.ts} +8 -8
- package/src/common/textFormat/getSymbolsInPatternWithPosition/{getSymbolsInPatternWithPosition.js → getSymbolsInPatternWithPosition.ts} +7 -2
- package/src/common/textFormat/unformatWithPattern/{unformatWithPattern.js → unformatWithPattern.ts} +3 -2
- package/src/flowNavigation/FlowNavigation.story.js +1 -1
- package/src/i18n/th.json +3 -3
- package/src/index.ts +8 -0
- package/src/inputWithDisplayFormat/InputWithDisplayFormat.tsx +10 -0
- package/src/inputWithDisplayFormat/index.ts +2 -0
- package/src/inputs/SelectInput.tsx +2 -2
- package/src/moneyInput/{MoneyInput.rtl.spec.js → MoneyInput.rtl.spec.tsx} +4 -4
- package/src/moneyInput/MoneyInput.spec.js +109 -49
- package/src/moneyInput/MoneyInput.story.tsx +6 -14
- package/src/moneyInput/{MoneyInput.js → MoneyInput.tsx} +189 -173
- package/src/moneyInput/{currencyFormatting.spec.js → currencyFormatting.spec.ts} +2 -2
- package/src/moneyInput/{currencyFormatting.js → currencyFormatting.ts} +7 -10
- package/src/moneyInput/index.ts +7 -0
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.spec.js +3 -1
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.story.tsx +32 -0
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.tsx +13 -0
- package/src/textareaWithDisplayFormat/index.ts +2 -0
- package/src/withDisplayFormat/WithDisplayFormat.spec.js +1 -1
- package/src/withDisplayFormat/{WithDisplayFormat.js → WithDisplayFormat.tsx} +127 -107
- package/src/withDisplayFormat/index.ts +2 -0
- package/src/inputWithDisplayFormat/InputWithDisplayFormat.js +0 -14
- package/src/inputWithDisplayFormat/index.js +0 -1
- package/src/moneyInput/index.js +0 -1
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.js +0 -14
- package/src/textareaWithDisplayFormat/index.js +0 -1
- package/src/withDisplayFormat/index.js +0 -1
- /package/src/moneyInput/{MoneyInput.messages.js → MoneyInput.messages.ts} +0 -0
|
@@ -1,48 +1,79 @@
|
|
|
1
1
|
import { isEmpty, isNumber, isNull } from '@transferwise/neptune-validation';
|
|
2
2
|
import { Flag } from '@wise/art';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
|
-
import PropTypes from 'prop-types';
|
|
5
4
|
import { Component } from 'react';
|
|
6
|
-
import { injectIntl } from 'react-intl';
|
|
5
|
+
import { injectIntl, WrappedComponentProps } from 'react-intl';
|
|
7
6
|
|
|
8
7
|
import { Typography } from '../common';
|
|
9
8
|
import { Key as keyValues } from '../common/key';
|
|
10
9
|
import keyCodes from '../common/keyCodes';
|
|
11
|
-
import { Size } from '../common/propsValues/size';
|
|
10
|
+
import { Size, SizeLarge, SizeMedium, SizeSmall } from '../common/propsValues/size';
|
|
12
11
|
import { Input } from '../inputs/Input';
|
|
13
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
SelectInput,
|
|
14
|
+
SelectInputItem,
|
|
15
|
+
SelectInputOptionContent,
|
|
16
|
+
SelectInputOptionItem,
|
|
17
|
+
SelectInputProps,
|
|
18
|
+
} from '../inputs/SelectInput';
|
|
14
19
|
import Title from '../title';
|
|
15
20
|
|
|
16
21
|
import messages from './MoneyInput.messages';
|
|
17
22
|
import { formatAmount, parseAmount } from './currencyFormatting';
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
header
|
|
21
|
-
value:
|
|
22
|
-
label:
|
|
23
|
-
currency:
|
|
24
|
-
note
|
|
25
|
-
searchable
|
|
26
|
-
}
|
|
24
|
+
export interface CurrencyOptionItem {
|
|
25
|
+
header?: never;
|
|
26
|
+
value: string;
|
|
27
|
+
label: string;
|
|
28
|
+
currency: string;
|
|
29
|
+
note?: string;
|
|
30
|
+
searchable?: string;
|
|
31
|
+
}
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
export interface CurrencyHeaderItem {
|
|
34
|
+
header: string;
|
|
35
|
+
}
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
export type CurrencyItem = CurrencyOptionItem | CurrencyHeaderItem;
|
|
38
|
+
|
|
39
|
+
const isNumberOrNull = (v: unknown): v is number | null => isNumber(v) || isNull(v);
|
|
40
|
+
|
|
41
|
+
const formatAmountIfSet = ({
|
|
42
|
+
amount,
|
|
43
|
+
currency,
|
|
44
|
+
locale,
|
|
45
|
+
maxLengthOverride,
|
|
46
|
+
}: {
|
|
47
|
+
amount: number | null | undefined;
|
|
48
|
+
currency: string;
|
|
49
|
+
locale: string;
|
|
50
|
+
maxLengthOverride?: number;
|
|
51
|
+
}) => {
|
|
31
52
|
if (maxLengthOverride) {
|
|
32
|
-
return amount
|
|
53
|
+
return amount != null ? String(amount) : '';
|
|
33
54
|
} else {
|
|
34
55
|
return typeof amount === 'number' ? formatAmount(amount, currency, locale) : '';
|
|
35
56
|
}
|
|
36
57
|
};
|
|
37
58
|
|
|
38
|
-
const parseNumber = (
|
|
59
|
+
const parseNumber = ({
|
|
60
|
+
amount,
|
|
61
|
+
currency,
|
|
62
|
+
locale,
|
|
63
|
+
maxLengthOverride,
|
|
64
|
+
}: {
|
|
65
|
+
amount: string;
|
|
66
|
+
currency: string;
|
|
67
|
+
locale: string;
|
|
68
|
+
maxLengthOverride?: number;
|
|
69
|
+
}) => {
|
|
39
70
|
if (!maxLengthOverride) {
|
|
40
71
|
return parseAmount(amount, currency, locale);
|
|
41
72
|
}
|
|
42
73
|
if (maxLengthOverride && amount.length > maxLengthOverride) {
|
|
43
74
|
return 0;
|
|
44
75
|
}
|
|
45
|
-
return
|
|
76
|
+
return Number(amount);
|
|
46
77
|
};
|
|
47
78
|
|
|
48
79
|
const inputKeyCodeAllowlist = new Set([
|
|
@@ -61,39 +92,76 @@ const inputKeyCodeAllowlist = new Set([
|
|
|
61
92
|
|
|
62
93
|
const inputKeyAllowlist = new Set([keyValues.PERIOD, keyValues.COMMA]);
|
|
63
94
|
|
|
64
|
-
|
|
65
|
-
|
|
95
|
+
export interface MoneyInputProps extends WrappedComponentProps {
|
|
96
|
+
id?: string;
|
|
97
|
+
currencies: readonly CurrencyItem[];
|
|
98
|
+
selectedCurrency: CurrencyOptionItem;
|
|
99
|
+
onCurrencyChange?: (value: CurrencyOptionItem) => void;
|
|
100
|
+
placeholder?: number;
|
|
101
|
+
amount: number | null;
|
|
102
|
+
size?: SizeSmall | SizeMedium | SizeLarge;
|
|
103
|
+
onAmountChange?: (value: number | null) => void;
|
|
104
|
+
addon?: React.ReactNode;
|
|
105
|
+
searchPlaceholder?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Allows the consumer to react to searching, while the search itself is handled internally.
|
|
108
|
+
*/
|
|
109
|
+
onSearchChange?: (value: { searchQuery: string; filteredOptions: CurrencyItem[] }) => void;
|
|
110
|
+
customActionLabel?: React.ReactNode;
|
|
111
|
+
onCustomAction?: () => void;
|
|
112
|
+
classNames?: Record<string, string>;
|
|
113
|
+
selectProps?: Partial<SelectInputProps<CurrencyOptionItem>>;
|
|
114
|
+
maxLengthOverride?: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface MoneyInputState {
|
|
118
|
+
searchQuery: string;
|
|
119
|
+
formattedAmount: string;
|
|
120
|
+
locale: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
class MoneyInput extends Component<MoneyInputProps, MoneyInputState> {
|
|
124
|
+
declare props: MoneyInputProps &
|
|
125
|
+
Required<Pick<MoneyInputProps, keyof typeof MoneyInput.defaultProps>>;
|
|
126
|
+
|
|
127
|
+
static defaultProps = {
|
|
128
|
+
size: Size.LARGE,
|
|
129
|
+
classNames: {},
|
|
130
|
+
selectProps: {},
|
|
131
|
+
} satisfies Partial<MoneyInputProps>;
|
|
132
|
+
|
|
133
|
+
amountFocused = false;
|
|
134
|
+
|
|
135
|
+
constructor(props: MoneyInputProps) {
|
|
66
136
|
super(props);
|
|
67
|
-
const { locale } = this.props.intl;
|
|
68
|
-
this.formatMessage = this.props.intl.formatMessage;
|
|
69
137
|
this.state = {
|
|
70
138
|
searchQuery: '',
|
|
71
|
-
formattedAmount: formatAmountIfSet(
|
|
72
|
-
props.amount,
|
|
73
|
-
props.selectedCurrency.currency,
|
|
74
|
-
locale,
|
|
75
|
-
props.maxLengthOverride,
|
|
76
|
-
),
|
|
77
|
-
locale,
|
|
139
|
+
formattedAmount: formatAmountIfSet({
|
|
140
|
+
amount: props.amount,
|
|
141
|
+
currency: props.selectedCurrency.currency,
|
|
142
|
+
locale: props.intl.locale,
|
|
143
|
+
maxLengthOverride: props.maxLengthOverride,
|
|
144
|
+
}),
|
|
145
|
+
locale: props.intl.locale,
|
|
78
146
|
};
|
|
79
147
|
}
|
|
80
148
|
|
|
81
|
-
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
82
|
-
this.setState({ locale: nextProps
|
|
149
|
+
UNSAFE_componentWillReceiveProps(nextProps: MoneyInputProps) {
|
|
150
|
+
this.setState({ locale: nextProps.intl.locale });
|
|
83
151
|
|
|
84
152
|
if (!this.amountFocused) {
|
|
85
153
|
this.setState({
|
|
86
|
-
formattedAmount: formatAmountIfSet(
|
|
87
|
-
nextProps.amount,
|
|
88
|
-
nextProps.selectedCurrency.currency,
|
|
89
|
-
nextProps
|
|
90
|
-
nextProps.maxLengthOverride,
|
|
91
|
-
),
|
|
154
|
+
formattedAmount: formatAmountIfSet({
|
|
155
|
+
amount: nextProps.amount,
|
|
156
|
+
currency: nextProps.selectedCurrency.currency,
|
|
157
|
+
locale: nextProps.intl.locale,
|
|
158
|
+
maxLengthOverride: nextProps.maxLengthOverride,
|
|
159
|
+
}),
|
|
92
160
|
});
|
|
93
161
|
}
|
|
94
162
|
}
|
|
95
163
|
|
|
96
|
-
isInputAllowedForKeyEvent = (event) => {
|
|
164
|
+
isInputAllowedForKeyEvent = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
97
165
|
const { keyCode, metaKey, key, ctrlKey } = event;
|
|
98
166
|
const isNumberKey = isNumber(parseInt(key, 10));
|
|
99
167
|
|
|
@@ -106,54 +174,54 @@ class MoneyInput extends Component {
|
|
|
106
174
|
);
|
|
107
175
|
};
|
|
108
176
|
|
|
109
|
-
handleKeyDown = (event) => {
|
|
177
|
+
handleKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
|
|
110
178
|
if (!this.isInputAllowedForKeyEvent(event)) {
|
|
111
179
|
event.preventDefault();
|
|
112
180
|
}
|
|
113
181
|
};
|
|
114
182
|
|
|
115
|
-
handlePaste = (event) => {
|
|
116
|
-
const paste =
|
|
183
|
+
handlePaste: React.ClipboardEventHandler<HTMLInputElement> = (event) => {
|
|
184
|
+
const paste = event.clipboardData.getData('text');
|
|
117
185
|
const { locale } = this.state;
|
|
118
186
|
const parsed = isEmpty(paste)
|
|
119
187
|
? null
|
|
120
|
-
: parseNumber(
|
|
121
|
-
paste,
|
|
122
|
-
this.props.selectedCurrency.currency,
|
|
123
|
-
locale,
|
|
124
|
-
this.props.maxLengthOverride,
|
|
125
|
-
);
|
|
188
|
+
: parseNumber({
|
|
189
|
+
amount: paste,
|
|
190
|
+
currency: this.props.selectedCurrency.currency,
|
|
191
|
+
locale: locale,
|
|
192
|
+
maxLengthOverride: this.props.maxLengthOverride,
|
|
193
|
+
});
|
|
126
194
|
|
|
127
195
|
if (isNumberOrNull(parsed)) {
|
|
128
196
|
this.setState({
|
|
129
|
-
formattedAmount: formatAmountIfSet(
|
|
130
|
-
parsed,
|
|
131
|
-
this.props.selectedCurrency.currency,
|
|
132
|
-
locale,
|
|
133
|
-
this.props.maxLengthOverride,
|
|
134
|
-
),
|
|
197
|
+
formattedAmount: formatAmountIfSet({
|
|
198
|
+
amount: parsed,
|
|
199
|
+
currency: this.props.selectedCurrency.currency,
|
|
200
|
+
locale: locale,
|
|
201
|
+
maxLengthOverride: this.props.maxLengthOverride,
|
|
202
|
+
}),
|
|
135
203
|
});
|
|
136
|
-
this.props.onAmountChange(parsed);
|
|
204
|
+
this.props.onAmountChange?.(parsed);
|
|
137
205
|
}
|
|
138
206
|
|
|
139
207
|
event.preventDefault();
|
|
140
208
|
};
|
|
141
209
|
|
|
142
|
-
onAmountChange = (event) => {
|
|
210
|
+
onAmountChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
|
143
211
|
const { value } = event.target;
|
|
144
212
|
this.setState({
|
|
145
213
|
formattedAmount: value,
|
|
146
214
|
});
|
|
147
215
|
const parsed = isEmpty(value)
|
|
148
216
|
? null
|
|
149
|
-
: parseNumber(
|
|
150
|
-
value,
|
|
151
|
-
this.props.selectedCurrency.currency,
|
|
152
|
-
this.state.locale,
|
|
153
|
-
this.props.maxLengthOverride,
|
|
154
|
-
);
|
|
217
|
+
: parseNumber({
|
|
218
|
+
amount: value,
|
|
219
|
+
currency: this.props.selectedCurrency.currency,
|
|
220
|
+
locale: this.state.locale,
|
|
221
|
+
maxLengthOverride: this.props.maxLengthOverride,
|
|
222
|
+
});
|
|
155
223
|
if (isNumberOrNull(parsed)) {
|
|
156
|
-
this.props.onAmountChange(parsed);
|
|
224
|
+
this.props.onAmountChange?.(parsed);
|
|
157
225
|
}
|
|
158
226
|
};
|
|
159
227
|
|
|
@@ -166,34 +234,26 @@ class MoneyInput extends Component {
|
|
|
166
234
|
this.amountFocused = true;
|
|
167
235
|
};
|
|
168
236
|
|
|
169
|
-
mapOption = (item) => {
|
|
170
|
-
return {
|
|
171
|
-
type: 'option',
|
|
172
|
-
value: item,
|
|
173
|
-
filterMatchers: [item.value, item.label, item.note, item.searchable],
|
|
174
|
-
};
|
|
175
|
-
};
|
|
176
|
-
|
|
177
237
|
getSelectOptions() {
|
|
178
|
-
const selectOptions =
|
|
238
|
+
const selectOptions = filterCurrenciesForQuery(this.props.currencies, this.state.searchQuery);
|
|
179
239
|
|
|
180
|
-
|
|
181
|
-
let
|
|
240
|
+
const formattedOptions: SelectInputItem<CurrencyOptionItem>[] = [];
|
|
241
|
+
let currentGroupOptions: SelectInputOptionItem<CurrencyOptionItem>[] | undefined;
|
|
182
242
|
|
|
183
243
|
selectOptions.forEach((item) => {
|
|
184
|
-
if (item.header) {
|
|
244
|
+
if (item.header != null) {
|
|
245
|
+
currentGroupOptions = [];
|
|
185
246
|
formattedOptions.push({
|
|
186
247
|
type: 'group',
|
|
187
248
|
label: item.header,
|
|
188
|
-
options:
|
|
249
|
+
options: currentGroupOptions,
|
|
189
250
|
});
|
|
190
|
-
groupIndex = formattedOptions.length - 1;
|
|
191
251
|
} else {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
252
|
+
(currentGroupOptions ?? formattedOptions).push({
|
|
253
|
+
type: 'option',
|
|
254
|
+
value: item,
|
|
255
|
+
filterMatchers: [item.value, item.label, item.note ?? '', item.searchable ?? ''],
|
|
256
|
+
});
|
|
197
257
|
}
|
|
198
258
|
});
|
|
199
259
|
|
|
@@ -202,51 +262,47 @@ class MoneyInput extends Component {
|
|
|
202
262
|
|
|
203
263
|
setAmount() {
|
|
204
264
|
this.setState((previousState) => {
|
|
205
|
-
const parsed = parseNumber(
|
|
206
|
-
previousState.formattedAmount,
|
|
207
|
-
this.props.selectedCurrency.currency,
|
|
208
|
-
previousState.locale,
|
|
209
|
-
this.props.maxLengthOverride,
|
|
210
|
-
);
|
|
265
|
+
const parsed = parseNumber({
|
|
266
|
+
amount: previousState.formattedAmount,
|
|
267
|
+
currency: this.props.selectedCurrency.currency,
|
|
268
|
+
locale: previousState.locale,
|
|
269
|
+
maxLengthOverride: this.props.maxLengthOverride,
|
|
270
|
+
});
|
|
211
271
|
if (!isNumberOrNull(parsed)) {
|
|
212
272
|
return {
|
|
213
273
|
formattedAmount: previousState.formattedAmount,
|
|
214
274
|
};
|
|
215
275
|
}
|
|
216
276
|
return {
|
|
217
|
-
formattedAmount: formatAmountIfSet(
|
|
218
|
-
parsed,
|
|
219
|
-
this.props.selectedCurrency.currency,
|
|
220
|
-
previousState.locale,
|
|
221
|
-
this.props.maxLengthOverride,
|
|
222
|
-
),
|
|
277
|
+
formattedAmount: formatAmountIfSet({
|
|
278
|
+
amount: parsed,
|
|
279
|
+
currency: this.props.selectedCurrency.currency,
|
|
280
|
+
locale: previousState.locale,
|
|
281
|
+
maxLengthOverride: this.props.maxLengthOverride,
|
|
282
|
+
}),
|
|
223
283
|
};
|
|
224
284
|
});
|
|
225
285
|
}
|
|
226
286
|
|
|
227
|
-
handleSelectChange = (value) => {
|
|
287
|
+
handleSelectChange = (value: CurrencyOptionItem) => {
|
|
228
288
|
this.handleSearchChange('');
|
|
229
|
-
this.props.onCurrencyChange(value);
|
|
289
|
+
this.props.onCurrencyChange?.(value);
|
|
230
290
|
};
|
|
231
291
|
|
|
232
292
|
handleCustomAction = () => {
|
|
233
293
|
this.handleSearchChange('');
|
|
234
|
-
|
|
235
|
-
this.props.onCustomAction();
|
|
236
|
-
}
|
|
294
|
+
this.props.onCustomAction?.();
|
|
237
295
|
};
|
|
238
296
|
|
|
239
|
-
handleSearchChange = (searchQuery) => {
|
|
297
|
+
handleSearchChange = (searchQuery: string) => {
|
|
240
298
|
this.setState({ searchQuery });
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
});
|
|
246
|
-
}
|
|
299
|
+
this.props.onSearchChange?.({
|
|
300
|
+
searchQuery,
|
|
301
|
+
filteredOptions: filterCurrenciesForQuery(this.props.currencies, searchQuery),
|
|
302
|
+
});
|
|
247
303
|
};
|
|
248
304
|
|
|
249
|
-
style = (className) => this.props.classNames[className] || className;
|
|
305
|
+
style = (className: string) => this.props.classNames[className] || className;
|
|
250
306
|
|
|
251
307
|
render() {
|
|
252
308
|
const { selectedCurrency, onCurrencyChange, size, addon, id, selectProps, maxLengthOverride } =
|
|
@@ -292,12 +348,12 @@ class MoneyInput extends Component {
|
|
|
292
348
|
inputMode="decimal"
|
|
293
349
|
disabled={disabled}
|
|
294
350
|
maxLength={maxLengthOverride}
|
|
295
|
-
placeholder={formatAmountIfSet(
|
|
296
|
-
this.props.placeholder,
|
|
297
|
-
this.props.selectedCurrency.currency,
|
|
298
|
-
this.state.locale,
|
|
299
|
-
this.props.maxLengthOverride,
|
|
300
|
-
)}
|
|
351
|
+
placeholder={formatAmountIfSet({
|
|
352
|
+
amount: this.props.placeholder,
|
|
353
|
+
currency: this.props.selectedCurrency.currency,
|
|
354
|
+
locale: this.state.locale,
|
|
355
|
+
maxLengthOverride: this.props.maxLengthOverride,
|
|
356
|
+
})}
|
|
301
357
|
autoComplete="off"
|
|
302
358
|
onKeyDown={this.handleKeyDown}
|
|
303
359
|
onChange={this.onAmountChange}
|
|
@@ -364,13 +420,13 @@ class MoneyInput extends Component {
|
|
|
364
420
|
this.props.onCustomAction
|
|
365
421
|
? () => (
|
|
366
422
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
|
367
|
-
<div role="button" tabIndex=
|
|
423
|
+
<div role="button" tabIndex={0} onClick={this.handleCustomAction}>
|
|
368
424
|
{this.props.customActionLabel}
|
|
369
425
|
</div>
|
|
370
426
|
)
|
|
371
|
-
:
|
|
427
|
+
: undefined
|
|
372
428
|
}
|
|
373
|
-
placeholder={this.formatMessage(messages.selectPlaceholder)}
|
|
429
|
+
placeholder={this.props.intl.formatMessage(messages.selectPlaceholder)}
|
|
374
430
|
filterable
|
|
375
431
|
filterPlaceholder={this.props.searchPlaceholder}
|
|
376
432
|
disabled={disabled}
|
|
@@ -388,33 +444,36 @@ class MoneyInput extends Component {
|
|
|
388
444
|
}
|
|
389
445
|
}
|
|
390
446
|
|
|
391
|
-
function
|
|
447
|
+
function filterCurrenciesForQuery(
|
|
448
|
+
currencies: readonly CurrencyItem[],
|
|
449
|
+
query: string,
|
|
450
|
+
): CurrencyItem[] {
|
|
392
451
|
if (!query) {
|
|
393
|
-
return
|
|
452
|
+
return [...currencies];
|
|
394
453
|
}
|
|
395
454
|
|
|
455
|
+
const options = currencies.filter(
|
|
456
|
+
(option): option is CurrencyOptionItem => option.header == null,
|
|
457
|
+
);
|
|
396
458
|
const filteredOptions = removeDuplicateValueOptions(options).filter((option) =>
|
|
397
|
-
|
|
459
|
+
currencyOptionFitsQuery(option, query),
|
|
398
460
|
);
|
|
399
461
|
|
|
400
462
|
return sortOptionsLabelsToFirst(filteredOptions, query);
|
|
401
463
|
}
|
|
402
464
|
|
|
403
|
-
function removeDuplicateValueOptions(options) {
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
result.push(option);
|
|
410
|
-
resultValues.push(option.value);
|
|
465
|
+
function removeDuplicateValueOptions(options: CurrencyOptionItem[]) {
|
|
466
|
+
const uniqueValues = new Set<string>();
|
|
467
|
+
return options.filter((option) => {
|
|
468
|
+
if (!uniqueValues.has(option.value)) {
|
|
469
|
+
uniqueValues.add(option.value);
|
|
470
|
+
return true;
|
|
411
471
|
}
|
|
472
|
+
return false;
|
|
412
473
|
});
|
|
413
|
-
|
|
414
|
-
return result;
|
|
415
474
|
}
|
|
416
475
|
|
|
417
|
-
function
|
|
476
|
+
function currencyOptionFitsQuery(option: CurrencyOptionItem, query: string) {
|
|
418
477
|
if (!option.value) {
|
|
419
478
|
return false;
|
|
420
479
|
}
|
|
@@ -426,11 +485,11 @@ function isCurrencyOptionAndFitsQuery(option, query) {
|
|
|
426
485
|
);
|
|
427
486
|
}
|
|
428
487
|
|
|
429
|
-
function contains(property, query) {
|
|
488
|
+
function contains(property: string | undefined, query: string) {
|
|
430
489
|
return property && property.toLowerCase().includes(query.toLowerCase());
|
|
431
490
|
}
|
|
432
491
|
|
|
433
|
-
function sortOptionsLabelsToFirst(options, query) {
|
|
492
|
+
function sortOptionsLabelsToFirst(options: CurrencyOptionItem[], query: string) {
|
|
434
493
|
return options.sort((first, second) => {
|
|
435
494
|
const firstContains = contains(first.label, query);
|
|
436
495
|
const secondContains = contains(second.label, query);
|
|
@@ -448,47 +507,4 @@ function sortOptionsLabelsToFirst(options, query) {
|
|
|
448
507
|
});
|
|
449
508
|
}
|
|
450
509
|
|
|
451
|
-
MoneyInput.propTypes = {
|
|
452
|
-
id: PropTypes.string,
|
|
453
|
-
currencies: PropTypes.arrayOf(Currency).isRequired,
|
|
454
|
-
selectedCurrency: Currency.isRequired,
|
|
455
|
-
onCurrencyChange: PropTypes.func,
|
|
456
|
-
placeholder: PropTypes.number,
|
|
457
|
-
amount: PropTypes.number,
|
|
458
|
-
size: PropTypes.oneOf(['sm', 'md', 'lg']),
|
|
459
|
-
onAmountChange: PropTypes.func,
|
|
460
|
-
addon: PropTypes.node,
|
|
461
|
-
searchPlaceholder: PropTypes.string,
|
|
462
|
-
/**
|
|
463
|
-
* Allows the consumer to react to searching, while the search itself is handled internally. Called with `{ searchQuery: string, filteredOptions: Currency[] }`
|
|
464
|
-
*/
|
|
465
|
-
onSearchChange: PropTypes.func,
|
|
466
|
-
customActionLabel: PropTypes.node,
|
|
467
|
-
onCustomAction: PropTypes.func,
|
|
468
|
-
classNames: PropTypes.objectOf(PropTypes.string),
|
|
469
|
-
selectProps: PropTypes.object,
|
|
470
|
-
maxLengthOverride: PropTypes.number,
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
MoneyInput.defaultProps = {
|
|
474
|
-
id: null,
|
|
475
|
-
size: Size.LARGE,
|
|
476
|
-
addon: null,
|
|
477
|
-
searchPlaceholder: '',
|
|
478
|
-
onSearchChange: undefined,
|
|
479
|
-
onCurrencyChange: null,
|
|
480
|
-
placeholder: null,
|
|
481
|
-
amount: null,
|
|
482
|
-
onAmountChange: null,
|
|
483
|
-
customActionLabel: '',
|
|
484
|
-
onCustomAction: null,
|
|
485
|
-
classNames: {},
|
|
486
|
-
selectProps: {},
|
|
487
|
-
maxLengthOverride: null,
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
// this export is necessary for react-to-typescript-definitions
|
|
491
|
-
// to be able to properly generate TS types, this is due to us wrapping this component in `injectIntl` before exporting
|
|
492
|
-
export { MoneyInput };
|
|
493
|
-
|
|
494
510
|
export default injectIntl(MoneyInput);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { formatAmount, parseAmount } from './currencyFormatting';
|
|
2
2
|
|
|
3
3
|
jest.mock('@transferwise/formatting', () => ({
|
|
4
|
-
formatAmount: (number) => `formatted ${number}`,
|
|
4
|
+
formatAmount: (number: number) => `formatted ${number}`,
|
|
5
5
|
}));
|
|
6
6
|
|
|
7
7
|
describe('Number formatting', () => {
|
|
8
8
|
it('uses @transferwise/formatting for formatting numbers', () => {
|
|
9
|
-
expect(formatAmount(100)).toBe('formatted 100');
|
|
9
|
+
expect(formatAmount(100, 'gbp')).toBe('formatted 100');
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
it('parses localized numbers', () => {
|
|
@@ -5,7 +5,7 @@ import { DEFAULT_LOCALE } from '../common/locale';
|
|
|
5
5
|
export { formatAmount };
|
|
6
6
|
|
|
7
7
|
// TODO: do not duplicate this between formatting and components
|
|
8
|
-
const currencyDecimals = {
|
|
8
|
+
const currencyDecimals: Record<string, number> = {
|
|
9
9
|
BIF: 0,
|
|
10
10
|
BYR: 0,
|
|
11
11
|
CLP: 0,
|
|
@@ -41,30 +41,27 @@ function isNumberLocaleSupported() {
|
|
|
41
41
|
return numberString === '1,234';
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function getValidLocale(locale) {
|
|
44
|
+
function getValidLocale(locale: string) {
|
|
45
45
|
try {
|
|
46
46
|
const noUnderscoreLocale = locale.replace(/_/, '-');
|
|
47
47
|
|
|
48
48
|
Intl.NumberFormat(noUnderscoreLocale);
|
|
49
49
|
return noUnderscoreLocale;
|
|
50
50
|
} catch {
|
|
51
|
-
return
|
|
51
|
+
return DEFAULT_LOCALE;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function getCurrencyDecimals(currency
|
|
55
|
+
function getCurrencyDecimals(currency: string) {
|
|
56
56
|
const upperCaseCurrency = currency.toUpperCase();
|
|
57
|
-
|
|
58
|
-
return currencyDecimals[upperCaseCurrency];
|
|
59
|
-
}
|
|
60
|
-
return DEFAULT_CURRENCY_DECIMALS;
|
|
57
|
+
return currencyDecimals[upperCaseCurrency] ?? DEFAULT_CURRENCY_DECIMALS;
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
function getDecimalSeparator(locale) {
|
|
60
|
+
function getDecimalSeparator(locale: string) {
|
|
64
61
|
return isNumberLocaleSupported() ? (1.1).toLocaleString(locale)[1] : '.';
|
|
65
62
|
}
|
|
66
63
|
|
|
67
|
-
export function parseAmount(number, currency, locale) {
|
|
64
|
+
export function parseAmount(number: string, currency: string, locale = DEFAULT_LOCALE) {
|
|
68
65
|
const validLocale = getValidLocale(locale);
|
|
69
66
|
|
|
70
67
|
const precision = getCurrencyDecimals(currency);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { shallow } from 'enzyme';
|
|
2
2
|
|
|
3
|
+
import { TextArea } from '../inputs/TextArea';
|
|
4
|
+
|
|
3
5
|
import TextareaWithDisplayFormat from '.';
|
|
4
6
|
|
|
5
7
|
describe('TextareaWithDisplayFormat', () => {
|
|
@@ -10,6 +12,6 @@ describe('TextareaWithDisplayFormat', () => {
|
|
|
10
12
|
.find('WithDisplayFormat')
|
|
11
13
|
.renderProp('render')({ value: 'test' });
|
|
12
14
|
|
|
13
|
-
expect(view.find(
|
|
15
|
+
expect(view.find(TextArea).props('value')).toStrictEqual({ value: 'test' });
|
|
14
16
|
});
|
|
15
17
|
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { StoryObj } from '@storybook/react';
|
|
2
|
+
|
|
3
|
+
import { userEvent, within } from '../test-utils';
|
|
4
|
+
|
|
5
|
+
import TextareaWithDisplayFormat from './TextareaWithDisplayFormat';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
component: TextareaWithDisplayFormat,
|
|
9
|
+
title: 'Forms/TextareaWithDisplayFormat',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type Story = StoryObj<typeof TextareaWithDisplayFormat>;
|
|
13
|
+
|
|
14
|
+
export const Basic: Story = {
|
|
15
|
+
render: (args) => {
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<TextareaWithDisplayFormat
|
|
19
|
+
value="0000"
|
|
20
|
+
displayPattern="**** - **** - ****"
|
|
21
|
+
onChange={console.log}
|
|
22
|
+
/>
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
// intentionally use interactive typing (over init value via `value` prop)
|
|
27
|
+
// to trigger event handlers in the component
|
|
28
|
+
play: ({ canvasElement }) => {
|
|
29
|
+
const canvas = within(canvasElement);
|
|
30
|
+
userEvent.type(canvas.getByRole('textbox'), '111122223333');
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TextArea, type TextAreaProps } from '../inputs/TextArea';
|
|
2
|
+
import WithDisplayFormat, { type WithDisplayFormatProps } from '../withDisplayFormat';
|
|
3
|
+
|
|
4
|
+
export interface TextareaWithDisplayFormatProps extends Omit<WithDisplayFormatProps, 'render'> {}
|
|
5
|
+
|
|
6
|
+
const TextareaWithDisplayFormat = (props: TextareaWithDisplayFormatProps) => (
|
|
7
|
+
<WithDisplayFormat<TextAreaProps>
|
|
8
|
+
{...props}
|
|
9
|
+
render={(renderProps) => <TextArea {...renderProps} />}
|
|
10
|
+
/>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export default TextareaWithDisplayFormat;
|