@ledgerhq/lumen-ui-rnative 0.1.39 → 0.1.41
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/dist/module/lib/Components/AmountDisplay/AmountDisplay.js +28 -11
- package/dist/module/lib/Components/AmountDisplay/AmountDisplay.js.map +1 -1
- package/dist/module/lib/Components/AmountDisplay/AmountDisplay.test.js +71 -0
- package/dist/module/lib/Components/AmountDisplay/AmountDisplay.test.js.map +1 -1
- package/dist/module/lib/Components/AmountInput/AmountInput.js +49 -154
- package/dist/module/lib/Components/AmountInput/AmountInput.js.map +1 -1
- package/dist/module/lib/Components/AmountInput/AmountInput.test.js +152 -0
- package/dist/module/lib/Components/AmountInput/AmountInput.test.js.map +1 -0
- package/dist/module/lib/Components/AmountInput/useAmountInputAnimations/useAmountInputAnimations.js +149 -0
- package/dist/module/lib/Components/AmountInput/useAmountInputAnimations/useAmountInputAnimations.js.map +1 -0
- package/dist/module/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.js +22 -0
- package/dist/module/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.js.map +1 -0
- package/dist/module/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.test.js +59 -0
- package/dist/module/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.test.js.map +1 -0
- package/dist/module/lib/Components/Avatar/Avatar.figma.js +5 -0
- package/dist/module/lib/Components/Avatar/Avatar.figma.js.map +1 -1
- package/dist/module/lib/Components/Avatar/Avatar.js +9 -2
- package/dist/module/lib/Components/Avatar/Avatar.js.map +1 -1
- package/dist/module/lib/Components/Avatar/Avatar.mdx +9 -0
- package/dist/module/lib/Components/Avatar/Avatar.stories.js +47 -0
- package/dist/module/lib/Components/Avatar/Avatar.stories.js.map +1 -1
- package/dist/module/lib/Components/Avatar/Avatar.test.js +23 -1
- package/dist/module/lib/Components/Avatar/Avatar.test.js.map +1 -1
- package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.js +0 -3
- package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.js.map +1 -1
- package/dist/module/lib/Components/Icon/Icon.test.js +1 -1
- package/dist/module/lib/Components/Switch/Switch.js +144 -8
- package/dist/module/lib/Components/Switch/Switch.js.map +1 -1
- package/dist/typescript/src/lib/Components/AmountDisplay/AmountDisplay.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/AmountInput/AmountInput.d.ts +1 -1
- package/dist/typescript/src/lib/Components/AmountInput/AmountInput.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/AmountInput/useAmountInputAnimations/useAmountInputAnimations.d.ts +16 -0
- package/dist/typescript/src/lib/Components/AmountInput/useAmountInputAnimations/useAmountInputAnimations.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.d.ts +18 -0
- package/dist/typescript/src/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Avatar/Avatar.d.ts +1 -1
- package/dist/typescript/src/lib/Components/Avatar/Avatar.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Avatar/types.d.ts +6 -0
- package/dist/typescript/src/lib/Components/Avatar/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/BottomSheet/BottomSheetHeader.d.ts +1 -1
- package/dist/typescript/src/lib/Components/BottomSheet/BottomSheetHeader.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Switch/Switch.d.ts +1 -1
- package/dist/typescript/src/lib/Components/Switch/Switch.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Switch/types.d.ts +2 -1
- package/dist/typescript/src/lib/Components/Switch/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/lib/Components/AmountDisplay/AmountDisplay.test.tsx +92 -0
- package/src/lib/Components/AmountDisplay/AmountDisplay.tsx +37 -15
- package/src/lib/Components/AmountInput/AmountInput.test.tsx +166 -0
- package/src/lib/Components/AmountInput/AmountInput.tsx +55 -110
- package/src/lib/Components/AmountInput/useAmountInputAnimations/useAmountInputAnimations.ts +100 -0
- package/src/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.test.ts +62 -0
- package/src/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.ts +48 -0
- package/src/lib/Components/Avatar/Avatar.figma.tsx +5 -0
- package/src/lib/Components/Avatar/Avatar.mdx +9 -0
- package/src/lib/Components/Avatar/Avatar.stories.tsx +41 -0
- package/src/lib/Components/Avatar/Avatar.test.tsx +31 -1
- package/src/lib/Components/Avatar/Avatar.tsx +17 -4
- package/src/lib/Components/Avatar/types.ts +6 -0
- package/src/lib/Components/BottomSheet/BottomSheetHeader.tsx +0 -4
- package/src/lib/Components/Icon/Icon.test.tsx +1 -1
- package/src/lib/Components/Switch/Switch.tsx +132 -11
- package/src/lib/Components/Switch/types.ts +3 -1
- package/dist/module/lib/Components/Switch/BaseSwitch.js +0 -221
- package/dist/module/lib/Components/Switch/BaseSwitch.js.map +0 -1
- package/dist/typescript/src/lib/Components/Switch/BaseSwitch.d.ts +0 -13
- package/dist/typescript/src/lib/Components/Switch/BaseSwitch.d.ts.map +0 -1
- package/src/lib/Components/Switch/BaseSwitch.tsx +0 -249
|
@@ -164,6 +164,12 @@ const useAnimatedDigitStrip = ({
|
|
|
164
164
|
return { animatedStyle };
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
+
// Horizontal breathing room for the clip box so glyphs that are slightly wider
|
|
168
|
+
// than the measured `targetWidth` are not cut on their left/right edges. RN has
|
|
169
|
+
// no per-axis overflow, so we extend the clip box horizontally while keeping the
|
|
170
|
+
// vertical clip tight to `lineHeight`
|
|
171
|
+
const HORIZONTAL_CLIP_PADDING = 8;
|
|
172
|
+
|
|
167
173
|
const DigitStrip = memo(
|
|
168
174
|
({ value, textStyle, animate, widths }: DigitStripProps) => {
|
|
169
175
|
const targetWidth = widths[value];
|
|
@@ -179,23 +185,39 @@ const DigitStrip = memo(
|
|
|
179
185
|
|
|
180
186
|
return (
|
|
181
187
|
<Animated.View
|
|
182
|
-
style={{
|
|
188
|
+
style={{
|
|
189
|
+
height: lineHeight,
|
|
190
|
+
width: animate ? width : targetWidth,
|
|
191
|
+
}}
|
|
183
192
|
accessibilityValue={{ text: String(value) }}
|
|
184
193
|
>
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
<View
|
|
195
|
+
pointerEvents='none'
|
|
196
|
+
style={{
|
|
197
|
+
position: 'absolute',
|
|
198
|
+
top: 0,
|
|
199
|
+
bottom: 0,
|
|
200
|
+
left: -HORIZONTAL_CLIP_PADDING,
|
|
201
|
+
right: -HORIZONTAL_CLIP_PADDING,
|
|
202
|
+
overflow: 'hidden',
|
|
203
|
+
alignItems: 'center',
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<Animated.View style={[animatedStyle, { alignItems: 'center' }]}>
|
|
207
|
+
{DIGITS.map((d) => (
|
|
208
|
+
<Text
|
|
209
|
+
allowFontScaling={false}
|
|
210
|
+
key={d}
|
|
211
|
+
style={[
|
|
212
|
+
textStyle,
|
|
213
|
+
RuntimeConstants.isAndroid && { height: lineHeight },
|
|
214
|
+
]}
|
|
215
|
+
>
|
|
216
|
+
{d}
|
|
217
|
+
</Text>
|
|
218
|
+
))}
|
|
219
|
+
</Animated.View>
|
|
220
|
+
</View>
|
|
199
221
|
</Animated.View>
|
|
200
222
|
);
|
|
201
223
|
},
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, expect, it, jest } from '@jest/globals';
|
|
2
|
+
import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
|
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/react-native';
|
|
4
|
+
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
|
|
5
|
+
import { AmountInput } from './AmountInput';
|
|
6
|
+
|
|
7
|
+
const hidden = { includeHiddenElements: true } as const;
|
|
8
|
+
|
|
9
|
+
const renderWithProvider = (component: React.ReactElement) =>
|
|
10
|
+
render(
|
|
11
|
+
<ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
|
|
12
|
+
{component}
|
|
13
|
+
</ThemeProvider>,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
describe('AmountInput', () => {
|
|
17
|
+
describe('Rendering', () => {
|
|
18
|
+
it('renders with an empty value', () => {
|
|
19
|
+
renderWithProvider(
|
|
20
|
+
<AmountInput value='' onChangeText={jest.fn()} testID='input' />,
|
|
21
|
+
);
|
|
22
|
+
expect(screen.getByTestId('input')).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('renders the currency text', () => {
|
|
26
|
+
renderWithProvider(
|
|
27
|
+
<AmountInput value='' onChangeText={jest.fn()} currencyText='USD' />,
|
|
28
|
+
);
|
|
29
|
+
expect(screen.getByText('USD', hidden)).toBeTruthy();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('displays 0 when value is empty', () => {
|
|
33
|
+
renderWithProvider(<AmountInput value='' onChangeText={jest.fn()} />);
|
|
34
|
+
expect(screen.getByText('0', hidden)).toBeTruthy();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Interactions', () => {
|
|
39
|
+
it('calls onChangeText with formatted text on change', () => {
|
|
40
|
+
const onChangeText = jest.fn();
|
|
41
|
+
renderWithProvider(
|
|
42
|
+
<AmountInput
|
|
43
|
+
value=''
|
|
44
|
+
onChangeText={onChangeText}
|
|
45
|
+
testID='input'
|
|
46
|
+
thousandsSeparator={false}
|
|
47
|
+
/>,
|
|
48
|
+
);
|
|
49
|
+
fireEvent.changeText(screen.getByTestId('input'), '123.45');
|
|
50
|
+
expect(onChangeText).toHaveBeenCalledWith('123.45');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('formats with thousands separator on user input', () => {
|
|
54
|
+
const onChangeText = jest.fn();
|
|
55
|
+
renderWithProvider(
|
|
56
|
+
<AmountInput value='' onChangeText={onChangeText} testID='input' />,
|
|
57
|
+
);
|
|
58
|
+
fireEvent.changeText(screen.getByTestId('input'), '1000');
|
|
59
|
+
expect(onChangeText).toHaveBeenCalledWith('1 000');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('strips decimal input when allowDecimals is false', () => {
|
|
63
|
+
const onChangeText = jest.fn();
|
|
64
|
+
renderWithProvider(
|
|
65
|
+
<AmountInput
|
|
66
|
+
value=''
|
|
67
|
+
onChangeText={onChangeText}
|
|
68
|
+
allowDecimals={false}
|
|
69
|
+
thousandsSeparator={false}
|
|
70
|
+
testID='input'
|
|
71
|
+
/>,
|
|
72
|
+
);
|
|
73
|
+
fireEvent.changeText(screen.getByTestId('input'), '12.34');
|
|
74
|
+
expect(onChangeText).toHaveBeenCalledWith('1234');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('truncates decimal part with maxDecimalLength on user input', () => {
|
|
78
|
+
const onChangeText = jest.fn();
|
|
79
|
+
renderWithProvider(
|
|
80
|
+
<AmountInput
|
|
81
|
+
value=''
|
|
82
|
+
onChangeText={onChangeText}
|
|
83
|
+
maxDecimalLength={2}
|
|
84
|
+
thousandsSeparator={false}
|
|
85
|
+
testID='input'
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
fireEvent.changeText(screen.getByTestId('input'), '1.2345');
|
|
89
|
+
expect(onChangeText).toHaveBeenCalledWith('1.23');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('programmatic value formatting', () => {
|
|
94
|
+
it('formats a raw unformatted string on initial render', () => {
|
|
95
|
+
renderWithProvider(
|
|
96
|
+
<AmountInput value='2433.123456789' onChangeText={jest.fn()} />,
|
|
97
|
+
);
|
|
98
|
+
expect(screen.getByText('2 433.123456789', hidden)).toBeTruthy();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('formats a number value on initial render', () => {
|
|
102
|
+
renderWithProvider(
|
|
103
|
+
<AmountInput value={2433.123456789} onChangeText={jest.fn()} />,
|
|
104
|
+
);
|
|
105
|
+
expect(screen.getByText('2 433.123456789', hidden)).toBeTruthy();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('adds thousands grouping for a large number value', () => {
|
|
109
|
+
renderWithProvider(
|
|
110
|
+
<AmountInput value={1000000} onChangeText={jest.fn()} />,
|
|
111
|
+
);
|
|
112
|
+
expect(screen.getByText('1 000 000', hidden)).toBeTruthy();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('applies maxDecimalLength to a programmatically set value', () => {
|
|
116
|
+
renderWithProvider(
|
|
117
|
+
<AmountInput
|
|
118
|
+
value='2433.123456789'
|
|
119
|
+
maxDecimalLength={2}
|
|
120
|
+
onChangeText={jest.fn()}
|
|
121
|
+
/>,
|
|
122
|
+
);
|
|
123
|
+
expect(screen.getByText('2 433.12', hidden)).toBeTruthy();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('applies maxIntegerLength to a programmatically set value', () => {
|
|
127
|
+
renderWithProvider(
|
|
128
|
+
<AmountInput
|
|
129
|
+
value='1234567890'
|
|
130
|
+
maxIntegerLength={3}
|
|
131
|
+
allowDecimals={false}
|
|
132
|
+
thousandsSeparator={false}
|
|
133
|
+
onChangeText={jest.fn()}
|
|
134
|
+
/>,
|
|
135
|
+
);
|
|
136
|
+
expect(screen.getByText('123', hidden)).toBeTruthy();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('omits thousands separator when thousandsSeparator is false', () => {
|
|
140
|
+
renderWithProvider(
|
|
141
|
+
<AmountInput
|
|
142
|
+
value='2433.12'
|
|
143
|
+
thousandsSeparator={false}
|
|
144
|
+
onChangeText={jest.fn()}
|
|
145
|
+
/>,
|
|
146
|
+
);
|
|
147
|
+
expect(screen.getByText('2433.12', hidden)).toBeTruthy();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('formats consistently with what typing the same digits produces', () => {
|
|
151
|
+
const onChangeText = jest.fn();
|
|
152
|
+
renderWithProvider(
|
|
153
|
+
<AmountInput
|
|
154
|
+
value='2433.123456789'
|
|
155
|
+
onChangeText={onChangeText}
|
|
156
|
+
testID='input'
|
|
157
|
+
/>,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(screen.getByText('2 433.123456789', hidden)).toBeTruthy();
|
|
161
|
+
|
|
162
|
+
fireEvent.changeText(screen.getByTestId('input'), '2433.123456789');
|
|
163
|
+
expect(onChangeText).toHaveBeenCalledWith('2 433.123456789');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -1,25 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
useDisabledContext,
|
|
5
|
-
} from '@ledgerhq/lumen-utils-shared';
|
|
6
|
-
import { useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
1
|
+
import { useDisabledContext } from '@ledgerhq/lumen-utils-shared';
|
|
2
|
+
import { useImperativeHandle, useRef, useState } from 'react';
|
|
3
|
+
import type { StyleProp, TextStyle } from 'react-native';
|
|
7
4
|
import { Pressable, StyleSheet, TextInput, View } from 'react-native';
|
|
8
|
-
import Animated
|
|
9
|
-
|
|
10
|
-
useAnimatedStyle,
|
|
11
|
-
useSharedValue,
|
|
12
|
-
withRepeat,
|
|
13
|
-
withSequence,
|
|
14
|
-
withTiming,
|
|
15
|
-
} from 'react-native-reanimated';
|
|
16
|
-
import { useStyleSheet, useTheme } from '../../../styles';
|
|
5
|
+
import Animated from 'react-native-reanimated';
|
|
6
|
+
import { useStyleSheet } from '../../../styles';
|
|
17
7
|
import { Box } from '../Utility';
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
import type {
|
|
9
|
+
AmountInputAlign,
|
|
10
|
+
AmountInputProps,
|
|
11
|
+
AmountInputSize,
|
|
22
12
|
} from './types';
|
|
13
|
+
import { useAmountInputAnimations } from './useAmountInputAnimations/useAmountInputAnimations';
|
|
14
|
+
import { useAmountInputFormatting } from './useAmountInputFormatting/useAmountInputFormatting';
|
|
15
|
+
|
|
16
|
+
type CurrencyProps = {
|
|
17
|
+
style: StyleProp<TextStyle>;
|
|
18
|
+
children: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const Currency = ({ style, children }: CurrencyProps) => (
|
|
22
|
+
<Animated.Text style={style} allowFontScaling={false}>
|
|
23
|
+
{children}
|
|
24
|
+
</Animated.Text>
|
|
25
|
+
);
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* AmountInput component for handling numeric input with currency display.
|
|
@@ -46,109 +49,43 @@ export const AmountInput = ({
|
|
|
46
49
|
...props
|
|
47
50
|
}: AmountInputProps) => {
|
|
48
51
|
const inputRef = useRef<TextInput>(null);
|
|
49
|
-
const inputValue = String(value);
|
|
50
52
|
const [isFocused, setIsFocused] = useState(false);
|
|
53
|
+
|
|
51
54
|
const disabled = useDisabledContext({
|
|
52
55
|
consumerName: 'AmountInput',
|
|
53
56
|
mergeWith: { disabled: disabledProp },
|
|
54
57
|
});
|
|
55
58
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
const { formattedValue, handleChangeText } = useAmountInputFormatting({
|
|
60
|
+
value,
|
|
61
|
+
onChangeText,
|
|
62
|
+
formatOptions: {
|
|
63
|
+
allowDecimals,
|
|
64
|
+
thousandsSeparator,
|
|
65
|
+
maxIntegerLength,
|
|
66
|
+
maxDecimalLength,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
const { animatedInputStyle, animatedCurrencyStyle, animatedCaretStyle } =
|
|
71
|
+
useAmountInputAnimations({
|
|
72
|
+
formattedValue,
|
|
73
|
+
size,
|
|
74
|
+
isFocused,
|
|
75
|
+
disabled,
|
|
76
|
+
});
|
|
61
77
|
|
|
62
|
-
const { theme } = useTheme();
|
|
63
78
|
const styles = useStyles({
|
|
64
79
|
size,
|
|
65
80
|
align,
|
|
66
|
-
hasValue: !!
|
|
81
|
+
hasValue: !!formattedValue,
|
|
67
82
|
isEditable: !disabled,
|
|
68
83
|
isInvalid,
|
|
69
84
|
});
|
|
70
|
-
const caretFixedHeight = size === 'sm' ? theme.sizes.s28 : 0;
|
|
71
|
-
|
|
72
|
-
const animatedInputStyle = useAnimatedStyle(
|
|
73
|
-
() => ({
|
|
74
|
-
transform: [{ translateX: translateX.value }],
|
|
75
|
-
fontSize: animatedFontSize.value,
|
|
76
|
-
letterSpacing: 0,
|
|
77
|
-
}),
|
|
78
|
-
[translateX, animatedFontSize],
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
const animatedCurrencyStyle = useAnimatedStyle(
|
|
82
|
-
() => ({
|
|
83
|
-
fontSize: animatedFontSize.value,
|
|
84
|
-
letterSpacing: 0,
|
|
85
|
-
}),
|
|
86
|
-
[animatedFontSize],
|
|
87
|
-
);
|
|
88
85
|
|
|
89
|
-
|
|
90
|
-
() => ({
|
|
91
|
-
opacity: caretOpacity.value,
|
|
92
|
-
height: size === 'sm' ? caretFixedHeight : animatedFontSize.value,
|
|
93
|
-
}),
|
|
94
|
-
[caretOpacity, animatedFontSize, size, caretFixedHeight],
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
const newSize = getFontSize(inputValue, size);
|
|
99
|
-
|
|
100
|
-
translateX.value = withSequence(
|
|
101
|
-
withTiming(4, { duration: 0 }),
|
|
102
|
-
withTiming(0, {
|
|
103
|
-
duration: 250,
|
|
104
|
-
easing: Easing.bezier(0.4, 0, 0.2, 1),
|
|
105
|
-
}),
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
animatedFontSize.value = withTiming(newSize, {
|
|
109
|
-
duration: 250,
|
|
110
|
-
easing: Easing.bezier(0.4, 0, 0.2, 1),
|
|
111
|
-
});
|
|
112
|
-
}, [inputValue, size, animatedFontSize, translateX]);
|
|
113
|
-
|
|
114
|
-
useEffect(() => {
|
|
115
|
-
if (isFocused && !disabled) {
|
|
116
|
-
caretOpacity.value = withRepeat(
|
|
117
|
-
withSequence(
|
|
118
|
-
withTiming(1, { duration: 150, easing: Easing.ease }),
|
|
119
|
-
withTiming(1, { duration: 500 }),
|
|
120
|
-
withTiming(0, { duration: 150, easing: Easing.ease }),
|
|
121
|
-
withTiming(0, { duration: 500 }),
|
|
122
|
-
),
|
|
123
|
-
-1,
|
|
124
|
-
false,
|
|
125
|
-
);
|
|
126
|
-
} else {
|
|
127
|
-
caretOpacity.value = 0;
|
|
128
|
-
}
|
|
129
|
-
}, [isFocused, disabled, caretOpacity]);
|
|
130
|
-
|
|
131
|
-
const handleChangeText = (text: string) => {
|
|
132
|
-
const formatted = textFormatter(text, {
|
|
133
|
-
allowDecimals,
|
|
134
|
-
thousandsSeparator,
|
|
135
|
-
maxIntegerLength,
|
|
136
|
-
maxDecimalLength,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
onChangeText(formatted);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const CurrencyText = currencyText ? (
|
|
143
|
-
<Animated.Text
|
|
144
|
-
style={[styles.currency, animatedCurrencyStyle]}
|
|
145
|
-
allowFontScaling={false}
|
|
146
|
-
>
|
|
147
|
-
{currencyText}
|
|
148
|
-
</Animated.Text>
|
|
149
|
-
) : null;
|
|
86
|
+
useImperativeHandle(ref, () => inputRef.current as TextInput, []);
|
|
150
87
|
|
|
151
|
-
const handlePress = () => {
|
|
88
|
+
const handlePress = (): void => {
|
|
152
89
|
if (!disabled) {
|
|
153
90
|
inputRef.current?.focus();
|
|
154
91
|
}
|
|
@@ -161,7 +98,7 @@ export const AmountInput = ({
|
|
|
161
98
|
ref={inputRef}
|
|
162
99
|
keyboardType='decimal-pad'
|
|
163
100
|
editable={editable !== false && !disabled}
|
|
164
|
-
value={
|
|
101
|
+
value={formattedValue}
|
|
165
102
|
onChangeText={handleChangeText}
|
|
166
103
|
onFocus={(e) => {
|
|
167
104
|
setIsFocused(true);
|
|
@@ -180,20 +117,28 @@ export const AmountInput = ({
|
|
|
180
117
|
style={styles.pressable}
|
|
181
118
|
accessibilityLabel={props.accessibilityLabel || 'Amount input'}
|
|
182
119
|
>
|
|
183
|
-
{currencyPosition === 'left' &&
|
|
120
|
+
{currencyText && currencyPosition === 'left' && (
|
|
121
|
+
<Currency style={[styles.currency, animatedCurrencyStyle]}>
|
|
122
|
+
{currencyText}
|
|
123
|
+
</Currency>
|
|
124
|
+
)}
|
|
184
125
|
|
|
185
126
|
{/** display text that mirrors the hidden input's value */}
|
|
186
127
|
<Animated.Text
|
|
187
128
|
style={[styles.displayText, animatedInputStyle, style]}
|
|
188
129
|
allowFontScaling={false}
|
|
189
130
|
>
|
|
190
|
-
{
|
|
131
|
+
{formattedValue || '0'}
|
|
191
132
|
</Animated.Text>
|
|
192
133
|
|
|
193
134
|
{/** custom caret */}
|
|
194
135
|
<Animated.View style={[styles.caret, animatedCaretStyle]} />
|
|
195
136
|
|
|
196
|
-
{currencyPosition === 'right' &&
|
|
137
|
+
{currencyText && currencyPosition === 'right' && (
|
|
138
|
+
<Currency style={[styles.currency, animatedCurrencyStyle]}>
|
|
139
|
+
{currencyText}
|
|
140
|
+
</Currency>
|
|
141
|
+
)}
|
|
197
142
|
</Pressable>
|
|
198
143
|
</View>
|
|
199
144
|
</Box>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { getFontSize } from '@ledgerhq/lumen-utils-shared';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
Easing,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withRepeat,
|
|
8
|
+
withSequence,
|
|
9
|
+
withTiming,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
import { useTheme } from '../../../../styles';
|
|
12
|
+
import type { AmountInputSize } from '../types';
|
|
13
|
+
|
|
14
|
+
type UseAmountInputAnimationsArgs = {
|
|
15
|
+
formattedValue: string;
|
|
16
|
+
size: AmountInputSize;
|
|
17
|
+
isFocused: boolean;
|
|
18
|
+
disabled: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type UseAmountInputAnimationsReturn = {
|
|
22
|
+
animatedInputStyle: ReturnType<typeof useAnimatedStyle>;
|
|
23
|
+
animatedCurrencyStyle: ReturnType<typeof useAnimatedStyle>;
|
|
24
|
+
animatedCaretStyle: ReturnType<typeof useAnimatedStyle>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const useAmountInputAnimations = ({
|
|
28
|
+
formattedValue,
|
|
29
|
+
size,
|
|
30
|
+
isFocused,
|
|
31
|
+
disabled,
|
|
32
|
+
}: UseAmountInputAnimationsArgs): UseAmountInputAnimationsReturn => {
|
|
33
|
+
const { theme } = useTheme();
|
|
34
|
+
const caretFixedHeight = size === 'sm' ? theme.sizes.s28 : 0;
|
|
35
|
+
|
|
36
|
+
const translateX = useSharedValue(0);
|
|
37
|
+
const animatedFontSize = useSharedValue(getFontSize(formattedValue, size));
|
|
38
|
+
const caretOpacity = useSharedValue(0);
|
|
39
|
+
|
|
40
|
+
const animatedInputStyle = useAnimatedStyle(
|
|
41
|
+
() => ({
|
|
42
|
+
transform: [{ translateX: translateX.value }],
|
|
43
|
+
fontSize: animatedFontSize.value,
|
|
44
|
+
letterSpacing: 0,
|
|
45
|
+
}),
|
|
46
|
+
[translateX, animatedFontSize],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const animatedCurrencyStyle = useAnimatedStyle(
|
|
50
|
+
() => ({
|
|
51
|
+
fontSize: animatedFontSize.value,
|
|
52
|
+
letterSpacing: 0,
|
|
53
|
+
}),
|
|
54
|
+
[animatedFontSize],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const animatedCaretStyle = useAnimatedStyle(
|
|
58
|
+
() => ({
|
|
59
|
+
opacity: caretOpacity.value,
|
|
60
|
+
height: size === 'sm' ? caretFixedHeight : animatedFontSize.value,
|
|
61
|
+
}),
|
|
62
|
+
[caretOpacity, animatedFontSize, size, caretFixedHeight],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const newSize = getFontSize(formattedValue, size);
|
|
67
|
+
|
|
68
|
+
translateX.value = withSequence(
|
|
69
|
+
withTiming(4, { duration: 0 }),
|
|
70
|
+
withTiming(0, {
|
|
71
|
+
duration: 250,
|
|
72
|
+
easing: Easing.bezier(0.4, 0, 0.2, 1),
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
animatedFontSize.value = withTiming(newSize, {
|
|
77
|
+
duration: 250,
|
|
78
|
+
easing: Easing.bezier(0.4, 0, 0.2, 1),
|
|
79
|
+
});
|
|
80
|
+
}, [formattedValue, size, animatedFontSize, translateX]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (isFocused && !disabled) {
|
|
84
|
+
caretOpacity.value = withRepeat(
|
|
85
|
+
withSequence(
|
|
86
|
+
withTiming(1, { duration: 150, easing: Easing.ease }),
|
|
87
|
+
withTiming(1, { duration: 500 }),
|
|
88
|
+
withTiming(0, { duration: 150, easing: Easing.ease }),
|
|
89
|
+
withTiming(0, { duration: 500 }),
|
|
90
|
+
),
|
|
91
|
+
-1,
|
|
92
|
+
false,
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
caretOpacity.value = 0;
|
|
96
|
+
}
|
|
97
|
+
}, [isFocused, disabled, caretOpacity]);
|
|
98
|
+
|
|
99
|
+
return { animatedInputStyle, animatedCurrencyStyle, animatedCaretStyle };
|
|
100
|
+
};
|
package/src/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.test.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, jest } from '@jest/globals';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react-native';
|
|
3
|
+
import { useAmountInputFormatting } from './useAmountInputFormatting';
|
|
4
|
+
|
|
5
|
+
const defaultFormatOptions = {
|
|
6
|
+
allowDecimals: true,
|
|
7
|
+
thousandsSeparator: true,
|
|
8
|
+
maxIntegerLength: 9,
|
|
9
|
+
maxDecimalLength: 9,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('useAmountInputFormatting', () => {
|
|
13
|
+
it('formats the value prop', () => {
|
|
14
|
+
const { result } = renderHook(() =>
|
|
15
|
+
useAmountInputFormatting({
|
|
16
|
+
value: '1000',
|
|
17
|
+
onChangeText: jest.fn(),
|
|
18
|
+
formatOptions: defaultFormatOptions,
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
expect(result.current.formattedValue).toBe('1 000');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('updates formattedValue when the value prop changes', () => {
|
|
26
|
+
const { result, rerender } = renderHook<
|
|
27
|
+
ReturnType<typeof useAmountInputFormatting>,
|
|
28
|
+
{ value: string }
|
|
29
|
+
>(
|
|
30
|
+
({ value }) =>
|
|
31
|
+
useAmountInputFormatting({
|
|
32
|
+
value,
|
|
33
|
+
onChangeText: jest.fn(),
|
|
34
|
+
formatOptions: defaultFormatOptions,
|
|
35
|
+
}),
|
|
36
|
+
{ initialProps: { value: '100' } },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(result.current.formattedValue).toBe('100');
|
|
40
|
+
|
|
41
|
+
rerender({ value: '2000' });
|
|
42
|
+
|
|
43
|
+
expect(result.current.formattedValue).toBe('2 000');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('formats user input and calls onChangeText with the cleaned value', () => {
|
|
47
|
+
const onChangeText = jest.fn();
|
|
48
|
+
const { result } = renderHook(() =>
|
|
49
|
+
useAmountInputFormatting({
|
|
50
|
+
value: '',
|
|
51
|
+
onChangeText,
|
|
52
|
+
formatOptions: defaultFormatOptions,
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
act(() => {
|
|
57
|
+
result.current.handleChangeText('1000');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(onChangeText).toHaveBeenCalledWith('1 000');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { textFormatter } from '@ledgerhq/lumen-utils-shared';
|
|
2
|
+
import { useCallback, useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
type FormatOptions = {
|
|
5
|
+
allowDecimals: boolean;
|
|
6
|
+
thousandsSeparator: boolean;
|
|
7
|
+
maxIntegerLength: number;
|
|
8
|
+
maxDecimalLength: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type UseAmountInputFormattingArgs = {
|
|
12
|
+
value: string | number;
|
|
13
|
+
onChangeText: (text: string) => void;
|
|
14
|
+
formatOptions: FormatOptions;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type UseAmountInputFormattingReturn = {
|
|
18
|
+
formattedValue: string;
|
|
19
|
+
handleChangeText: (text: string) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const useAmountInputFormatting = ({
|
|
23
|
+
value,
|
|
24
|
+
onChangeText,
|
|
25
|
+
formatOptions,
|
|
26
|
+
}: UseAmountInputFormattingArgs): UseAmountInputFormattingReturn => {
|
|
27
|
+
const format = useCallback(
|
|
28
|
+
(v: string | number): string => textFormatter(String(v), formatOptions),
|
|
29
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
30
|
+
[
|
|
31
|
+
formatOptions.allowDecimals,
|
|
32
|
+
formatOptions.thousandsSeparator,
|
|
33
|
+
formatOptions.maxIntegerLength,
|
|
34
|
+
formatOptions.maxDecimalLength,
|
|
35
|
+
],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const formattedValue = useMemo(() => format(value), [value, format]);
|
|
39
|
+
|
|
40
|
+
const handleChangeText = useCallback(
|
|
41
|
+
(text: string): void => {
|
|
42
|
+
onChangeText(format(text));
|
|
43
|
+
},
|
|
44
|
+
[format, onChangeText],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return { formattedValue, handleChangeText };
|
|
48
|
+
};
|
|
@@ -7,6 +7,10 @@ figma.connect(
|
|
|
7
7
|
{
|
|
8
8
|
imports: ["import { Avatar } from '@ledgerhq/lumen-ui-rnative'"],
|
|
9
9
|
props: {
|
|
10
|
+
appearance: figma.enum('appearance', {
|
|
11
|
+
gray: 'gray',
|
|
12
|
+
transparent: 'transparent',
|
|
13
|
+
}),
|
|
10
14
|
size: figma.enum('size', {
|
|
11
15
|
sm: 'sm',
|
|
12
16
|
md: 'md',
|
|
@@ -21,6 +25,7 @@ figma.connect(
|
|
|
21
25
|
example: (props) => (
|
|
22
26
|
<Avatar
|
|
23
27
|
src='https://example-image.com'
|
|
28
|
+
appearance={props.appearance}
|
|
24
29
|
size={props.size}
|
|
25
30
|
alt="John Doe's Avatar"
|
|
26
31
|
showNotification={props.showNotification}
|
|
@@ -40,6 +40,15 @@ Avatars come in four different sizes:
|
|
|
40
40
|
|
|
41
41
|
<Canvas of={AvatarStories.SizeShowcase} />
|
|
42
42
|
|
|
43
|
+
### Appearance
|
|
44
|
+
|
|
45
|
+
The `appearance` prop controls the background color of the avatar container.
|
|
46
|
+
|
|
47
|
+
- **`gray`**: Uses a muted gray background
|
|
48
|
+
- **`transparent`**: Uses a semi-transparent muted background — default
|
|
49
|
+
|
|
50
|
+
<Canvas of={AvatarStories.AppearanceShowcase} />
|
|
51
|
+
|
|
43
52
|
### States
|
|
44
53
|
|
|
45
54
|
The Avatar component handles two primary states automatically:
|