@momo-kits/foundation 0.169.9-beta.1 → 0.169.9

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.
Files changed (63) hide show
  1. package/Application/BottomSheet.hooks.ts +27 -17
  2. package/Application/BottomSheet.tsx +35 -27
  3. package/Application/BottomTab/ActiveIndicator.tsx +5 -1
  4. package/Application/BottomTab/BottomTabBar.styles.ts +18 -1
  5. package/Application/BottomTab/BottomTabBar.tsx +14 -16
  6. package/Application/BottomTab/CurvedContainer.tsx +8 -2
  7. package/Application/BottomTab/CustomBottomTabItem.tsx +2 -2
  8. package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
  9. package/Application/BottomTab/DuoTabBar.tsx +15 -3
  10. package/Application/BottomTab/TabScreen.tsx +11 -10
  11. package/Application/BottomTab/index.tsx +5 -5
  12. package/Application/BottomTab/metrics.ts +23 -3
  13. package/Application/BottomTab/usePressLift.ts +3 -3
  14. package/Application/Components/HeaderAnimated.tsx +13 -38
  15. package/Application/Components/HeaderBackground.tsx +3 -16
  16. package/Application/Components/HeaderExtendHeader.tsx +28 -41
  17. package/Application/Components/HeaderLeft.tsx +18 -4
  18. package/Application/Components/HeaderRight.styles.ts +13 -15
  19. package/Application/Components/HeaderTitle.shared.ts +11 -10
  20. package/Application/Components/HeaderToolkitAction.tsx +54 -21
  21. package/Application/Components/NavigationButton.tsx +6 -9
  22. package/Application/Components/SearchHeader.tsx +7 -20
  23. package/Application/Components/useToolkitActions.tsx +22 -21
  24. package/Application/ModalScreen.tsx +19 -15
  25. package/Application/StackScreen.tsx +13 -18
  26. package/Application/index.ts +1 -11
  27. package/Application/modalLayout.ts +1 -0
  28. package/Application/navigationOptions.tsx +15 -6
  29. package/Application/types.header.ts +2 -7
  30. package/Application/types.tabs.ts +0 -1
  31. package/Application/types.ts +0 -7
  32. package/Context/index.ts +3 -1
  33. package/Image/index.tsx +6 -6
  34. package/Input/ErrorView.tsx +59 -0
  35. package/Input/FloatingView.tsx +101 -0
  36. package/Input/Input.tsx +40 -37
  37. package/Input/InputDropDown.tsx +13 -10
  38. package/Input/InputMoney.tsx +17 -17
  39. package/Input/InputMoneyView.tsx +12 -12
  40. package/Input/InputOTP.tsx +46 -34
  41. package/Input/InputPhoneNumber.tsx +48 -43
  42. package/Input/InputSearch.tsx +26 -39
  43. package/Input/InputTextArea.tsx +29 -23
  44. package/Input/RenderTrailing.tsx +79 -0
  45. package/Input/constants.ts +8 -0
  46. package/Input/index.tsx +1 -7
  47. package/Input/utils.ts +126 -7
  48. package/Layout/FloatingButton.tsx +38 -24
  49. package/Layout/Screen.tsx +0 -3
  50. package/Layout/layoutMetrics.ts +43 -34
  51. package/Layout/screenHeader.tsx +52 -55
  52. package/Layout/screenTypes.ts +4 -13
  53. package/Pagination/PaginationScroll.tsx +5 -3
  54. package/Pagination/types.ts +3 -5
  55. package/Popup/PopupNotify.tsx +17 -14
  56. package/Popup/PopupPromotion.tsx +5 -4
  57. package/Popup/types.ts +2 -4
  58. package/Popup/utils.ts +7 -4
  59. package/Skeleton/index.tsx +15 -12
  60. package/package.json +1 -1
  61. package/Application/headerLayout.ts +0 -165
  62. package/Input/common.tsx +0 -257
  63. package/Input/moneyUtils.ts +0 -23
@@ -0,0 +1,101 @@
1
+ import React, { type FC, useContext } from 'react';
2
+ import {
3
+ type GestureResponderEvent,
4
+ TouchableOpacity,
5
+ View,
6
+ type ViewStyle,
7
+ } from 'react-native';
8
+ import { ApplicationContext } from '../Context';
9
+ import { Icon } from '../Icon';
10
+ import { Text, useScaleSize } from '../Text';
11
+ import { PRESSED_OPACITY } from './constants';
12
+ import styles from './styles';
13
+
14
+ const LABEL_OFFSET = 10;
15
+ const ICON_SIZE = 16;
16
+
17
+ type FloatingViewProps = {
18
+ floatingValue?: string;
19
+ floatingIconColor?: string;
20
+ componentId?: string;
21
+ disabled?: boolean;
22
+ floatingIcon?: string;
23
+ required?: boolean;
24
+ style?: ViewStyle;
25
+ onPress?: (e: GestureResponderEvent) => void;
26
+ };
27
+
28
+ /**
29
+ * Floating label on the input's top border with a required `*` and an
30
+ * optional pressable icon; renders nothing without `floatingValue`.
31
+ */
32
+ const FloatingView: FC<FloatingViewProps> = ({
33
+ floatingValue,
34
+ floatingIconColor,
35
+ disabled,
36
+ floatingIcon,
37
+ required,
38
+ style,
39
+ onPress,
40
+ componentId,
41
+ }) => {
42
+ const { theme } = useContext(ApplicationContext);
43
+ const scaledTop = useScaleSize(LABEL_OFFSET);
44
+ if (!floatingValue) {
45
+ return null;
46
+ }
47
+
48
+ let floatingTextColor = theme.colors.text.hint;
49
+ let floatingIconTintColor = floatingIconColor;
50
+ if (disabled) {
51
+ floatingTextColor = theme.colors.text.disable;
52
+ floatingIconTintColor = theme.colors.text.disable;
53
+ }
54
+
55
+ return (
56
+ <View
57
+ style={[
58
+ styles.floatingView,
59
+ styles.floatingViewLayer,
60
+ {
61
+ backgroundColor: theme.colors.background.surface,
62
+ top: -scaledTop,
63
+ },
64
+ style,
65
+ ]}
66
+ >
67
+ <Text
68
+ color={floatingTextColor}
69
+ typography={'label_s_medium'}
70
+ accessibilityLabel={`${componentId}|floating-text`}
71
+ >
72
+ {floatingValue}
73
+ {required && (
74
+ <Text
75
+ typography={'label_s_medium'}
76
+ color={theme.colors.error.primary}
77
+ >
78
+ *
79
+ </Text>
80
+ )}
81
+ </Text>
82
+ {!!floatingIcon && (
83
+ <TouchableOpacity
84
+ activeOpacity={onPress ? PRESSED_OPACITY : 1}
85
+ onPress={onPress}
86
+ accessibilityLabel={`${componentId}|floating-icon-touch`}
87
+ >
88
+ <Icon
89
+ color={floatingIconTintColor}
90
+ source={floatingIcon}
91
+ size={ICON_SIZE}
92
+ style={styles.floatingIcon}
93
+ accessibilityLabel={`${componentId}|floating-icon`}
94
+ />
95
+ </TouchableOpacity>
96
+ )}
97
+ </View>
98
+ );
99
+ };
100
+
101
+ export { FloatingView };
package/Input/Input.tsx CHANGED
@@ -13,22 +13,25 @@ import {
13
13
  } from 'react-native';
14
14
  import { useComponentId } from '../Application';
15
15
  import { Icon } from '../Icon';
16
- import { exportFontFamily, useScaleSize } from '../Text';
17
- import {
18
- ErrorView,
19
- FloatingView,
20
- getBorderColor,
21
- getSizeStyle,
22
- RenderTrailing,
23
- } from './common';
24
- import { InputProps } from './index';
25
- import styles from './styles';
26
- import SystemTextInput from './SystemTextInput';
27
16
  import {
28
17
  ApplicationContext,
29
18
  ComponentContext,
30
19
  MiniAppContext,
31
20
  } from '../Context';
21
+ import { exportFontFamily, useScaleSize } from '../Text';
22
+ import { CLEAR_ICON_SIZE, INPUT_FONT_SIZE } from './constants';
23
+ import { ErrorView } from './ErrorView';
24
+ import { FloatingView } from './FloatingView';
25
+ import { InputProps } from './index';
26
+ import { RenderTrailing } from './RenderTrailing';
27
+ import styles from './styles';
28
+ import SystemTextInput from './SystemTextInput';
29
+ import {
30
+ getBorderColor,
31
+ getInputHeight,
32
+ getLeadingIconSize,
33
+ getSizeStyle,
34
+ } from './utils';
32
35
 
33
36
  /**
34
37
  * Text input with floating label, leading/trailing/clear icons and an error or
@@ -73,11 +76,11 @@ const Input = forwardRef(
73
76
  ) => {
74
77
  const { theme } = useContext(ApplicationContext);
75
78
  const context = useContext<any>(MiniAppContext);
76
- const scaledFontSize = useScaleSize(14);
77
- const scaleHeight = useScaleSize(size === 'small' ? 48 : 56);
78
- const [focused, setFocused] = useState(false);
79
+ const scaledFontSize = useScaleSize(INPUT_FONT_SIZE);
80
+ const scaleHeight = useScaleSize(getInputHeight(size));
81
+ const [isFocused, setIsFocused] = useState(false);
79
82
  const [internalText, setInternalText] = useState(defaultValue || '');
80
- const haveValue = value !== undefined ? !!value : !!internalText;
83
+ const hasValue = value !== undefined ? !!value : !!internalText;
81
84
  const inputRef = useRef<TextInput | null>(null);
82
85
  const componentName = 'Input';
83
86
 
@@ -88,30 +91,30 @@ const Input = forwardRef(
88
91
  props.accessibilityLabel,
89
92
  );
90
93
 
91
- const onClearText = () => {
94
+ const handleClearText = () => {
92
95
  inputRef?.current?.clear();
93
96
  inputRef.current?.setNativeProps({ text: '' });
94
- _onChangeText('');
97
+ handleChangeText('');
95
98
  };
96
99
 
97
- const _onChangeText = (text: string) => {
100
+ const handleChangeText = (text: string) => {
98
101
  if (value === undefined) {
99
102
  setInternalText(text);
100
103
  }
101
104
  onChangeText?.(text);
102
105
  };
103
106
 
104
- const _onFocus = (e: TextInputFocusEvent) => {
105
- setFocused(true);
107
+ const handleFocus = (e: TextInputFocusEvent) => {
108
+ setIsFocused(true);
106
109
  onFocus?.(e);
107
110
  };
108
111
 
109
- const _onBlur = (e: TextInputFocusEvent) => {
110
- setFocused(false);
112
+ const handleBlur = (e: TextInputFocusEvent) => {
113
+ setIsFocused(false);
111
114
  onBlur?.(e);
112
115
  };
113
116
 
114
- const _setText = (text: string) => {
117
+ const handleSetText = (text: string) => {
115
118
  if (__DEV__ && value !== undefined) {
116
119
  console.error(
117
120
  `${componentName}: calling ref.setText() on a controlled input (\`value\` is provided) is an anti-pattern. ` +
@@ -120,21 +123,21 @@ const Input = forwardRef(
120
123
  );
121
124
  }
122
125
  inputRef?.current?.setNativeProps({ text });
123
- _onChangeText(text);
126
+ handleChangeText(text);
124
127
  };
125
128
 
126
129
  useImperativeHandle(ref, () => {
127
130
  return {
128
- clear: onClearText,
131
+ clear: handleClearText,
129
132
  focus: () => inputRef.current?.focus(),
130
133
  blur: () => inputRef.current?.blur(),
131
- setText: _setText,
134
+ setText: handleSetText,
132
135
  };
133
136
  });
134
137
 
135
138
  const renderInputView = () => {
136
139
  const disabledColor = theme.colors.text.disable;
137
- const secure = secureTextEntry;
140
+ const isSecure = secureTextEntry;
138
141
  let textColor = theme.colors.text.default;
139
142
  let placeholderColor = theme.colors.text.hint;
140
143
  let iconTintColor = trailingColor;
@@ -156,7 +159,7 @@ const Input = forwardRef(
156
159
  styles.inputWrapper,
157
160
  { backgroundColor: theme.colors.background.surface },
158
161
  getSizeStyle(size, undefined, scaleHeight),
159
- getBorderColor(theme, focused, errorMessage, disabled),
162
+ getBorderColor(theme, isFocused, errorMessage, disabled),
160
163
  ]}
161
164
  >
162
165
  <FloatingView
@@ -178,7 +181,7 @@ const Input = forwardRef(
178
181
  <Icon
179
182
  color={leadingIconColor}
180
183
  source={leadingIcon}
181
- size={size === 'small' ? 24 : 32}
184
+ size={getLeadingIconSize(size)}
182
185
  accessibilityLabel={`${componentId}|leading-icon`}
183
186
  />
184
187
  </TouchableOpacity>
@@ -188,7 +191,7 @@ const Input = forwardRef(
188
191
  {...props}
189
192
  accessibilityLabel={`${componentId}`}
190
193
  editable={editable && !disabled}
191
- secureTextEntry={secure}
194
+ secureTextEntry={isSecure}
192
195
  textAlignVertical="center"
193
196
  ref={inputRef}
194
197
  accessibilityState={{
@@ -202,15 +205,15 @@ const Input = forwardRef(
202
205
  color: textColor,
203
206
  fontFamily: exportFontFamily(fontWeight ?? 'regular'),
204
207
  },
205
- secure && haveValue && secureTextStyle,
208
+ isSecure && hasValue && secureTextStyle,
206
209
  ]}
207
210
  defaultValue={defaultValue}
208
211
  allowFontScaling={false}
209
212
  textBreakStrategy="highQuality"
210
213
  value={value}
211
- onChangeText={_onChangeText}
212
- onFocus={_onFocus}
213
- onBlur={_onBlur}
214
+ onChangeText={handleChangeText}
215
+ onFocus={handleFocus}
216
+ onBlur={handleBlur}
214
217
  placeholder={placeholder}
215
218
  selectionColor={theme.colors.primary}
216
219
  placeholderTextColor={placeholderColor}
@@ -218,16 +221,16 @@ const Input = forwardRef(
218
221
  />
219
222
  </View>
220
223
  <View style={styles.iconView}>
221
- {showClearIcon && focused && haveValue && (
224
+ {showClearIcon && isFocused && hasValue && (
222
225
  <TouchableOpacity
223
226
  accessibilityLabel={`${componentId}|clear-icon-touch`}
224
227
  accessible={false}
225
228
  style={styles.iconWrapper}
226
- onPress={onClearText}
229
+ onPress={handleClearText}
227
230
  >
228
231
  <Icon
229
232
  source="24_navigation_close_circle_full"
230
- size={16}
233
+ size={CLEAR_ICON_SIZE}
231
234
  color={theme.colors.text.hint}
232
235
  accessibilityLabel={`${componentId}|clear-icon`}
233
236
  />
@@ -6,17 +6,20 @@ import {
6
6
  ComponentContext,
7
7
  MiniAppContext,
8
8
  } from '../Context';
9
+ import { Spacing } from '../Consts';
9
10
  import { Icon } from '../Icon';
11
+ import { Text, useScaleSize } from '../Text';
12
+ import { PRESSED_OPACITY } from './constants';
13
+ import { ErrorView } from './ErrorView';
14
+ import { FloatingView } from './FloatingView';
15
+ import { InputDropDownProps } from './index';
16
+ import styles from './styles';
10
17
  import {
11
- ErrorView,
12
- FloatingView,
13
18
  getBorderColor,
19
+ getInputHeight,
20
+ getLeadingIconSize,
14
21
  getSizeStyle,
15
- } from './common';
16
- import { InputDropDownProps } from './index';
17
- import { Text, useScaleSize } from '../Text';
18
- import styles from './styles';
19
- import { Spacing } from '../Consts';
22
+ } from './utils';
20
23
 
21
24
  /**
22
25
  * Pressable field styled like an input with a chevron; shows `value` or
@@ -44,7 +47,7 @@ const InputDropDown = ({
44
47
  }: InputDropDownProps) => {
45
48
  const { theme } = useContext(ApplicationContext);
46
49
  const context = useContext<any>(MiniAppContext);
47
- const scaleHeight = useScaleSize(size === 'small' ? 48 : 56);
50
+ const scaleHeight = useScaleSize(getInputHeight(size));
48
51
  const componentName = 'InputDropDown';
49
52
 
50
53
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
@@ -99,7 +102,7 @@ const InputDropDown = ({
99
102
  <Icon
100
103
  color={leadingIconColor}
101
104
  source={leadingIcon}
102
- size={size === 'small' ? 24 : 32}
105
+ size={getLeadingIconSize(size)}
103
106
  accessibilityLabel={`${componentId}|leading-icon`}
104
107
  />
105
108
  </View>
@@ -136,7 +139,7 @@ const InputDropDown = ({
136
139
  {...props}
137
140
  accessibilityLabel={`${componentId}|touch`}
138
141
  accessibilityState={{ disabled: disabled as boolean }}
139
- activeOpacity={0.6}
142
+ activeOpacity={PRESSED_OPACITY}
140
143
  style={[
141
144
  style,
142
145
  styles.wrapper,
@@ -10,11 +10,11 @@ import React, {
10
10
  import { Platform, TextInput, TextInputFocusEvent, View } from 'react-native';
11
11
  import { useComponentId } from '../Application';
12
12
  import { ComponentContext, MiniAppContext } from '../Context';
13
- import { ErrorView } from './common';
13
+ import { ErrorView } from './ErrorView';
14
14
  import { InputMoneyProps } from './index';
15
15
  import { InputMoneyView } from './InputMoneyView';
16
- import { findEdit, moneyFormatter } from './moneyUtils';
17
16
  import styles from './styles';
17
+ import { findEdit, moneyFormatter } from './utils';
18
18
 
19
19
  /**
20
20
  * Money input: keeps digits only, shows them grouped with a currency suffix and
@@ -52,7 +52,7 @@ const InputMoney = forwardRef(
52
52
  ref,
53
53
  ) => {
54
54
  const context = useContext<any>(MiniAppContext);
55
- const [focused, setFocused] = useState(false);
55
+ const [isFocused, setIsFocused] = useState(false);
56
56
  const [displayValue, setDisplayValue] = useState('');
57
57
  const [numericValue, setNumericValue] = useState('');
58
58
  const [lastDisplayValue, setLastDisplayValue] = useState('');
@@ -126,7 +126,7 @@ const InputMoney = forwardRef(
126
126
  }
127
127
  };
128
128
 
129
- const onClearText = () => {
129
+ const handleClearText = () => {
130
130
  inputRef?.current?.clear();
131
131
  inputRef.current?.setNativeProps({ text: '' });
132
132
  setDisplayValue('');
@@ -134,7 +134,7 @@ const InputMoney = forwardRef(
134
134
  onChangeText?.('');
135
135
  };
136
136
 
137
- const _onChangeText = (text: string) => {
137
+ const handleChangeText = (text: string) => {
138
138
  if (!text) {
139
139
  lastRawText.current = '';
140
140
  setDisplayValue('');
@@ -166,17 +166,17 @@ const InputMoney = forwardRef(
166
166
  inputRef.current?.setSelection(displayValue.length, displayValue.length);
167
167
  };
168
168
 
169
- const _onFocus = (e: TextInputFocusEvent) => {
170
- setFocused(true);
169
+ const handleFocus = (e: TextInputFocusEvent) => {
170
+ setIsFocused(true);
171
171
  onFocus?.(e);
172
172
  };
173
173
 
174
- const _onBlur = (e: TextInputFocusEvent) => {
175
- setFocused(false);
174
+ const handleBlur = (e: TextInputFocusEvent) => {
175
+ setIsFocused(false);
176
176
  onBlur?.(e);
177
177
  };
178
178
 
179
- const _setText = (text: string) => {
179
+ const handleSetText = (text: string) => {
180
180
  caretDigitsAfter.current = undefined;
181
181
  if (!text) {
182
182
  setDisplayValue('');
@@ -190,10 +190,10 @@ const InputMoney = forwardRef(
190
190
 
191
191
  useImperativeHandle(ref, () => {
192
192
  return {
193
- clear: onClearText,
193
+ clear: handleClearText,
194
194
  focus: () => inputRef.current?.focus(),
195
195
  blur: () => inputRef.current?.blur(),
196
- setText: _setText,
196
+ setText: handleSetText,
197
197
  };
198
198
  });
199
199
 
@@ -240,16 +240,16 @@ const InputMoney = forwardRef(
240
240
  showClearIcon={showClearIcon}
241
241
  placeholder={placeholder}
242
242
  currency={currency}
243
- focused={focused}
243
+ focused={isFocused}
244
244
  componentId={componentId}
245
245
  displayValue={displayValue}
246
246
  caretSelection={caretSelection}
247
247
  inputRef={inputRef}
248
248
  focusAtEnd={focusAtEnd}
249
- onClearText={onClearText}
250
- onChangeText={_onChangeText}
251
- onFocus={_onFocus}
252
- onBlur={_onBlur}
249
+ onClearText={handleClearText}
250
+ onChangeText={handleChangeText}
251
+ onFocus={handleFocus}
252
+ onBlur={handleBlur}
253
253
  inputProps={props}
254
254
  />
255
255
  <ErrorView
@@ -11,15 +11,15 @@ import {
11
11
  import { ApplicationContext } from '../Context';
12
12
  import { Icon } from '../Icon';
13
13
  import { useScaleSize, exportFontFamily } from '../Text';
14
- import {
15
- FloatingView,
16
- getBorderColor,
17
- getSizeStyle,
18
- RenderTrailing,
19
- } from './common';
14
+ import { CLEAR_ICON_SIZE } from './constants';
15
+ import { FloatingView } from './FloatingView';
20
16
  import { InputMoneyProps } from './index';
17
+ import { RenderTrailing } from './RenderTrailing';
21
18
  import styles from './styles';
22
19
  import SystemTextInput from './SystemTextInput';
20
+ import { getBorderColor, getInputHeight, getSizeStyle } from './utils';
21
+
22
+ const MONEY_FONT_SIZE = 20;
23
23
 
24
24
  type InputMoneyViewProps = Pick<
25
25
  InputMoneyProps,
@@ -85,8 +85,8 @@ export const InputMoneyView = ({
85
85
  inputProps: props,
86
86
  }: InputMoneyViewProps) => {
87
87
  const { theme } = useContext(ApplicationContext);
88
- const scaleHeight = useScaleSize(size === 'small' ? 48 : 56);
89
- const scaledFontSize = useScaleSize(20);
88
+ const scaleHeight = useScaleSize(getInputHeight(size));
89
+ const scaledFontSize = useScaleSize(MONEY_FONT_SIZE);
90
90
  const inputFontFamily = exportFontFamily('bold');
91
91
  const disabledColor = theme.colors.text.disable;
92
92
  let textColor = theme.colors.text.default;
@@ -101,11 +101,11 @@ export const InputMoneyView = ({
101
101
  fontSize: scaledFontSize,
102
102
  fontFamily: inputFontFamily,
103
103
  };
104
- const placeholderHasCurrency = placeholder.endsWith(currency);
105
- const shouldShowCurrency = !!displayValue || placeholderHasCurrency;
104
+ const hasCurrencyInPlaceholder = placeholder.endsWith(currency);
105
+ const shouldShowCurrency = !!displayValue || hasCurrencyInPlaceholder;
106
106
  const shouldShowClearIcon = !!(showClearIcon && focused && displayValue);
107
107
  let inputPlaceholder = placeholder;
108
- if (placeholderHasCurrency) {
108
+ if (hasCurrencyInPlaceholder) {
109
109
  inputPlaceholder = placeholder.slice(0, -currency.length);
110
110
  }
111
111
  return (
@@ -175,7 +175,7 @@ export const InputMoneyView = ({
175
175
  >
176
176
  <Icon
177
177
  source="24_navigation_close_circle_full"
178
- size={16}
178
+ size={CLEAR_ICON_SIZE}
179
179
  color={theme.colors.text.hint}
180
180
  accessibilityLabel={`${componentId}|clear-icon`}
181
181
  />
@@ -26,31 +26,37 @@ import Animated, {
26
26
  } from 'react-native-reanimated';
27
27
  import { useComponentId } from '../Application';
28
28
  import { Spacing, Styles } from '../Consts';
29
- import { useScaleSize, Text } from '../Text';
30
- import { ErrorView, getBorderColor } from './common';
31
- import { CaretProps, InputOTPProps } from './index';
32
- import styles from './styles';
33
- import { Icon } from '../Icon';
34
- import SystemTextInput from './SystemTextInput';
35
29
  import {
36
30
  ApplicationContext,
37
31
  ComponentContext,
38
32
  MiniAppContext,
39
33
  } from '../Context';
34
+ import { Icon } from '../Icon';
35
+ import { useScaleSize, Text } from '../Text';
36
+ import { ErrorView } from './ErrorView';
37
+ import { CaretProps, InputOTPProps } from './index';
38
+ import styles from './styles';
39
+ import SystemTextInput from './SystemTextInput';
40
+ import { getBorderColor } from './utils';
41
+
42
+ const BLINK_DURATION = 300;
43
+ const CARET_HEIGHT = 12;
44
+ const OTP_CLEAR_ICON_SIZE = 12;
45
+ const MAX_LENGTH = 10;
46
+ const COMPACT_LENGTH_THRESHOLD = 6;
40
47
 
41
48
  /** Blinking caret shown in the OTP slot being typed. */
42
49
  const OTPCaret: FC<CaretProps> = ({ index, length }) => {
43
- const DURATION = 300;
44
50
  const { theme } = useContext(ApplicationContext);
45
51
  const opacity = useSharedValue(0);
46
52
 
47
53
  useEffect(() => {
48
54
  opacity.value = withRepeat(
49
55
  withSequence(
50
- withTiming(1, { duration: DURATION, easing: Easing.linear }),
56
+ withTiming(1, { duration: BLINK_DURATION, easing: Easing.linear }),
51
57
  withDelay(
52
- DURATION * 2,
53
- withTiming(0, { duration: DURATION, easing: Easing.linear }),
58
+ BLINK_DURATION * 2,
59
+ withTiming(0, { duration: BLINK_DURATION, easing: Easing.linear }),
54
60
  ),
55
61
  ),
56
62
  -1,
@@ -72,7 +78,7 @@ const OTPCaret: FC<CaretProps> = ({ index, length }) => {
72
78
  style={[
73
79
  styles.otpCursor,
74
80
  {
75
- height: useScaleSize(12),
81
+ height: useScaleSize(CARET_HEIGHT),
76
82
  backgroundColor: theme.colors.primary,
77
83
  },
78
84
  animatedStyle,
@@ -108,9 +114,8 @@ const InputOTP = forwardRef(
108
114
  }: InputOTPProps,
109
115
  ref,
110
116
  ) => {
111
- const MAX_LENGTH = 10;
112
117
  const [value, setValue] = useState('');
113
- const [focused, setFocused] = useState(false);
118
+ const [isFocused, setIsFocused] = useState(false);
114
119
  const inputRef = useRef<TextInput | null>(null);
115
120
  const { theme } = useContext(ApplicationContext);
116
121
  const context = useContext<any>(MiniAppContext);
@@ -127,25 +132,25 @@ const InputOTP = forwardRef(
127
132
  useImperativeHandle(ref, () => {
128
133
  return {
129
134
  onChangeText: (text: string) => {
130
- _onChangeText(text);
135
+ handleChangeText(text);
131
136
  },
132
137
  focus: () => inputRef.current?.focus(),
133
138
  blur: () => inputRef.current?.blur(),
134
- setText: (text: string) => _onChangeText(text),
139
+ setText: (text: string) => handleChangeText(text),
135
140
  };
136
141
  });
137
142
 
138
- const onFocusInput = (e: TextInputFocusEvent) => {
139
- setFocused(true);
143
+ const handleFocusInput = (e: TextInputFocusEvent) => {
144
+ setIsFocused(true);
140
145
  onFocus?.(e);
141
146
  };
142
147
 
143
- const onBlurInput = (e: TextInputFocusEvent) => {
144
- setFocused(false);
148
+ const handleBlurInput = (e: TextInputFocusEvent) => {
149
+ setIsFocused(false);
145
150
  onBlur?.(e);
146
151
  };
147
152
 
148
- const _onChangeText = (text: string) => {
153
+ const handleChangeText = (text: string) => {
149
154
  if (text.length > MAX_LENGTH) return;
150
155
  const isNumeric = !isNaN(Number(text));
151
156
  if (dataType === 'number' && !isNumeric) return;
@@ -156,8 +161,8 @@ const InputOTP = forwardRef(
156
161
  onChangeText?.(text);
157
162
  };
158
163
 
159
- const onClearText = () => {
160
- _onChangeText('');
164
+ const handleClearText = () => {
165
+ handleChangeText('');
161
166
  };
162
167
 
163
168
  const fontScale = PixelRatio.getFontScale();
@@ -167,7 +172,7 @@ const InputOTP = forwardRef(
167
172
  const textColors = theme.colors.text;
168
173
  for (let i = 0; i < inputLength; i++) {
169
174
  let slot: React.ReactNode;
170
- if (focused && value.length === i) {
175
+ if (isFocused && value.length === i) {
171
176
  slot = <OTPCaret index={i} length={inputLength} />;
172
177
  } else {
173
178
  slot = (
@@ -188,7 +193,7 @@ const InputOTP = forwardRef(
188
193
  const renderUnidentifiedInputs = () => {
189
194
  const splitValue = value.split('');
190
195
 
191
- if (!focused && value.length === 0 && !!placeholder) {
196
+ if (!isFocused && value.length === 0 && !!placeholder) {
192
197
  return (
193
198
  <Text
194
199
  color={theme.colors.text.hint}
@@ -203,8 +208,9 @@ const InputOTP = forwardRef(
203
208
  <View style={Styles.row}>
204
209
  {splitValue.map((letter, index) => {
205
210
  const isLast = index === value.length - 1;
206
- const isTyping = focused && value.length !== MAX_LENGTH;
207
- const isCompact = fontScale > 1 && value.length > 6;
211
+ const isTyping = isFocused && value.length !== MAX_LENGTH;
212
+ const isLongCode = value.length > COMPACT_LENGTH_THRESHOLD;
213
+ const isCompact = fontScale > 1 && isLongCode;
208
214
  const marginRight = isCompact ? Spacing.M : Spacing.L;
209
215
  return (
210
216
  <Text
@@ -216,7 +222,7 @@ const InputOTP = forwardRef(
216
222
  </Text>
217
223
  );
218
224
  })}
219
- {focused && value.length !== MAX_LENGTH && <OTPCaret />}
225
+ {isFocused && value.length !== MAX_LENGTH && <OTPCaret />}
220
226
  </View>
221
227
  );
222
228
  };
@@ -229,7 +235,7 @@ const InputOTP = forwardRef(
229
235
  style={[
230
236
  styles.otpInput,
231
237
  { backgroundColor: theme.colors.background.surface },
232
- getBorderColor(theme, focused, errorMessage),
238
+ getBorderColor(theme, isFocused, errorMessage),
233
239
  ]}
234
240
  >
235
241
  {!!floatingValue && (
@@ -248,9 +254,15 @@ const InputOTP = forwardRef(
248
254
  </Text>
249
255
  </View>
250
256
  )}
251
- {value.length > 0 && focused && (
252
- <TouchableOpacity onPress={onClearText} style={styles.clearIcon}>
253
- <Icon source={'24_navigation_close_circle_full'} size={12} />
257
+ {value.length > 0 && isFocused && (
258
+ <TouchableOpacity
259
+ onPress={handleClearText}
260
+ style={styles.clearIcon}
261
+ >
262
+ <Icon
263
+ source={'24_navigation_close_circle_full'}
264
+ size={OTP_CLEAR_ICON_SIZE}
265
+ />
254
266
  </TouchableOpacity>
255
267
  )}
256
268
  <View style={styles.otpInputsView}>
@@ -262,11 +274,11 @@ const InputOTP = forwardRef(
262
274
  ref={inputRef}
263
275
  value={value}
264
276
  textAlign={'center'}
265
- onChangeText={_onChangeText}
277
+ onChangeText={handleChangeText}
266
278
  keyboardType={dataType === 'number' ? 'number-pad' : 'default'}
267
279
  allowFontScaling={false}
268
- onFocus={onFocusInput}
269
- onBlur={onBlurInput}
280
+ onFocus={handleFocusInput}
281
+ onBlur={handleBlurInput}
270
282
  style={styles.otpRealInput}
271
283
  selectionColor={theme.colors.primary}
272
284
  placeholderTextColor={theme.colors.text.hint}