@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.
- package/build/expressiveMoneyInput/AmountInput.js +281 -0
- package/build/expressiveMoneyInput/AmountInput.js.map +1 -0
- package/build/expressiveMoneyInput/AmountInput.mjs +279 -0
- package/build/expressiveMoneyInput/AmountInput.mjs.map +1 -0
- package/build/expressiveMoneyInput/AnimatedNumber.js +50 -0
- package/build/expressiveMoneyInput/AnimatedNumber.js.map +1 -0
- package/build/expressiveMoneyInput/AnimatedNumber.mjs +48 -0
- package/build/expressiveMoneyInput/AnimatedNumber.mjs.map +1 -0
- package/build/expressiveMoneyInput/Chevron.js +33 -0
- package/build/expressiveMoneyInput/Chevron.js.map +1 -0
- package/build/expressiveMoneyInput/Chevron.mjs +31 -0
- package/build/expressiveMoneyInput/Chevron.mjs.map +1 -0
- package/build/expressiveMoneyInput/CurrencySelector.js +160 -0
- package/build/expressiveMoneyInput/CurrencySelector.js.map +1 -0
- package/build/expressiveMoneyInput/CurrencySelector.mjs +157 -0
- package/build/expressiveMoneyInput/CurrencySelector.mjs.map +1 -0
- package/build/expressiveMoneyInput/ExpressiveMoneyInput.js +114 -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 +110 -0
- package/build/expressiveMoneyInput/ExpressiveMoneyInput.mjs.map +1 -0
- package/build/expressiveMoneyInput/useFocus.js +37 -0
- package/build/expressiveMoneyInput/useFocus.js.map +1 -0
- package/build/expressiveMoneyInput/useFocus.mjs +35 -0
- package/build/expressiveMoneyInput/useFocus.mjs.map +1 -0
- package/build/expressiveMoneyInput/useInputStyle.js +71 -0
- package/build/expressiveMoneyInput/useInputStyle.js.map +1 -0
- package/build/expressiveMoneyInput/useInputStyle.mjs +69 -0
- package/build/expressiveMoneyInput/useInputStyle.mjs.map +1 -0
- package/build/expressiveMoneyInput/utils.js +87 -0
- package/build/expressiveMoneyInput/utils.js.map +1 -0
- package/build/expressiveMoneyInput/utils.mjs +78 -0
- package/build/expressiveMoneyInput/utils.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/main.css +65 -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/AmountInput.css +32 -0
- package/build/styles/expressiveMoneyInput/Chevron.css +12 -0
- package/build/styles/expressiveMoneyInput/CurrencySelector.css +6 -0
- package/build/styles/expressiveMoneyInput/ExpressiveMoneyInput.css +58 -0
- package/build/styles/main.css +65 -7
- package/build/styles/prompt/InlinePrompt/InlinePrompt.css +7 -7
- package/build/types/expressiveMoneyInput/AmountInput.d.ts +13 -0
- package/build/types/expressiveMoneyInput/AmountInput.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/AnimatedNumber.d.ts +9 -0
- package/build/types/expressiveMoneyInput/AnimatedNumber.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/Chevron.d.ts +6 -0
- package/build/types/expressiveMoneyInput/Chevron.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/CurrencySelector.d.ts +30 -0
- package/build/types/expressiveMoneyInput/CurrencySelector.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/ExpressiveMoneyInput.d.ts +33 -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/index.d.ts +3 -0
- package/build/types/expressiveMoneyInput/index.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/useFocus.d.ts +7 -0
- package/build/types/expressiveMoneyInput/useFocus.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/useInputStyle.d.ts +10 -0
- package/build/types/expressiveMoneyInput/useInputStyle.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/useSelectionRange.d.ts +10 -0
- package/build/types/expressiveMoneyInput/useSelectionRange.d.ts.map +1 -0
- package/build/types/expressiveMoneyInput/utils.d.ts +22 -0
- package/build/types/expressiveMoneyInput/utils.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 +1 -0
- package/build/withDisplayFormat/WithDisplayFormat.js.map +1 -1
- package/build/withDisplayFormat/WithDisplayFormat.mjs +1 -0
- package/build/withDisplayFormat/WithDisplayFormat.mjs.map +1 -1
- package/package.json +1 -1
- package/src/expressiveMoneyInput/AmountInput.css +32 -0
- package/src/expressiveMoneyInput/AmountInput.less +43 -0
- package/src/expressiveMoneyInput/AmountInput.tsx +353 -0
- package/src/expressiveMoneyInput/AnimatedNumber.tsx +40 -0
- package/src/expressiveMoneyInput/Chevron.css +12 -0
- package/src/expressiveMoneyInput/Chevron.less +13 -0
- package/src/expressiveMoneyInput/Chevron.tsx +35 -0
- package/src/expressiveMoneyInput/CurrencySelector.css +6 -0
- package/src/expressiveMoneyInput/CurrencySelector.less +7 -0
- package/src/expressiveMoneyInput/CurrencySelector.tsx +218 -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 +290 -0
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.tsx +118 -0
- package/src/expressiveMoneyInput/index.ts +2 -0
- package/src/expressiveMoneyInput/useFocus.ts +35 -0
- package/src/expressiveMoneyInput/useInputStyle.ts +85 -0
- package/src/expressiveMoneyInput/useSelectionRange.ts +23 -0
- package/src/expressiveMoneyInput/utils.spec.ts +114 -0
- package/src/expressiveMoneyInput/utils.ts +116 -0
- package/src/i18n/en.json +2 -0
- package/src/index.ts +2 -0
- package/src/main.css +65 -7
- package/src/main.less +1 -0
- package/src/prompt/InlinePrompt/InlinePrompt.css +7 -7
- package/src/prompt/InlinePrompt/InlinePrompt.less +7 -7
- package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +49 -0
- package/src/prompt/InlinePrompt/InlinePrompt.tsx +12 -2
- package/src/ssr.spec.tsx +1 -0
- package/src/withDisplayFormat/WithDisplayFormat.tsx +1 -0
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDecimalSeparator,
|
|
3
|
+
getFormattedString,
|
|
4
|
+
getGroupSeparator,
|
|
5
|
+
getUnformattedNumber,
|
|
6
|
+
} from './utils';
|
|
7
|
+
|
|
8
|
+
describe('utils', () => {
|
|
9
|
+
describe('getDecimalSeparator', () => {
|
|
10
|
+
it('gets decimal separator for Spanish', () => {
|
|
11
|
+
expect(getDecimalSeparator('EUR', 'es')).toBe(',');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('gets decimal separator for English', () => {
|
|
15
|
+
expect(getDecimalSeparator('EUR', 'en')).toBe('.');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('gets decimal separator for Spanish in HUF', () => {
|
|
19
|
+
expect(getDecimalSeparator('HUF', 'es')).toBe('');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('gets decimal separator for English in HUF', () => {
|
|
23
|
+
expect(getDecimalSeparator('HUF', 'en')).toBe('');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('getGroupSeparator', () => {
|
|
28
|
+
it('gets group separator for Spanish', () => {
|
|
29
|
+
expect(getGroupSeparator('EUR', 'es')).toBe('.');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('gets group separator for English', () => {
|
|
33
|
+
expect(getGroupSeparator('EUR', 'en')).toBe(',');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('gets group separator for French', () => {
|
|
37
|
+
expect(getGroupSeparator('EUR', 'fr')).toBe(' ');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('getUnformattedNumber', () => {
|
|
42
|
+
it('can turn a Spanish string into a number', () => {
|
|
43
|
+
expect(
|
|
44
|
+
getUnformattedNumber({
|
|
45
|
+
value: '123.456,00',
|
|
46
|
+
currency: 'EUR',
|
|
47
|
+
locale: 'es',
|
|
48
|
+
}),
|
|
49
|
+
).toBe(123456);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('can turn a French string into a number', () => {
|
|
53
|
+
expect(
|
|
54
|
+
getUnformattedNumber({
|
|
55
|
+
value: '1 234 567,45',
|
|
56
|
+
currency: 'EUR',
|
|
57
|
+
locale: 'fr',
|
|
58
|
+
}),
|
|
59
|
+
).toBe(1234567.45);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('can turn a English string into a number', () => {
|
|
63
|
+
expect(
|
|
64
|
+
getUnformattedNumber({
|
|
65
|
+
value: '123,456.78',
|
|
66
|
+
currency: 'EUR',
|
|
67
|
+
locale: 'en',
|
|
68
|
+
}),
|
|
69
|
+
).toBe(123456.78);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('can turn a Magyar string into a number', () => {
|
|
73
|
+
expect(
|
|
74
|
+
getUnformattedNumber({
|
|
75
|
+
value: '11 000 000',
|
|
76
|
+
currency: 'HUF',
|
|
77
|
+
locale: 'hu',
|
|
78
|
+
}),
|
|
79
|
+
).toBe(11000000);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('getFormattedString', () => {
|
|
84
|
+
it('can turn a number into a Spanish string', () => {
|
|
85
|
+
expect(
|
|
86
|
+
getFormattedString({
|
|
87
|
+
value: 123456.45,
|
|
88
|
+
currency: 'EUR',
|
|
89
|
+
locale: 'es',
|
|
90
|
+
}),
|
|
91
|
+
).toBe('123.456,45');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('can turn a number into a French string', () => {
|
|
95
|
+
expect(
|
|
96
|
+
getFormattedString({
|
|
97
|
+
value: 1234567.45,
|
|
98
|
+
currency: 'EUR',
|
|
99
|
+
locale: 'fr',
|
|
100
|
+
}),
|
|
101
|
+
).toBe('1 234 567,45');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('can turn a number into a English string', () => {
|
|
105
|
+
expect(
|
|
106
|
+
getFormattedString({
|
|
107
|
+
value: 123456.78,
|
|
108
|
+
currency: 'EUR',
|
|
109
|
+
locale: 'en',
|
|
110
|
+
}),
|
|
111
|
+
).toBe('123,456.78');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -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
|
+
};
|
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';
|
package/src/main.css
CHANGED
|
@@ -526,26 +526,26 @@
|
|
|
526
526
|
background-color: var(--color-sentiment-positive-secondary-active);
|
|
527
527
|
}
|
|
528
528
|
.wds-inline-prompt--proposition {
|
|
529
|
-
background-color:
|
|
530
|
-
color: var(--color-
|
|
529
|
+
background-color: #D2F9F7;
|
|
530
|
+
color: var(--color-interactive-primary);
|
|
531
531
|
}
|
|
532
532
|
.wds-inline-prompt--proposition a,
|
|
533
533
|
.wds-inline-prompt--proposition button {
|
|
534
|
-
color: var(--color-
|
|
534
|
+
color: var(--color-interactive-primary);
|
|
535
535
|
}
|
|
536
536
|
.wds-inline-prompt--proposition a:hover,
|
|
537
537
|
.wds-inline-prompt--proposition button:hover {
|
|
538
|
-
color: var(--color-
|
|
538
|
+
color: var(--color-interactive-primary-hover);
|
|
539
539
|
}
|
|
540
540
|
.wds-inline-prompt--proposition a:active,
|
|
541
541
|
.wds-inline-prompt--proposition button:active {
|
|
542
|
-
color: var(--color-
|
|
542
|
+
color: var(--color-interactive-primary-active);
|
|
543
543
|
}
|
|
544
544
|
.wds-inline-prompt.wds-inline-prompt--proposition:has(a, button):hover {
|
|
545
|
-
background-color:
|
|
545
|
+
background-color: #B2F4F3;
|
|
546
546
|
}
|
|
547
547
|
.wds-inline-prompt.wds-inline-prompt--proposition:has(a, button):active {
|
|
548
|
-
background-color:
|
|
548
|
+
background-color: #91F0EE;
|
|
549
549
|
}
|
|
550
550
|
.wds-inline-prompt--neutral {
|
|
551
551
|
background-color: rgba(134,167,189,0.10196);
|
|
@@ -4447,6 +4447,64 @@ button.np-link {
|
|
|
4447
4447
|
box-shadow: inset 0 0 0 1px #c9cbce !important;
|
|
4448
4448
|
box-shadow: inset 0 0 0 1px var(--color-interactive-secondary) !important;
|
|
4449
4449
|
}
|
|
4450
|
+
.wds-amount-input-container {
|
|
4451
|
+
width: 100%;
|
|
4452
|
+
}
|
|
4453
|
+
.wds-amount-input-input-container {
|
|
4454
|
+
display: flex;
|
|
4455
|
+
justify-content: right;
|
|
4456
|
+
width: 100%;
|
|
4457
|
+
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;
|
|
4458
|
+
color: var(--color-interactive-primary);
|
|
4459
|
+
overflow: hidden;
|
|
4460
|
+
margin-bottom: 0 !important;
|
|
4461
|
+
}
|
|
4462
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4463
|
+
.wds-amount-input-input-container {
|
|
4464
|
+
transition: none;
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
.wds-amount-input-input {
|
|
4468
|
+
border: none;
|
|
4469
|
+
outline: none;
|
|
4470
|
+
flex-grow: 1;
|
|
4471
|
+
text-align: right;
|
|
4472
|
+
background-color: transparent;
|
|
4473
|
+
}
|
|
4474
|
+
.wds-amount-input-input:focus-visible {
|
|
4475
|
+
outline: none;
|
|
4476
|
+
}
|
|
4477
|
+
.wds-amount-input-placeholder {
|
|
4478
|
+
flex-grow: 0;
|
|
4479
|
+
display: flex;
|
|
4480
|
+
align-items: center;
|
|
4481
|
+
}
|
|
4482
|
+
.wds-currency-selector:disabled {
|
|
4483
|
+
opacity: 1 !important;
|
|
4484
|
+
cursor: auto !important;
|
|
4485
|
+
cursor: initial !important;
|
|
4486
|
+
mix-blend-mode: initial !important;
|
|
4487
|
+
}
|
|
4488
|
+
.wds-chevron-container {
|
|
4489
|
+
width: 32px;
|
|
4490
|
+
width: var(--size-32);
|
|
4491
|
+
overflow: hidden;
|
|
4492
|
+
color: var(--color-interactive-primary);
|
|
4493
|
+
margin-left: 8px;
|
|
4494
|
+
margin-left: var(--size-8);
|
|
4495
|
+
transition: width 0.3s ease;
|
|
4496
|
+
}
|
|
4497
|
+
.wds-chevron-hidden {
|
|
4498
|
+
width: 0;
|
|
4499
|
+
}
|
|
4500
|
+
.wds-expressive-money-input-currency-selector {
|
|
4501
|
+
flex-shrink: 0;
|
|
4502
|
+
margin-right: 24px;
|
|
4503
|
+
margin-right: var(--size-24);
|
|
4504
|
+
}
|
|
4505
|
+
.wds-expressive-money-input-chevron {
|
|
4506
|
+
transform: translateY(-5%);
|
|
4507
|
+
}
|
|
4450
4508
|
.np-navigation-option {
|
|
4451
4509
|
-webkit-text-decoration: none;
|
|
4452
4510
|
text-decoration: none;
|
package/src/main.less
CHANGED
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
@import "./logo/Logo.less";
|
|
52
52
|
@import "./modal/Modal.less";
|
|
53
53
|
@import "./moneyInput/MoneyInput.less";
|
|
54
|
+
@import "./expressiveMoneyInput/ExpressiveMoneyInput.less";
|
|
54
55
|
@import "./navigationOption/NavigationOption.less";
|
|
55
56
|
@import "./navigationOptionsList/NavigationOptionsList.less";
|
|
56
57
|
@import "./nudge/Nudge.less";
|
|
@@ -91,26 +91,26 @@
|
|
|
91
91
|
background-color: var(--color-sentiment-positive-secondary-active);
|
|
92
92
|
}
|
|
93
93
|
.wds-inline-prompt--proposition {
|
|
94
|
-
background-color:
|
|
95
|
-
color: var(--color-
|
|
94
|
+
background-color: #D2F9F7;
|
|
95
|
+
color: var(--color-interactive-primary);
|
|
96
96
|
}
|
|
97
97
|
.wds-inline-prompt--proposition a,
|
|
98
98
|
.wds-inline-prompt--proposition button {
|
|
99
|
-
color: var(--color-
|
|
99
|
+
color: var(--color-interactive-primary);
|
|
100
100
|
}
|
|
101
101
|
.wds-inline-prompt--proposition a:hover,
|
|
102
102
|
.wds-inline-prompt--proposition button:hover {
|
|
103
|
-
color: var(--color-
|
|
103
|
+
color: var(--color-interactive-primary-hover);
|
|
104
104
|
}
|
|
105
105
|
.wds-inline-prompt--proposition a:active,
|
|
106
106
|
.wds-inline-prompt--proposition button:active {
|
|
107
|
-
color: var(--color-
|
|
107
|
+
color: var(--color-interactive-primary-active);
|
|
108
108
|
}
|
|
109
109
|
.wds-inline-prompt.wds-inline-prompt--proposition:has(a, button):hover {
|
|
110
|
-
background-color:
|
|
110
|
+
background-color: #B2F4F3;
|
|
111
111
|
}
|
|
112
112
|
.wds-inline-prompt.wds-inline-prompt--proposition:has(a, button):active {
|
|
113
|
-
background-color:
|
|
113
|
+
background-color: #91F0EE;
|
|
114
114
|
}
|
|
115
115
|
.wds-inline-prompt--neutral {
|
|
116
116
|
background-color: rgba(134,167,189,0.10196);
|
|
@@ -94,26 +94,26 @@
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
&--proposition {
|
|
97
|
-
background-color:
|
|
97
|
+
background-color: #D2F9F7;
|
|
98
98
|
|
|
99
|
-
color: var(--color-
|
|
99
|
+
color: var(--color-interactive-primary);
|
|
100
100
|
|
|
101
101
|
a, button {
|
|
102
|
-
color: var(--color-
|
|
102
|
+
color: var(--color-interactive-primary);
|
|
103
103
|
&:hover {
|
|
104
|
-
color: var(--color-
|
|
104
|
+
color: var(--color-interactive-primary-hover);
|
|
105
105
|
}
|
|
106
106
|
&:active {
|
|
107
|
-
color: var(--color-
|
|
107
|
+
color: var(--color-interactive-primary-active);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
.wds-inline-prompt&:has(a, button) {
|
|
112
112
|
&:hover {
|
|
113
|
-
background-color:
|
|
113
|
+
background-color: #B2F4F3;
|
|
114
114
|
}
|
|
115
115
|
&:active {
|
|
116
|
-
background-color:
|
|
116
|
+
background-color: #91F0EE;
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { lorem10, lorem5 } from '../../test-utils';
|
|
2
|
+
import { InlinePrompt } from './InlinePrompt';
|
|
3
|
+
import { CardWise, Rewards } from '@transferwise/icons';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Internal/InlinePrompt',
|
|
7
|
+
component: InlinePrompt,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const AllVariants = () => {
|
|
11
|
+
return (
|
|
12
|
+
<div>
|
|
13
|
+
<InlinePrompt className="m-b-1" sentiment="positive">
|
|
14
|
+
{lorem5}
|
|
15
|
+
</InlinePrompt>
|
|
16
|
+
<InlinePrompt media={<Rewards />} className="m-b-1" sentiment="positive">
|
|
17
|
+
{lorem10}
|
|
18
|
+
</InlinePrompt>
|
|
19
|
+
<InlinePrompt className="m-b-1" sentiment="negative">
|
|
20
|
+
{lorem5}
|
|
21
|
+
</InlinePrompt>
|
|
22
|
+
<InlinePrompt className="m-b-1" sentiment="negative">
|
|
23
|
+
{lorem10}
|
|
24
|
+
</InlinePrompt>
|
|
25
|
+
<InlinePrompt className="m-b-1" sentiment="neutral">
|
|
26
|
+
{lorem5}
|
|
27
|
+
</InlinePrompt>
|
|
28
|
+
<InlinePrompt className="m-b-1" sentiment="neutral">
|
|
29
|
+
{lorem10}
|
|
30
|
+
</InlinePrompt>
|
|
31
|
+
<InlinePrompt className="m-b-1" sentiment="warning">
|
|
32
|
+
{lorem5}
|
|
33
|
+
</InlinePrompt>
|
|
34
|
+
<InlinePrompt className="m-b-1" sentiment="warning">
|
|
35
|
+
{lorem10}
|
|
36
|
+
</InlinePrompt>
|
|
37
|
+
<InlinePrompt className="m-b-1" sentiment="proposition">
|
|
38
|
+
{lorem5}
|
|
39
|
+
</InlinePrompt>
|
|
40
|
+
<InlinePrompt className="m-b-1" sentiment="proposition">
|
|
41
|
+
{lorem10}
|
|
42
|
+
</InlinePrompt>
|
|
43
|
+
<InlinePrompt className="m-b-1" sentiment="proposition" media={<CardWise />}>
|
|
44
|
+
{lorem10}
|
|
45
|
+
</InlinePrompt>
|
|
46
|
+
<InlinePrompt>{lorem5}</InlinePrompt>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Sentiment } from '../../common';
|
|
2
|
-
import { BackslashCircle } from '@transferwise/icons';
|
|
2
|
+
import { BackslashCircle, GiftBox } from '@transferwise/icons';
|
|
3
3
|
import ProcessIndicator from '../../processIndicator';
|
|
4
4
|
import StatusIcon from '../../statusIcon';
|
|
5
5
|
import { clsx } from 'clsx';
|
|
6
6
|
import Body from '../../body';
|
|
7
7
|
|
|
8
8
|
export type InlinePromptProps = {
|
|
9
|
-
sentiment?:
|
|
9
|
+
sentiment?:
|
|
10
|
+
| `${Sentiment.POSITIVE | Sentiment.NEGATIVE | Sentiment.NEUTRAL | Sentiment.WARNING}`
|
|
11
|
+
| 'proposition';
|
|
10
12
|
loading?: boolean;
|
|
11
13
|
/**
|
|
12
14
|
* Use for short-lived inline prompts to avoid swap of the icon (which is bad UX for short-lived prompts, e.g. when submit form)
|
|
@@ -16,6 +18,7 @@ export type InlinePromptProps = {
|
|
|
16
18
|
className?: string;
|
|
17
19
|
'data-testid'?: string;
|
|
18
20
|
children: React.ReactNode;
|
|
21
|
+
media?: React.ReactNode;
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
export const InlinePrompt = ({
|
|
@@ -24,9 +27,16 @@ export const InlinePrompt = ({
|
|
|
24
27
|
loading = false,
|
|
25
28
|
className,
|
|
26
29
|
children,
|
|
30
|
+
media = null,
|
|
27
31
|
...rest
|
|
28
32
|
}: InlinePromptProps) => {
|
|
29
33
|
const renderMedia = () => {
|
|
34
|
+
if (media && ['proposition', 'positive'].includes(sentiment)) {
|
|
35
|
+
return media;
|
|
36
|
+
}
|
|
37
|
+
if (sentiment === 'proposition') {
|
|
38
|
+
return <GiftBox />;
|
|
39
|
+
}
|
|
30
40
|
if (muted) {
|
|
31
41
|
return <BackslashCircle size={16} data-testid="InlinePrompt_Muted" />;
|
|
32
42
|
}
|
package/src/ssr.spec.tsx
CHANGED
|
@@ -92,6 +92,7 @@ describe('Server side rendering', () => {
|
|
|
92
92
|
// stick all possible properties we might need to render all components in here
|
|
93
93
|
const allProps: Record<string, unknown> = {
|
|
94
94
|
currencies: [],
|
|
95
|
+
currencySelector: { options: [] },
|
|
95
96
|
steps: [],
|
|
96
97
|
stepper: {
|
|
97
98
|
steps: [],
|