@transferwise/components 46.112.0 → 46.113.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/expressiveMoneyInput/ExpressiveMoneyInput.js +113 -0
- package/build/expressiveMoneyInput/ExpressiveMoneyInput.js.map +1 -0
- package/build/expressiveMoneyInput/ExpressiveMoneyInput.messages.js +17 -0
- package/build/expressiveMoneyInput/ExpressiveMoneyInput.messages.js.map +1 -0
- package/build/expressiveMoneyInput/ExpressiveMoneyInput.messages.mjs +13 -0
- package/build/expressiveMoneyInput/ExpressiveMoneyInput.messages.mjs.map +1 -0
- package/build/expressiveMoneyInput/ExpressiveMoneyInput.mjs +109 -0
- package/build/expressiveMoneyInput/ExpressiveMoneyInput.mjs.map +1 -0
- package/build/expressiveMoneyInput/amountInput/AmountInput.js +281 -0
- package/build/expressiveMoneyInput/amountInput/AmountInput.js.map +1 -0
- package/build/expressiveMoneyInput/amountInput/AmountInput.mjs +279 -0
- package/build/expressiveMoneyInput/amountInput/AmountInput.mjs.map +1 -0
- package/build/expressiveMoneyInput/amountInput/utils.js +87 -0
- package/build/expressiveMoneyInput/amountInput/utils.js.map +1 -0
- package/build/expressiveMoneyInput/amountInput/utils.mjs +78 -0
- package/build/expressiveMoneyInput/amountInput/utils.mjs.map +1 -0
- package/build/expressiveMoneyInput/animatedNumber/AnimatedNumber.js +50 -0
- package/build/expressiveMoneyInput/animatedNumber/AnimatedNumber.js.map +1 -0
- package/build/expressiveMoneyInput/animatedNumber/AnimatedNumber.mjs +48 -0
- package/build/expressiveMoneyInput/animatedNumber/AnimatedNumber.mjs.map +1 -0
- package/build/expressiveMoneyInput/chevron/Chevron.js +31 -0
- package/build/expressiveMoneyInput/chevron/Chevron.js.map +1 -0
- package/build/expressiveMoneyInput/chevron/Chevron.mjs +29 -0
- package/build/expressiveMoneyInput/chevron/Chevron.mjs.map +1 -0
- package/build/expressiveMoneyInput/currencySelector/CurrencySelector.js +160 -0
- package/build/expressiveMoneyInput/currencySelector/CurrencySelector.js.map +1 -0
- package/build/expressiveMoneyInput/currencySelector/CurrencySelector.mjs +157 -0
- package/build/expressiveMoneyInput/currencySelector/CurrencySelector.mjs.map +1 -0
- package/build/expressiveMoneyInput/hooks/useFocus.js +37 -0
- package/build/expressiveMoneyInput/hooks/useFocus.js.map +1 -0
- package/build/expressiveMoneyInput/hooks/useFocus.mjs +35 -0
- package/build/expressiveMoneyInput/hooks/useFocus.mjs.map +1 -0
- package/build/expressiveMoneyInput/hooks/useInputStyle.js +71 -0
- package/build/expressiveMoneyInput/hooks/useInputStyle.js.map +1 -0
- package/build/expressiveMoneyInput/hooks/useInputStyle.mjs +69 -0
- package/build/expressiveMoneyInput/hooks/useInputStyle.mjs.map +1 -0
- package/build/i18n/en.json +2 -0
- package/build/i18n/en.json.js +2 -0
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +2 -0
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/index.mjs +1 -0
- package/build/index.mjs.map +1 -1
- package/build/listItem/useListItemControl.js +1 -1
- package/build/listItem/useListItemControl.js.map +1 -1
- package/build/listItem/useListItemControl.mjs +2 -2
- package/build/listItem/useListItemControl.mjs.map +1 -1
- package/build/listItem/useListItemMedia.js +1 -1
- package/build/listItem/useListItemMedia.js.map +1 -1
- package/build/listItem/useListItemMedia.mjs +2 -2
- package/build/listItem/useListItemMedia.mjs.map +1 -1
- package/build/main.css +73 -7
- package/build/prompt/InlinePrompt/InlinePrompt.js +7 -0
- package/build/prompt/InlinePrompt/InlinePrompt.js.map +1 -1
- package/build/prompt/InlinePrompt/InlinePrompt.mjs +8 -1
- package/build/prompt/InlinePrompt/InlinePrompt.mjs.map +1 -1
- package/build/styles/expressiveMoneyInput/ExpressiveMoneyInput.css +58 -0
- package/build/styles/expressiveMoneyInput/amountInput/AmountInput.css +32 -0
- package/build/styles/expressiveMoneyInput/chevron/Chevron.css +12 -0
- package/build/styles/expressiveMoneyInput/currencySelector/CurrencySelector.css +6 -0
- package/build/styles/main.css +73 -7
- package/build/styles/moneyInput/MoneyInput.css +8 -0
- package/build/styles/prompt/InlinePrompt/InlinePrompt.css +7 -7
- package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.d.ts +59 -0
- package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.messages.d.ts +12 -0
- package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.messages.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/amountInput/AmountInput.d.ts +13 -0
- package/build/types/expressiveMoneyInput/amountInput/AmountInput.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/amountInput/utils.d.ts +22 -0
- package/build/types/expressiveMoneyInput/amountInput/utils.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/animatedNumber/AnimatedNumber.d.ts +9 -0
- package/build/types/expressiveMoneyInput/animatedNumber/AnimatedNumber.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/chevron/Chevron.d.ts +6 -0
- package/build/types/expressiveMoneyInput/chevron/Chevron.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/currencySelector/CurrencySelector.d.ts +30 -0
- package/build/types/expressiveMoneyInput/currencySelector/CurrencySelector.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/hooks/useFocus.d.ts +7 -0
- package/build/types/expressiveMoneyInput/hooks/useFocus.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/hooks/useInputStyle.d.ts +10 -0
- package/build/types/expressiveMoneyInput/hooks/useInputStyle.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/hooks/useSelectionRange.d.ts +10 -0
- package/build/types/expressiveMoneyInput/hooks/useSelectionRange.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/index.d.ts +3 -0
- package/build/types/expressiveMoneyInput/index.d.ts.map +1 -0
- package/build/types/index.d.ts +2 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts +3 -2
- package/build/types/prompt/InlinePrompt/InlinePrompt.d.ts.map +1 -1
- package/build/types/test-utils/index.d.ts +4 -0
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/build/types/withDisplayFormat/WithDisplayFormat.d.ts.map +1 -1
- package/build/withDisplayFormat/WithDisplayFormat.js +0 -1
- package/build/withDisplayFormat/WithDisplayFormat.js.map +1 -1
- package/build/withDisplayFormat/WithDisplayFormat.mjs +0 -1
- package/build/withDisplayFormat/WithDisplayFormat.mjs.map +1 -1
- package/package.json +11 -4
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.autofocus.docs.mdx +12 -0
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.css +58 -0
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.less +13 -0
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.messages.ts +13 -0
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.story.tsx +232 -0
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.tsx +156 -0
- package/src/expressiveMoneyInput/amountInput/AmountInput.css +32 -0
- package/src/expressiveMoneyInput/amountInput/AmountInput.less +43 -0
- package/src/expressiveMoneyInput/amountInput/AmountInput.tsx +353 -0
- package/src/expressiveMoneyInput/amountInput/utils.spec.ts +114 -0
- package/src/expressiveMoneyInput/amountInput/utils.ts +116 -0
- package/src/expressiveMoneyInput/animatedNumber/AnimatedNumber.tsx +40 -0
- package/src/expressiveMoneyInput/chevron/Chevron.css +12 -0
- package/src/expressiveMoneyInput/chevron/Chevron.less +13 -0
- package/src/expressiveMoneyInput/chevron/Chevron.tsx +35 -0
- package/src/expressiveMoneyInput/currencySelector/CurrencySelector.css +6 -0
- package/src/expressiveMoneyInput/currencySelector/CurrencySelector.less +7 -0
- package/src/expressiveMoneyInput/currencySelector/CurrencySelector.tsx +220 -0
- package/src/expressiveMoneyInput/hooks/useFocus.ts +35 -0
- package/src/expressiveMoneyInput/hooks/useInputStyle.ts +85 -0
- package/src/expressiveMoneyInput/hooks/useSelectionRange.ts +23 -0
- package/src/expressiveMoneyInput/index.ts +2 -0
- package/src/i18n/en.json +2 -0
- package/src/index.ts +2 -0
- package/src/listItem/useListItemControl.tsx +2 -2
- package/src/listItem/useListItemMedia.tsx +2 -2
- package/src/main.css +73 -7
- package/src/main.less +1 -0
- package/src/moneyInput/MoneyInput.css +8 -0
- package/src/moneyInput/MoneyInput.less +5 -0
- package/src/prompt/InlinePrompt/InlinePrompt.css +7 -7
- package/src/prompt/InlinePrompt/InlinePrompt.less +7 -7
- package/src/prompt/InlinePrompt/InlinePrompt.spec.tsx +6 -0
- package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +39 -0
- package/src/prompt/InlinePrompt/InlinePrompt.tsx +12 -2
- package/src/ssr.spec.tsx +1 -0
- package/src/withDisplayFormat/WithDisplayFormat.spec.js +28 -1
- package/src/withDisplayFormat/WithDisplayFormat.tsx +0 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { formatAmount } from '@transferwise/formatting';
|
|
2
|
+
import type { KeyboardEvent } from 'react';
|
|
3
|
+
|
|
4
|
+
export const getDecimalSeparator = (currency: string, locale: string): string | null => {
|
|
5
|
+
return formatAmount(1.1, currency, locale).replace(/\p{Number}/gu, '');
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const getGroupSeparator = (currency: string, locale: string): string | null => {
|
|
9
|
+
return formatAmount(10000000, currency, locale).replace(/\p{Number}/gu, '')[0];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const getDecimalCount = (currency: string, locale: string): number => {
|
|
13
|
+
const decimalSeparator = getDecimalSeparator(currency, locale);
|
|
14
|
+
if (!decimalSeparator) {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
const parts = formatAmount(1.1, currency, locale).split(decimalSeparator);
|
|
18
|
+
return parts.length === 2 ? parts[1].length : 0;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const getEnteredDecimalsCount = (value: string, decimalSeparator: string) => {
|
|
22
|
+
return value.split(decimalSeparator)[1]?.length ?? 0;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const getUnformattedNumber = ({
|
|
26
|
+
value: formattedValue,
|
|
27
|
+
currency,
|
|
28
|
+
locale,
|
|
29
|
+
}: {
|
|
30
|
+
value: string;
|
|
31
|
+
currency: string;
|
|
32
|
+
locale: string;
|
|
33
|
+
}): number | null => {
|
|
34
|
+
const groupSeparator = getGroupSeparator(currency, locale);
|
|
35
|
+
const decimalSeparator = getDecimalSeparator(currency, locale);
|
|
36
|
+
if (!formattedValue) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// parseFloat can't handle thousands separators
|
|
41
|
+
const withoutGroupSeparator = groupSeparator
|
|
42
|
+
? formattedValue.replace(/ /gu, '').replace(new RegExp(`\\${groupSeparator}`, 'g'), '')
|
|
43
|
+
: formattedValue;
|
|
44
|
+
|
|
45
|
+
// parseFloat can only handle . as decimal separator
|
|
46
|
+
const withNormalisedDecimalSeparator = decimalSeparator
|
|
47
|
+
? withoutGroupSeparator.replace(decimalSeparator, '.')
|
|
48
|
+
: withoutGroupSeparator;
|
|
49
|
+
|
|
50
|
+
const parsedValue = Number.parseFloat(withNormalisedDecimalSeparator);
|
|
51
|
+
return parsedValue;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const getFormattedString = ({
|
|
55
|
+
value: unformattedValue,
|
|
56
|
+
currency,
|
|
57
|
+
locale,
|
|
58
|
+
alwaysShowDecimals = false,
|
|
59
|
+
}: {
|
|
60
|
+
value: number;
|
|
61
|
+
currency: string;
|
|
62
|
+
locale: string;
|
|
63
|
+
alwaysShowDecimals?: boolean;
|
|
64
|
+
}): string => {
|
|
65
|
+
const decimalSeparator = getDecimalSeparator(currency, locale);
|
|
66
|
+
// formatAmount rounds extra decimals, so 1.999 will become 2. Instead we will manually strip extra decimals so that it becomes 1.99 after formatting
|
|
67
|
+
const decimalCount = getDecimalCount(currency, locale);
|
|
68
|
+
const unformattedString = unformattedValue.toString();
|
|
69
|
+
|
|
70
|
+
const [integerPart, decimalPart] = decimalSeparator
|
|
71
|
+
? unformattedString.split(decimalSeparator)
|
|
72
|
+
: [unformattedString, undefined];
|
|
73
|
+
|
|
74
|
+
const formattedDecimalPart = decimalPart ? decimalPart.slice(0, decimalCount) : '';
|
|
75
|
+
|
|
76
|
+
const sanitisedUnformattedValue = Number.parseFloat(`${integerPart}.${formattedDecimalPart}`);
|
|
77
|
+
|
|
78
|
+
return formatAmount(sanitisedUnformattedValue, currency, locale, {
|
|
79
|
+
alwaysShowDecimals,
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const isInputPossiblyOverflowing = ({
|
|
84
|
+
ref,
|
|
85
|
+
value,
|
|
86
|
+
}: {
|
|
87
|
+
ref: React.RefObject<HTMLInputElement>;
|
|
88
|
+
value: string;
|
|
89
|
+
}) => {
|
|
90
|
+
const textLength = value.length;
|
|
91
|
+
const inputWidth = ref.current?.clientWidth;
|
|
92
|
+
if (!inputWidth || !textLength) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const maxCharactersWithoutOverflow = Math.floor(inputWidth / 19);
|
|
97
|
+
return textLength > maxCharactersWithoutOverflow;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const allowedInputKeys = new Set([
|
|
101
|
+
'Backspace',
|
|
102
|
+
'Delete',
|
|
103
|
+
',',
|
|
104
|
+
'.',
|
|
105
|
+
'ArrowLeft',
|
|
106
|
+
'ArrowRight',
|
|
107
|
+
'Enter',
|
|
108
|
+
'Tab',
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
export const isAllowedInputKey = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
112
|
+
const { metaKey, key, ctrlKey } = e;
|
|
113
|
+
const isNumberKey = !Number.isNaN(Number.parseInt(key, 10));
|
|
114
|
+
|
|
115
|
+
return isNumberKey || metaKey || ctrlKey || allowedInputKeys.has(key);
|
|
116
|
+
};
|
|
@@ -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,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 />
|
|
32
|
+
</motion.div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
]
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.filter((avatar) => !(avatar && Object.keys(avatar).length === 0)),
|
|
125
|
+
}}
|
|
126
|
+
addonEnd={disabled ? undefined : { type: 'icon', value: <ChevronDown /> }}
|
|
127
|
+
onClick={(event) => handleTriggerClick(event)}
|
|
128
|
+
>
|
|
129
|
+
{currency}
|
|
130
|
+
</SelectInputTriggerButton>
|
|
131
|
+
)}
|
|
132
|
+
onChange={(newValue) => {
|
|
133
|
+
onChange?.(newValue.code);
|
|
134
|
+
}}
|
|
135
|
+
onFilterChange={({ queryNormalized }) => {
|
|
136
|
+
setSearchQuery(queryNormalized ?? '');
|
|
137
|
+
if (queryNormalized) {
|
|
138
|
+
onSearchChange?.({
|
|
139
|
+
query: queryNormalized,
|
|
140
|
+
resultCount: filterAndSortCurrenciesForQuery(allCurrencyOptions, queryNormalized)
|
|
141
|
+
.length,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const ButtonInput = forwardRef(function ButtonInput(
|
|
150
|
+
{ children, ...rest }: React.PropsWithChildren<ButtonHTMLAttributes<HTMLButtonElement>>,
|
|
151
|
+
ref: React.ForwardedRef<HTMLButtonElement | null>,
|
|
152
|
+
) {
|
|
153
|
+
return (
|
|
154
|
+
<Button
|
|
155
|
+
ref={ref}
|
|
156
|
+
size="md"
|
|
157
|
+
v2
|
|
158
|
+
className="wds-currency-selector"
|
|
159
|
+
priority="secondary-neutral"
|
|
160
|
+
{...rest}
|
|
161
|
+
>
|
|
162
|
+
{children}
|
|
163
|
+
</Button>
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const getCurrencySelectOption = (currency: CurrencyOption) => {
|
|
168
|
+
return {
|
|
169
|
+
type: 'option' as const,
|
|
170
|
+
value: currency,
|
|
171
|
+
filterMatchers: currency.keywords,
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const getCurrencyGroup = (section: CurrencySection) => {
|
|
176
|
+
return {
|
|
177
|
+
type: 'group' as const,
|
|
178
|
+
label: section.title,
|
|
179
|
+
options: section.currencies.map(getCurrencySelectOption),
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const getUniqueCurrencies = (options: CurrencyOptions) => {
|
|
184
|
+
const allCurrencyOptions = options.flatMap((section) => section.currencies);
|
|
185
|
+
const uniqueCurrencies = new Map<string, CurrencyOption>();
|
|
186
|
+
|
|
187
|
+
allCurrencyOptions.forEach((currencyObj) => {
|
|
188
|
+
uniqueCurrencies.set(currencyObj.code, currencyObj);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return Array.from(uniqueCurrencies.values());
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const filterAndSortCurrenciesForQuery = (
|
|
195
|
+
currencies: CurrencyOption[],
|
|
196
|
+
query: string,
|
|
197
|
+
): CurrencyOption[] => {
|
|
198
|
+
return (
|
|
199
|
+
currencies
|
|
200
|
+
.filter((currency) => {
|
|
201
|
+
return (
|
|
202
|
+
currency.code.toLowerCase().includes(query) ||
|
|
203
|
+
(currency.label ?? '').toLowerCase().includes(query) ||
|
|
204
|
+
currency.keywords?.some((keyword) => keyword.toLowerCase().includes(query))
|
|
205
|
+
);
|
|
206
|
+
})
|
|
207
|
+
// prefer exact matches, then sort alphabetically by code
|
|
208
|
+
.sort((a, b) => {
|
|
209
|
+
const aCode = a.code.toLowerCase();
|
|
210
|
+
const bCode = b.code.toLowerCase();
|
|
211
|
+
if (aCode === query) {
|
|
212
|
+
return -1;
|
|
213
|
+
}
|
|
214
|
+
if (bCode === query) {
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
217
|
+
return aCode.localeCompare(bCode);
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type CSSProperties, useEffect, useLayoutEffect, useState } from 'react';
|
|
2
|
+
import { Props as ExpressiveMoneyInputProps } from '../ExpressiveMoneyInput';
|
|
3
|
+
|
|
4
|
+
type InputStyleObject = {
|
|
5
|
+
value: string;
|
|
6
|
+
focus: boolean;
|
|
7
|
+
inputElement: HTMLInputElement | null;
|
|
8
|
+
} & Pick<ExpressiveMoneyInputProps, 'loading'>;
|
|
9
|
+
|
|
10
|
+
export const useInputStyle = ({ value, focus, inputElement, loading }: InputStyleObject) => {
|
|
11
|
+
const initialRender = !useTimeout(300);
|
|
12
|
+
const inputWidth = useFirstDefinedValue(inputElement?.clientWidth, [inputElement, value]);
|
|
13
|
+
|
|
14
|
+
const getStyle = (): CSSProperties => {
|
|
15
|
+
const fontSize = getFontSize(value, focus, inputWidth);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
fontSize,
|
|
19
|
+
height: fontSize,
|
|
20
|
+
// aligns the top of the digit with the currency button
|
|
21
|
+
marginTop: fontSize * -0.19,
|
|
22
|
+
// if the input loads with a pre-filled value, we don't want to animate the font-size immediately
|
|
23
|
+
transition: initialRender ? 'none' : undefined,
|
|
24
|
+
color: loading ? 'var(--color-interactive-secondary)' : undefined,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const [style, setStyle] = useState(getStyle());
|
|
29
|
+
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
setStyle(getStyle());
|
|
32
|
+
}, [value, focus, loading, inputWidth]);
|
|
33
|
+
|
|
34
|
+
return style;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function getFontSize(inputValue: string, isFocused: boolean, inputWidth: number | undefined) {
|
|
38
|
+
const defaultFontSize = 52;
|
|
39
|
+
const focusFontSize = 90;
|
|
40
|
+
const minimumFontSize = 34;
|
|
41
|
+
|
|
42
|
+
let fontSize = isFocused ? focusFontSize : defaultFontSize;
|
|
43
|
+
|
|
44
|
+
if (typeof inputWidth === 'undefined') {
|
|
45
|
+
return fontSize;
|
|
46
|
+
}
|
|
47
|
+
const textLength = inputValue.length;
|
|
48
|
+
const maxCharactersWithoutShrinking = Math.floor(inputWidth / 40);
|
|
49
|
+
|
|
50
|
+
if (textLength > maxCharactersWithoutShrinking) {
|
|
51
|
+
const adjustedSize = Math.round((inputWidth / textLength) * 1.9);
|
|
52
|
+
fontSize = Math.min(fontSize, adjustedSize);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Math.max(fontSize, minimumFontSize);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const useFirstDefinedValue = (newValue: number | undefined, dependencies: unknown[]) => {
|
|
59
|
+
const [value, setValue] = useState<number | undefined>(newValue);
|
|
60
|
+
|
|
61
|
+
useLayoutEffect(() => {
|
|
62
|
+
if (typeof newValue !== 'undefined' && typeof value === 'undefined') {
|
|
63
|
+
setValue(newValue);
|
|
64
|
+
}
|
|
65
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
+
}, [...dependencies, value]);
|
|
67
|
+
|
|
68
|
+
return value;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const useTimeout = (delay: number) => {
|
|
72
|
+
const [ready, setReady] = useState(false);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const timeout = setTimeout(() => {
|
|
76
|
+
setReady(true);
|
|
77
|
+
}, delay);
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
};
|
|
82
|
+
}, [delay]);
|
|
83
|
+
|
|
84
|
+
return ready;
|
|
85
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type SyntheticEvent, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export const useSelectionRange = () => {
|
|
4
|
+
const selection = useRef<{
|
|
5
|
+
selectionStart: number | null;
|
|
6
|
+
selectionEnd: number | null;
|
|
7
|
+
}>();
|
|
8
|
+
|
|
9
|
+
const handleSelect = (e: SyntheticEvent<HTMLInputElement>) => {
|
|
10
|
+
const input = e.target as HTMLInputElement;
|
|
11
|
+
const { selectionStart, selectionEnd } = input;
|
|
12
|
+
selection.current = { selectionStart, selectionEnd };
|
|
13
|
+
};
|
|
14
|
+
const handleSelectionBlur = () => {
|
|
15
|
+
selection.current = undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
selection: selection.current,
|
|
20
|
+
handleSelect,
|
|
21
|
+
handleSelectionBlur,
|
|
22
|
+
};
|
|
23
|
+
};
|
package/src/i18n/en.json
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"neptune.DateLookup.selected": "selected",
|
|
17
17
|
"neptune.DateLookup.twentyYears": "20 years",
|
|
18
18
|
"neptune.DateLookup.year": "year",
|
|
19
|
+
"neptune.ExpressiveMoneyInput.currency.search.placeholder": "Type a currency / country",
|
|
20
|
+
"neptune.ExpressiveMoneyInput.currency.select.currency": "Select currency",
|
|
19
21
|
"neptune.FlowNavigation.back": "back to previous step",
|
|
20
22
|
"neptune.Info.ariaLabel": "More information",
|
|
21
23
|
"neptune.Label.optional": "(Optional)",
|
package/src/index.ts
CHANGED
|
@@ -62,6 +62,7 @@ export type {
|
|
|
62
62
|
CurrencyOptionItem,
|
|
63
63
|
MoneyInputProps,
|
|
64
64
|
} from './moneyInput';
|
|
65
|
+
export type { ExpressiveMoneyInputProps } from './expressiveMoneyInput';
|
|
65
66
|
export type { NavigationOptionListProps } from './navigationOptionsList';
|
|
66
67
|
export type { NavigationOptionProps } from './navigationOption/NavigationOption';
|
|
67
68
|
export type { OverlayHeaderProps } from './overlayHeader';
|
|
@@ -188,6 +189,7 @@ export { default as Markdown } from './markdown';
|
|
|
188
189
|
export { default as Modal } from './modal';
|
|
189
190
|
export { default as Money } from './money';
|
|
190
191
|
export { default as MoneyInput } from './moneyInput';
|
|
192
|
+
export { default as ExpressiveMoneyInput } from './expressiveMoneyInput';
|
|
191
193
|
export { default as NavigationOption } from './navigationOption';
|
|
192
194
|
export { default as NavigationOptionsList } from './navigationOptionsList';
|
|
193
195
|
export { default as Nudge } from './nudge';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useContext,
|
|
1
|
+
import { useContext, useLayoutEffect } from 'react';
|
|
2
2
|
import { ListItemContext, type ListItemContextData } from './ListItemContext';
|
|
3
3
|
import type { ListItemTypes, ListItemControlProps } from './ListItem';
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ export function useListItemControl(controlType: ListItemTypes, controlProps: Lis
|
|
|
9
9
|
props: baseItemProps,
|
|
10
10
|
} = useContext<ListItemContextData>(ListItemContext);
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
13
|
setControlType(controlType);
|
|
14
14
|
setControlProps(controlProps);
|
|
15
15
|
}, [controlType, controlProps, setControlType, setControlProps]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useContext,
|
|
1
|
+
import { useContext, useLayoutEffect } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
ListItemContext,
|
|
4
4
|
type ListItemContextData,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
export function useListItemMedia(size?: ListItemMediaSize) {
|
|
9
9
|
const { setMediaSize, mediaSize } = useContext<ListItemContextData>(ListItemContext);
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
useLayoutEffect(() => {
|
|
12
12
|
setMediaSize(size);
|
|
13
13
|
}, [size, setMediaSize]);
|
|
14
14
|
|