|
1 |
| -import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'; |
2 |
| -import { LayoutChangeEvent, Pressable, TextInput, TextInputProps } from 'react-native'; |
| 1 | +import { forwardRef } from 'react'; |
| 2 | +import { TextInput } from 'react-native'; |
3 | 3 |
|
4 |
| -import { useField } from '@suite-native/forms'; |
5 |
| -import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; |
| 4 | +import type { UseFormReturn } from '@suite-native/forms'; |
6 | 5 |
|
7 | 6 | import { useBuyFormContext } from '../../hooks/buy/useBuyFormContext';
|
8 |
| -import { truncateDecimals } from '../../utils/general/amountUtils'; |
| 7 | +import { FocusableFormValues } from '../../types/general'; |
| 8 | +import { AmountInput, AmountInputProps } from '../general/AmountInput'; |
9 | 9 |
|
10 |
| -export type TradingAmountInputProps = { |
11 |
| - name: 'fiatValue' | 'cryptoValue'; |
12 |
| - inputTransformer: (value: string) => string; |
13 |
| - maxDecimals?: number; |
14 |
| -} & Omit< |
15 |
| - TextInputProps, |
16 |
| - 'value' | 'style' | 'onBlur' | 'onFocus' | 'onChangeText' | 'onLayout' | 'onContentSizeChange' |
17 |
| ->; |
| 10 | +type InputKey = 'fiatValue' | 'cryptoValue'; |
18 | 11 |
|
19 |
| -const MAX_FONT_SIZE = 34; |
20 |
| -const MIN_FONT_SIZE = Math.ceil(MAX_FONT_SIZE / 2); |
21 |
| -const FONT_TO_LINE_HEIGHT_RATIO = 1.235; |
22 |
| -const FONT_SIZE_SHRINK_THRESHOLD = 20; |
23 |
| -const FONT_SIZE_GROW_HYSTERESIS = 20; |
| 12 | +export type BuyAmountInputProps = Omit<AmountInputProps<InputKey>, 'form'>; |
24 | 13 |
|
25 |
| -export const MIN_INPUT_WIDTH = 70; |
26 |
| -export const MAX_INPUT_HEIGHT = Math.floor(MAX_FONT_SIZE * FONT_TO_LINE_HEIGHT_RATIO); |
| 14 | +export { MAX_INPUT_HEIGHT, MIN_INPUT_WIDTH } from '../general/AmountInput'; |
27 | 15 |
|
28 |
| -const boxStyle = prepareNativeStyle(() => ({ |
29 |
| - flex: 1, |
30 |
| - alignItems: 'flex-end', |
31 |
| - paddingLeft: 0, |
32 |
| - marginLeft: 0, |
33 |
| - overflow: 'visible', |
34 |
| -})); |
| 16 | +export const BuyAmountInput = forwardRef<TextInput, BuyAmountInputProps>((props, ref) => { |
| 17 | + const form = useBuyFormContext() as unknown as UseFormReturn<FocusableFormValues<string>>; |
35 | 18 |
|
36 |
| -const inputStyle = prepareNativeStyle<{ hasError: boolean; fontSize: number }>( |
37 |
| - ({ colors, typography }, { hasError, fontSize }) => ({ |
38 |
| - ...typography.body, |
39 |
| - color: hasError ? colors.textAlertRed : colors.textDefault, |
40 |
| - textAlign: 'right', |
41 |
| - fontSize, |
42 |
| - lineHeight: Math.floor(fontSize * FONT_TO_LINE_HEIGHT_RATIO), |
43 |
| - minWidth: MIN_INPUT_WIDTH, |
44 |
| - }), |
45 |
| -); |
46 |
| - |
47 |
| -const useInputLayoutControls = () => { |
48 |
| - const [availableWidth, setAvailableWidth] = useState( |
49 |
| - MIN_INPUT_WIDTH + FONT_SIZE_SHRINK_THRESHOLD, |
50 |
| - ); |
51 |
| - const [fontSize, setFontSize] = useState(MAX_FONT_SIZE); |
52 |
| - |
53 |
| - const handleAvailableWith = useCallback(({ nativeEvent }: LayoutChangeEvent) => { |
54 |
| - const { width } = nativeEvent.layout; |
55 |
| - setAvailableWidth(width); |
56 |
| - }, []); |
57 |
| - |
58 |
| - const handleFontSizeOnContentChange = useCallback( |
59 |
| - ({ nativeEvent }: LayoutChangeEvent) => { |
60 |
| - const contentWidth = nativeEvent.layout.width; |
61 |
| - |
62 |
| - if (contentWidth === 0 || availableWidth === 0) { |
63 |
| - setFontSize(MAX_FONT_SIZE); |
64 |
| - |
65 |
| - return; |
66 |
| - } |
67 |
| - |
68 |
| - const shrinkThreshold = availableWidth - FONT_SIZE_SHRINK_THRESHOLD; |
69 |
| - if (contentWidth > shrinkThreshold) { |
70 |
| - const newFontSize = Math.max( |
71 |
| - Math.floor((shrinkThreshold / contentWidth) * fontSize), |
72 |
| - MIN_FONT_SIZE, |
73 |
| - ); |
74 |
| - setFontSize(newFontSize); |
75 |
| - } |
76 |
| - |
77 |
| - const growThreshold = shrinkThreshold - FONT_SIZE_GROW_HYSTERESIS; |
78 |
| - if (contentWidth < growThreshold) { |
79 |
| - const newFontSize = Math.min( |
80 |
| - Math.floor((shrinkThreshold / contentWidth) * fontSize), |
81 |
| - MAX_FONT_SIZE, |
82 |
| - ); |
83 |
| - setFontSize(newFontSize); |
84 |
| - } |
85 |
| - }, |
86 |
| - [availableWidth, fontSize], |
87 |
| - ); |
88 |
| - |
89 |
| - return { |
90 |
| - fontSize, |
91 |
| - onBoxLayout: handleAvailableWith, |
92 |
| - onInputLayout: handleFontSizeOnContentChange, |
93 |
| - }; |
94 |
| -}; |
95 |
| - |
96 |
| -const useInputFormControls = ( |
97 |
| - name: 'fiatValue' | 'cryptoValue', |
98 |
| - inputTransformer: (value: string) => string, |
99 |
| - maxLength: number | undefined, |
100 |
| - maxDecimals: number | undefined, |
101 |
| -) => { |
102 |
| - const { getValues, setValue } = useBuyFormContext(); |
103 |
| - // do not use `value` from `useField` here, because it does not work properly with `undefined` |
104 |
| - const value = getValues(name); |
105 |
| - const { onChange, onBlur, hasError } = useField({ name }); |
106 |
| - |
107 |
| - const setFocusedValue = useCallback(() => { |
108 |
| - setValue('focusedValue', name); |
109 |
| - }, [name, setValue]); |
110 |
| - |
111 |
| - const handleTextChange = useCallback( |
112 |
| - (text: string) => { |
113 |
| - let transformedText = inputTransformer(text); |
114 |
| - transformedText = truncateDecimals(transformedText, maxDecimals); |
115 |
| - transformedText = transformedText.slice(0, maxLength); |
116 |
| - |
117 |
| - return onChange(transformedText === '' ? undefined : transformedText); |
118 |
| - }, |
119 |
| - [maxLength, maxDecimals, inputTransformer, onChange], |
120 |
| - ); |
121 |
| - |
122 |
| - const clearFocusedValueAndBlur = useCallback(() => { |
123 |
| - onBlur(); |
124 |
| - setValue('focusedValue', undefined); |
125 |
| - }, [onBlur, setValue]); |
126 |
| - |
127 |
| - return { |
128 |
| - value, |
129 |
| - hasError, |
130 |
| - onFocus: setFocusedValue, |
131 |
| - onChangeText: handleTextChange, |
132 |
| - onBlur: clearFocusedValueAndBlur, |
133 |
| - }; |
134 |
| -}; |
135 |
| - |
136 |
| -export const BuyAmountInput = forwardRef<TextInput, TradingAmountInputProps>( |
137 |
| - ({ name, inputTransformer, maxLength, maxDecimals, onPress, ...inputProps }, ref) => { |
138 |
| - const innerRef = useRef<TextInput>(null); |
139 |
| - useImperativeHandle(ref, () => innerRef.current!, []); |
140 |
| - |
141 |
| - const { applyStyle, utils } = useNativeStyles(); |
142 |
| - const { fontSize, onBoxLayout, onInputLayout } = useInputLayoutControls(); |
143 |
| - const { value, hasError, onFocus, onChangeText, onBlur } = useInputFormControls( |
144 |
| - name, |
145 |
| - inputTransformer, |
146 |
| - maxLength, |
147 |
| - maxDecimals, |
148 |
| - ); |
149 |
| - |
150 |
| - const focusInputCallback = useCallback(() => { |
151 |
| - innerRef.current?.focus(); |
152 |
| - }, [innerRef]); |
153 |
| - const wrapperOnPress = onPress ?? focusInputCallback; |
154 |
| - |
155 |
| - // Note: it would be nice to use `onContentSizeChange` instead of `onLayout` on `<Pressable />` once this bug is fixed https://github.com/facebook/react-native/issues/29702. |
156 |
| - // It would also allow us to remove `innerRef` and `useImperativeHandle` logic. |
157 |
| - return ( |
158 |
| - <Pressable |
159 |
| - style={applyStyle(boxStyle)} |
160 |
| - onLayout={onBoxLayout} |
161 |
| - testID="@trading/amountInput/wrapper" |
162 |
| - onPress={wrapperOnPress} |
163 |
| - > |
164 |
| - <TextInput |
165 |
| - ref={innerRef} |
166 |
| - style={applyStyle(inputStyle, { hasError, fontSize })} |
167 |
| - keyboardType="decimal-pad" |
168 |
| - inputMode="decimal" |
169 |
| - placeholder="0.0" |
170 |
| - placeholderTextColor={utils.colors.textDisabled} |
171 |
| - value={value ?? ''} |
172 |
| - maxLength={maxLength} |
173 |
| - onChangeText={onChangeText} |
174 |
| - onFocus={onFocus} |
175 |
| - onBlur={onBlur} |
176 |
| - onLayout={onInputLayout} |
177 |
| - onPress={onPress} |
178 |
| - {...inputProps} |
179 |
| - /> |
180 |
| - </Pressable> |
181 |
| - ); |
182 |
| - }, |
183 |
| -); |
| 19 | + return <AmountInput ref={ref} form={form} {...props} />; |
| 20 | +}); |
0 commit comments