@momo-kits/foundation 0.169.4 → 0.169.5

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 (125) hide show
  1. package/Application/BottomSheet.hooks.ts +8 -8
  2. package/Application/BottomSheet.parts.tsx +44 -34
  3. package/Application/BottomSheet.tsx +35 -26
  4. package/Application/BottomTab/ActiveIndicator.tsx +15 -14
  5. package/Application/BottomTab/BottomTabBar.tsx +43 -34
  6. package/Application/BottomTab/BottomTabInset.tsx +8 -4
  7. package/Application/BottomTab/CurvedContainer.tsx +14 -12
  8. package/Application/BottomTab/CustomBottomTabItem.tsx +10 -6
  9. package/Application/BottomTab/DuoBarCompression.tsx +13 -19
  10. package/Application/BottomTab/DuoRailTabBar.tsx +13 -13
  11. package/Application/BottomTab/DuoTabBar.tsx +8 -9
  12. package/Application/BottomTab/TabBarIcon.tsx +13 -8
  13. package/Application/BottomTab/TabScreen.tsx +1 -5
  14. package/Application/BottomTab/index.tsx +38 -30
  15. package/Application/BottomTab/metrics.ts +21 -20
  16. package/Application/BottomTab/tabItems.ts +1 -1
  17. package/Application/BottomTab/usePressLift.ts +2 -2
  18. package/Application/Components/BackgroundImageView.tsx +4 -0
  19. package/Application/Components/DuoGlassBackground.tsx +5 -4
  20. package/Application/Components/HeaderAnimated.tsx +4 -0
  21. package/Application/Components/HeaderBackground.tsx +4 -0
  22. package/Application/Components/HeaderExtendHeader.tsx +11 -10
  23. package/Application/Components/HeaderLeft.tsx +2 -1
  24. package/Application/Components/HeaderRight.tsx +2 -1
  25. package/Application/Components/HeaderTitle.shared.ts +11 -9
  26. package/Application/Components/HeaderTitle.tsx +4 -3
  27. package/Application/Components/HeaderToolkitAction.tsx +17 -20
  28. package/Application/Components/NavigationButton.tsx +2 -1
  29. package/Application/Components/SearchHeader.tsx +17 -10
  30. package/Application/Components/TitleJourney.tsx +2 -2
  31. package/Application/Components/TitleLocation.tsx +2 -2
  32. package/Application/Components/TitleUser.tsx +2 -2
  33. package/Application/Components/useToolkitActions.tsx +17 -32
  34. package/Application/Localize.ts +7 -0
  35. package/Application/ModalScreen.tsx +9 -3
  36. package/Application/Navigation.ts +14 -16
  37. package/Application/NavigationContainer.tsx +19 -16
  38. package/Application/Navigator.ts +20 -48
  39. package/Application/StackScreen.tracking.ts +11 -26
  40. package/Application/StackScreen.tsx +8 -35
  41. package/Application/TooltipPortal.tsx +12 -5
  42. package/Application/WidgetContainer.tsx +10 -4
  43. package/Application/modalLayout.ts +11 -12
  44. package/Application/navigationOptions.tsx +21 -17
  45. package/Badge/Badge.tsx +5 -0
  46. package/Badge/BadgeDot.tsx +1 -0
  47. package/Badge/BadgeDotAnimation.tsx +15 -7
  48. package/Badge/BadgeRibbon.tsx +46 -38
  49. package/Badge/Shape.tsx +2 -0
  50. package/Badge/types.ts +2 -2
  51. package/Button/index.tsx +5 -16
  52. package/Button/utils.ts +9 -15
  53. package/CheckBox/index.tsx +4 -0
  54. package/Consts/theme.ts +10 -10
  55. package/Context/index.ts +1 -0
  56. package/Divider/DashDivider.tsx +1 -0
  57. package/Divider/index.tsx +4 -0
  58. package/FoundationList/index.tsx +4 -0
  59. package/Icon/index.tsx +6 -6
  60. package/Icon/types.ts +1 -1
  61. package/IconButton/index.tsx +4 -12
  62. package/Image/index.tsx +9 -7
  63. package/Input/Input.tsx +8 -9
  64. package/Input/InputDropDown.tsx +10 -12
  65. package/Input/InputMoney.tsx +12 -8
  66. package/Input/InputMoneyView.tsx +14 -7
  67. package/Input/InputOTP.tsx +35 -33
  68. package/Input/InputPhoneNumber.tsx +8 -9
  69. package/Input/InputSearch.tsx +4 -6
  70. package/Input/InputTextArea.tsx +4 -0
  71. package/Input/SearchTextTyping.tsx +4 -0
  72. package/Input/SystemTextInput.tsx +7 -2
  73. package/Input/TextTyping.tsx +4 -0
  74. package/Input/common.tsx +26 -12
  75. package/Input/index.tsx +17 -20
  76. package/Input/moneyUtils.ts +10 -4
  77. package/Input/styles.ts +0 -5
  78. package/Input/utils.ts +14 -12
  79. package/Layout/Card.tsx +7 -4
  80. package/Layout/FloatingButton.tsx +4 -0
  81. package/Layout/Item.tsx +6 -2
  82. package/Layout/ItemList.tsx +9 -7
  83. package/Layout/ItemSectionList.tsx +2 -1
  84. package/Layout/ReservedRegions.tsx +25 -25
  85. package/Layout/Screen.tsx +56 -58
  86. package/Layout/Section.tsx +8 -5
  87. package/Layout/TrackingScope.tsx +1 -0
  88. package/Layout/screenContent.tsx +4 -2
  89. package/Layout/screenFooter.tsx +28 -18
  90. package/Layout/screenHeader.tsx +34 -31
  91. package/Layout/screenQuickReturn.tsx +17 -20
  92. package/Layout/screenScroll.ts +4 -7
  93. package/Layout/screenTypes.ts +3 -2
  94. package/Layout/types.ts +2 -4
  95. package/Layout/utils.ts +31 -37
  96. package/Loader/DotLoader.tsx +4 -0
  97. package/Loader/Loader.tsx +4 -0
  98. package/Loader/ProgressBar.tsx +1 -0
  99. package/Loader/Spinner.tsx +4 -0
  100. package/Loader/utils.ts +11 -7
  101. package/Pagination/Dot.tsx +4 -0
  102. package/Pagination/PaginationDot.tsx +1 -0
  103. package/Pagination/PaginationNumber.tsx +5 -3
  104. package/Pagination/PaginationScroll.tsx +5 -0
  105. package/Pagination/PaginationWhiteDot.tsx +1 -0
  106. package/Pagination/index.tsx +4 -0
  107. package/Pagination/types.ts +2 -2
  108. package/Popup/PopupNotify.tsx +39 -39
  109. package/Popup/PopupPromotion.tsx +4 -7
  110. package/Popup/types.ts +4 -4
  111. package/Popup/utils.ts +4 -0
  112. package/Radio/index.tsx +4 -0
  113. package/Skeleton/index.tsx +8 -0
  114. package/Switch/index.tsx +1 -0
  115. package/Tag/index.tsx +5 -1
  116. package/Tag/types.ts +3 -3
  117. package/Text/index.tsx +14 -3
  118. package/Text/styles.ts +1 -1
  119. package/Text/types.ts +1 -6
  120. package/Text/utils.ts +9 -0
  121. package/Title/index.tsx +41 -32
  122. package/ViewBoundary/index.tsx +4 -2
  123. package/index.ts +0 -6
  124. package/package.json +1 -1
  125. package/Application/__tests__/modalLayout.test.ts +0 -128
package/Button/index.tsx CHANGED
@@ -63,6 +63,11 @@ export interface ButtonProps extends TouchableOpacityProps {
63
63
  params?: any;
64
64
  }
65
65
 
66
+ /**
67
+ * Design-system button: `type` sets the colors, `size` the height and text,
68
+ * and `loading` swaps an icon slot for a spinner. Primary buttons use the mini
69
+ * app gradient from `config.navigationBar.buttonColors` when set.
70
+ */
66
71
  const Button: FC<ButtonProps> = ({
67
72
  type = 'primary',
68
73
  size = 'large',
@@ -90,33 +95,21 @@ const Button: FC<ButtonProps> = ({
90
95
  rest.accessibilityLabel,
91
96
  );
92
97
 
93
- /**
94
- * animate press in (Reanimated)
95
- */
96
98
  const animateIn = (event: GestureResponderEvent) => {
97
99
  rest?.onPressIn?.(event);
98
100
  pressAnim.value = withTiming(1, { duration: 100 });
99
101
  };
100
102
 
101
- /**
102
- * animate press out (Reanimated)
103
- */
104
103
  const animateOut = (event: GestureResponderEvent) => {
105
104
  rest?.onPressOut?.(event);
106
105
  pressAnim.value = withTiming(0, { duration: 100 });
107
106
  };
108
107
 
109
- /**
110
- * export size style
111
- */
112
108
  const getSizeStyle = () => {
113
109
  const styleSheet: { [key: string]: any } = styles;
114
110
  return styleSheet[size ?? 'small'];
115
111
  };
116
112
 
117
- /**
118
- * render title
119
- */
120
113
  const renderTitle = () => {
121
114
  const typography = getTypography(size);
122
115
  const textColor = getTextColor(type, theme, color);
@@ -127,10 +120,6 @@ const Button: FC<ButtonProps> = ({
127
120
  );
128
121
  };
129
122
 
130
- /**
131
- * render icon
132
- * @param position
133
- */
134
123
  const renderIcon = (position: 'left' | 'right') => {
135
124
  const iconSize = getIconSize(size);
136
125
  const space = getIconSpace(size);
package/Button/utils.ts CHANGED
@@ -4,10 +4,8 @@ import { Typography } from '../Text/types';
4
4
  import { ButtonProps } from './index';
5
5
 
6
6
  /**
7
- * determine loading position
8
- * @param left
9
- * @param right
10
- * @param isLeft
7
+ * Whether the loading spinner belongs in the `isLeft` icon slot: it takes the
8
+ * right slot when there is a right icon, otherwise the left one.
11
9
  */
12
10
  export const shouldShowLoading = (
13
11
  left?: string,
@@ -23,7 +21,8 @@ export const shouldShowLoading = (
23
21
  };
24
22
 
25
23
  /**
26
- * export type style
24
+ * Background and border of a button `type`; `color` (mini app button color)
25
+ * overrides the outline border.
27
26
  */
28
27
  export const getTypeStyle = (
29
28
  type: ButtonProps['type'],
@@ -64,9 +63,7 @@ export const getTypeStyle = (
64
63
  }
65
64
  };
66
65
 
67
- /**
68
- * export icon size
69
- */
66
+ /** Icon size for a button `size`. */
70
67
  export const getIconSize = (size: ButtonProps['size']) => {
71
68
  switch (size) {
72
69
  case 'large':
@@ -79,9 +76,7 @@ export const getIconSize = (size: ButtonProps['size']) => {
79
76
  }
80
77
  };
81
78
 
82
- /**
83
- * export icon space
84
- */
79
+ /** Gap between icon and title for a button `size`. */
85
80
  export const getIconSpace = (size: ButtonProps['size']) => {
86
81
  switch (size) {
87
82
  case 'large':
@@ -94,9 +89,7 @@ export const getIconSpace = (size: ButtonProps['size']) => {
94
89
  }
95
90
  };
96
91
 
97
- /**
98
- * export typography style
99
- */
92
+ /** Title typography for a button `size`. */
100
93
  export const getTypography = (size: ButtonProps['size']): Typography => {
101
94
  switch (size) {
102
95
  case 'large':
@@ -111,7 +104,8 @@ export const getTypography = (size: ButtonProps['size']): Typography => {
111
104
  };
112
105
 
113
106
  /**
114
- * export text color
107
+ * Title and icon color of a button `type`; `color` (mini app button color)
108
+ * overrides the outline and text types.
115
109
  */
116
110
  export const getTextColor = (
117
111
  type: ButtonProps['type'],
@@ -11,6 +11,10 @@ import {
11
11
  import { Text } from '../Text';
12
12
  import { Icon } from '../Icon';
13
13
 
14
+ /**
15
+ * Checkbox with an optional label. `indeterminate` shows a minus icon and is
16
+ * styled and announced as checked.
17
+ */
14
18
  const CheckBox: FC<CheckBoxProps> = ({
15
19
  value,
16
20
  disabled = false,
package/Consts/theme.ts CHANGED
@@ -62,22 +62,22 @@ const defaultDarkTheme: Theme = {
62
62
  secondary: Colors.pink_08,
63
63
  gradient: '#FDCADE',
64
64
  background: {
65
- default: '#121212', // Dark background
66
- surface: '#1e1e1e', // Slightly lighter surface background
65
+ default: '#121212',
66
+ surface: '#1e1e1e',
67
67
  tonal: Colors.pink_10,
68
- pressed: '#1a1a1a', // Pressed state background
68
+ pressed: '#1a1a1a',
69
69
  selected: Colors.pink_11,
70
- disable: '#303030', // Disabled state background
70
+ disable: '#303030',
71
71
  },
72
72
  text: {
73
- default: '#ffffff', // White text for better contrast
74
- secondary: '#b0b0b0', // Light gray secondary text
75
- hint: '#727272', // Hint text color
76
- disable: '#505050', // Disabled text color
73
+ default: '#ffffff',
74
+ secondary: '#b0b0b0',
75
+ hint: '#727272',
76
+ disable: '#505050',
77
77
  },
78
78
  border: {
79
- default: '#2a2a2a', // Darker borders
80
- disable: '#242424', // Disabled state border color
79
+ default: '#2a2a2a',
80
+ disable: '#242424',
81
81
  },
82
82
  success: {
83
83
  primary: Colors.green_03,
package/Context/index.ts CHANGED
@@ -5,6 +5,7 @@ import { defaultContext } from '../Consts';
5
5
  const Context = createContext({});
6
6
  const ApplicationContext = createContext(defaultContext);
7
7
 
8
+ // Shared by the host app via `Platform`; standalone falls back to a local one.
8
9
  const MiniAppContext = (Platform as any).MiniAppContext ?? Context;
9
10
  const ScreenContext = (Platform as any).ScreenContext ?? Context;
10
11
  const ComponentContext = (Platform as any).ComponentContext ?? Context;
@@ -11,6 +11,7 @@ export interface DashDividerProps {
11
11
  style?: ViewStyle;
12
12
  }
13
13
 
14
+ /** 1px dashed horizontal line in the default border color. */
14
15
  const DashDivider: React.FC<DashDividerProps> = ({ style }) => {
15
16
  const { theme } = useContext(ApplicationContext);
16
17
  const borderColor = theme.colors.border.default;
package/Divider/index.tsx CHANGED
@@ -18,6 +18,10 @@ export interface DividerProps {
18
18
  type?: 'dash' | 'default';
19
19
  }
20
20
 
21
+ /**
22
+ * 1px horizontal line in the default border color. `type='dash'` renders
23
+ * `DashDivider`, which always has a 4px vertical margin.
24
+ */
21
25
  const Divider: React.FC<DividerProps> = ({
22
26
  style,
23
27
  useMargin = true,
@@ -5,6 +5,10 @@ import { FlatListProps } from 'react-native';
5
5
  type CombinedListProps<ItemT> = FlatListProps<ItemT> &
6
6
  Partial<FlashListProps<ItemT>>;
7
7
 
8
+ /**
9
+ * FlashList typed to also accept FlatList props, so FlatList call sites can
10
+ * switch to it without type changes.
11
+ */
8
12
  const FoundationList = <ItemT,>(props: CombinedListProps<ItemT>) => {
9
13
  return <FlashList {...props} />;
10
14
  };
package/Icon/index.tsx CHANGED
@@ -7,6 +7,10 @@ import { Image } from '../Image';
7
7
  import { StyleSheet } from 'react-native';
8
8
  import { Colors } from '../Consts';
9
9
 
10
+ /**
11
+ * Tinted icon from the bundled icon map (unknown names fall back to
12
+ * `ic_warning`) or from a URL. `color={null}` keeps the image's own colors.
13
+ */
10
14
  const Icon: React.FC<IconProps> = ({
11
15
  source = 'ic_back',
12
16
  size = 24,
@@ -19,9 +23,6 @@ const Icon: React.FC<IconProps> = ({
19
23
 
20
24
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
21
25
 
22
- /**
23
- * get icon source maps or remote http
24
- */
25
26
  const getIconSource = (): any => {
26
27
  if (source && !source.includes('http')) {
27
28
  let icon: { [key: string]: { uri: string } } = IconSources;
@@ -30,9 +31,8 @@ const Icon: React.FC<IconProps> = ({
30
31
  return { uri: source };
31
32
  };
32
33
 
33
- const tintColorProps = color !== null && {
34
- tintColor: color ?? theme.colors.text.default,
35
- };
34
+ const tintColor = color ?? theme.colors.text.default;
35
+ const tintColorProps = color !== null && { tintColor };
36
36
 
37
37
  return (
38
38
  <Image
package/Icon/types.ts CHANGED
@@ -15,7 +15,7 @@ export type IconProps = {
15
15
  size?: number;
16
16
 
17
17
  /**
18
- * Optional. Represents the color of the icon.
18
+ * Optional. Represents the color of the icon; `null` keeps the image colors.
19
19
  */
20
20
  color?: string | null;
21
21
 
@@ -41,6 +41,10 @@ export interface IconButtonProps extends TouchableOpacityProps {
41
41
  params?: any;
42
42
  }
43
43
 
44
+ /**
45
+ * Icon-only button. `type` sets the background and icon color; `'disabled'`
46
+ * also blocks presses.
47
+ */
44
48
  const IconButton: React.FC<IconButtonProps> = ({
45
49
  type = 'primary',
46
50
  icon,
@@ -54,9 +58,6 @@ const IconButton: React.FC<IconButtonProps> = ({
54
58
 
55
59
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
56
60
 
57
- /**
58
- * get size icon button
59
- */
60
61
  const getSizeStyle = () => {
61
62
  switch (size) {
62
63
  case 'small':
@@ -68,16 +69,10 @@ const IconButton: React.FC<IconButtonProps> = ({
68
69
  }
69
70
  };
70
71
 
71
- /**
72
- * get shape icon button
73
- */
74
72
  const getShapeStyle = () => {
75
73
  return shape === 'square' ? styles.square : styles.circle;
76
74
  };
77
75
 
78
- /**
79
- * get style for icon button by type
80
- */
81
76
  const getTypeStyle = () => {
82
77
  switch (type) {
83
78
  case 'disabled':
@@ -111,9 +106,6 @@ const IconButton: React.FC<IconButtonProps> = ({
111
106
  }
112
107
  };
113
108
 
114
- /**
115
- * get color for icon
116
- */
117
109
  const getIconColor = () => {
118
110
  switch (type) {
119
111
  case 'disabled':
package/Image/index.tsx CHANGED
@@ -15,6 +15,7 @@ import { ImageProps } from './types';
15
15
 
16
16
  type Status = 'success' | 'loading' | 'error';
17
17
 
18
+ /** Last path segment of a `{ uri }` source, used in QA labels; '' otherwise. */
18
19
  export const getImageName = (source: any): string => {
19
20
  const parts = source?.uri?.split?.('/');
20
21
  if (parts?.length > 0) {
@@ -23,6 +24,10 @@ export const getImageName = (source: any): string => {
23
24
  return '';
24
25
  };
25
26
 
27
+ /**
28
+ * FastImage that shows a skeleton while loading (if `loading`) or inside a
29
+ * loading `SkeletonProvider`, and a fail icon on error.
30
+ */
26
31
  const Image: React.FC<ImageProps> = ({
27
32
  style,
28
33
  source,
@@ -44,15 +49,12 @@ const Image: React.FC<ImageProps> = ({
44
49
 
45
50
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
46
51
 
47
- let accessibilityLabel: any = `img|${getImageName(source)}`;
52
+ const imageName = getImageName(source);
53
+ let accessibilityLabel: any = `img|${imageName}`;
48
54
  if (component?.componentId) {
49
- accessibilityLabel = `${component?.componentId}|img|${getImageName(
50
- source,
51
- )}`;
55
+ accessibilityLabel = `${component?.componentId}|img|${imageName}`;
52
56
  }
53
- /**
54
- * render content loading | fail | rendered
55
- */
57
+
56
58
  const renderContent = () => {
57
59
  if (status === 'loading' || status === 'error') {
58
60
  let content = <Skeleton />;
package/Input/Input.tsx CHANGED
@@ -31,7 +31,8 @@ import {
31
31
  } from '../Context';
32
32
 
33
33
  /**
34
- * Input default component
34
+ * Text input with floating label, leading/trailing/clear icons and an error or
35
+ * hint row; controlled via `value` or uncontrolled via `defaultValue`.
35
36
  */
36
37
  const Input = forwardRef(
37
38
  (
@@ -131,15 +132,17 @@ const Input = forwardRef(
131
132
  };
132
133
  });
133
134
 
134
- /**
135
- * Render input view
136
- */
137
135
  const renderInputView = () => {
138
136
  const disabledColor = theme.colors.text.disable;
139
137
  const secure = secureTextEntry;
140
138
  let textColor = theme.colors.text.default;
141
139
  let placeholderColor = theme.colors.text.hint;
142
140
  let iconTintColor = trailingColor;
141
+ let secureTextStyle = styles.secureTextLarge;
142
+
143
+ if (size === 'small') {
144
+ secureTextStyle = styles.secureTextSmall;
145
+ }
143
146
 
144
147
  if (disabled) {
145
148
  textColor = disabledColor;
@@ -199,11 +202,7 @@ const Input = forwardRef(
199
202
  color: textColor,
200
203
  fontFamily: exportFontFamily(fontWeight ?? 'regular'),
201
204
  },
202
- secure &&
203
- haveValue &&
204
- (size === 'small'
205
- ? styles.secureTextSmall
206
- : styles.secureTextLarge),
205
+ secure && haveValue && secureTextStyle,
207
206
  ]}
208
207
  defaultValue={defaultValue}
209
208
  allowFontScaling={false}
@@ -1,5 +1,5 @@
1
1
  import React, { useContext } from 'react';
2
- import { TouchableOpacity, View } from 'react-native';
2
+ import { TouchableOpacity, View, type ViewStyle } from 'react-native';
3
3
  import { useComponentId } from '../Application';
4
4
  import {
5
5
  ApplicationContext,
@@ -18,6 +18,10 @@ import { Text, useScaleSize } from '../Text';
18
18
  import styles from './styles';
19
19
  import { Spacing } from '../Consts';
20
20
 
21
+ /**
22
+ * Pressable field styled like an input with a chevron; shows `value` or
23
+ * `placeholder`. Opening the picker is left to `onPress`.
24
+ */
21
25
  const InputDropDown = ({
22
26
  value,
23
27
  floatingValue,
@@ -50,18 +54,19 @@ const InputDropDown = ({
50
54
  props.accessibilityLabel,
51
55
  );
52
56
 
53
- /**
54
- * Render the input view
55
- */
56
57
  const renderInputView = () => {
57
58
  const disabledColor = theme.colors.text.disable;
58
59
  let textColor = theme.colors.text.default;
59
60
  let placeholderColor = theme.colors.text.hint;
61
+ let chevronStyle: ViewStyle = styles.iconViewDropDownSingle;
60
62
 
61
63
  if (disabled) {
62
64
  textColor = disabledColor;
63
65
  placeholderColor = disabledColor;
64
66
  }
67
+ if (multiline) {
68
+ chevronStyle = styles.iconViewDropDownMultiline;
69
+ }
65
70
 
66
71
  return (
67
72
  <View
@@ -111,14 +116,7 @@ const InputDropDown = ({
111
116
  </Text>
112
117
  </View>
113
118
  </View>
114
- <View
115
- style={[
116
- styles.iconViewDropDown,
117
- multiline
118
- ? styles.iconViewDropDownMultiline
119
- : styles.iconViewDropDownSingle,
120
- ]}
121
- >
119
+ <View style={[styles.iconViewDropDown, chevronStyle]}>
122
120
  <Icon source={'arrow_chevron_down_small'} />
123
121
  </View>
124
122
  </View>
@@ -16,6 +16,11 @@ import { InputMoneyView } from './InputMoneyView';
16
16
  import { findEdit, moneyFormatter } from './moneyUtils';
17
17
  import styles from './styles';
18
18
 
19
+ /**
20
+ * Money input: keeps digits only, shows them grouped with a currency suffix and
21
+ * reports raw digits to `onChangeText`. On Android it restores the caret by the
22
+ * count of digits after it, since regrouping shifts positions.
23
+ */
19
24
  const InputMoney = forwardRef(
20
25
  (
21
26
  {
@@ -88,9 +93,9 @@ const InputMoney = forwardRef(
88
93
 
89
94
  useEffect(() => {
90
95
  if (!caretSelection) return;
91
- setCaretSelection(current =>
92
- current === caretSelection ? undefined : current,
93
- );
96
+ setCaretSelection(current => {
97
+ return current === caretSelection ? undefined : current;
98
+ });
94
99
  }, [caretSelection]);
95
100
 
96
101
  const updateDisplayValue = (numeric: string) => {
@@ -142,11 +147,10 @@ const InputMoney = forwardRef(
142
147
  lastRawText.current = text;
143
148
  const formattedEdit = findEdit(lastDisplayValue, text);
144
149
  const rawEdit = findEdit(raw, text);
145
- const { old, start, suffix } =
146
- raw && rawEdit.size < formattedEdit.size ? rawEdit : formattedEdit;
147
- caretDigitsAfter.current = parseInputText(
148
- text.slice(text.length - suffix),
149
- ).length;
150
+ const isRawEditSmaller = !!raw && rawEdit.size < formattedEdit.size;
151
+ const { old, start, suffix } = isRawEditSmaller ? rawEdit : formattedEdit;
152
+ const textAfterEdit = text.slice(text.length - suffix);
153
+ caretDigitsAfter.current = parseInputText(textAfterEdit).length;
150
154
  if (text.length < old.length && digits === numericValue) {
151
155
  const index = parseInputText(text.slice(0, start)).length - 1;
152
156
  if (index >= 0) {
@@ -52,6 +52,10 @@ type InputMoneyViewProps = Pick<
52
52
  inputProps: TextInputProps;
53
53
  };
54
54
 
55
+ /**
56
+ * Presentational body of InputMoney: bordered field, currency suffix (kept
57
+ * after a placeholder that ends with it) and the clear/trailing icons.
58
+ */
55
59
  export const InputMoneyView = ({
56
60
  size,
57
61
  errorMessage,
@@ -98,9 +102,12 @@ export const InputMoneyView = ({
98
102
  fontFamily: inputFontFamily,
99
103
  };
100
104
  const placeholderHasCurrency = placeholder.endsWith(currency);
101
- const inputPlaceholder = placeholderHasCurrency
102
- ? placeholder.slice(0, -currency.length)
103
- : placeholder;
105
+ const shouldShowCurrency = !!displayValue || placeholderHasCurrency;
106
+ const shouldShowClearIcon = !!(showClearIcon && focused && displayValue);
107
+ let inputPlaceholder = placeholder;
108
+ if (placeholderHasCurrency) {
109
+ inputPlaceholder = placeholder.slice(0, -currency.length);
110
+ }
104
111
  return (
105
112
  <View
106
113
  style={[
@@ -147,7 +154,7 @@ export const InputMoneyView = ({
147
154
  keyboardType={'numeric'}
148
155
  maxLength={undefined}
149
156
  />
150
- {displayValue || placeholderHasCurrency ? (
157
+ {shouldShowCurrency && (
151
158
  <Text
152
159
  style={[
153
160
  textStyle,
@@ -157,10 +164,10 @@ export const InputMoneyView = ({
157
164
  >
158
165
  {currency}
159
166
  </Text>
160
- ) : null}
167
+ )}
161
168
  </Pressable>
162
169
  <View style={styles.iconView}>
163
- {showClearIcon && focused && displayValue ? (
170
+ {shouldShowClearIcon && (
164
171
  <TouchableOpacity
165
172
  accessibilityLabel={`${componentId}|clear-icon-touch`}
166
173
  style={styles.iconWrapper}
@@ -173,7 +180,7 @@ export const InputMoneyView = ({
173
180
  accessibilityLabel={`${componentId}|clear-icon`}
174
181
  />
175
182
  </TouchableOpacity>
176
- ) : null}
183
+ )}
177
184
  <RenderTrailing
178
185
  color={iconTintColor}
179
186
  icon={trailing}
@@ -38,6 +38,7 @@ import {
38
38
  MiniAppContext,
39
39
  } from '../Context';
40
40
 
41
+ /** Blinking caret shown in the OTP slot being typed. */
41
42
  const OTPCaret: FC<CaretProps> = ({ index, length }) => {
42
43
  const DURATION = 300;
43
44
  const { theme } = useContext(ApplicationContext);
@@ -60,9 +61,7 @@ const OTPCaret: FC<CaretProps> = ({ index, length }) => {
60
61
  };
61
62
  }, [opacity]);
62
63
 
63
- const animatedStyle = useAnimatedStyle(() => ({
64
- opacity: opacity.value,
65
- }));
64
+ const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
66
65
 
67
66
  const hasNextInput = !isNaN(Number(length)) && index !== Number(length) - 1;
68
67
  const spacingStyle = hasNextInput && { marginRight: Spacing.L };
@@ -86,6 +85,10 @@ const OTPCaret: FC<CaretProps> = ({ index, length }) => {
86
85
  );
87
86
  };
88
87
 
88
+ /**
89
+ * OTP input: a hidden TextInput drives `length` slots (or a free-length row
90
+ * when `length` is unset) with a blinking caret; accepts at most 10 chars.
91
+ */
89
92
  const InputOTP = forwardRef(
90
93
  (
91
94
  {
@@ -121,14 +124,16 @@ const InputOTP = forwardRef(
121
124
  props.accessibilityLabel,
122
125
  );
123
126
 
124
- useImperativeHandle(ref, () => ({
125
- onChangeText: (text: string) => {
126
- _onChangeText(text);
127
- },
128
- focus: () => inputRef.current?.focus(),
129
- blur: () => inputRef.current?.blur(),
130
- setText: (text: string) => _onChangeText(text),
131
- }));
127
+ useImperativeHandle(ref, () => {
128
+ return {
129
+ onChangeText: (text: string) => {
130
+ _onChangeText(text);
131
+ },
132
+ focus: () => inputRef.current?.focus(),
133
+ blur: () => inputRef.current?.blur(),
134
+ setText: (text: string) => _onChangeText(text),
135
+ };
136
+ });
132
137
 
133
138
  const onFocusInput = (e: TextInputFocusEvent) => {
134
139
  setFocused(true);
@@ -142,13 +147,10 @@ const InputOTP = forwardRef(
142
147
 
143
148
  const _onChangeText = (text: string) => {
144
149
  if (text.length > MAX_LENGTH) return;
145
- if (dataType === 'number' && isNaN(Number(text))) return;
146
- if (
147
- !isNaN(Number(text)) &&
148
- value.length >= Number(length) &&
149
- text.length >= value.length
150
- )
151
- return;
150
+ const isNumeric = !isNaN(Number(text));
151
+ if (dataType === 'number' && !isNumeric) return;
152
+ const isFull = value.length >= Number(length);
153
+ if (isNumeric && isFull && text.length >= value.length) return;
152
154
 
153
155
  setValue(text);
154
156
  onChangeText?.(text);
@@ -164,21 +166,21 @@ const InputOTP = forwardRef(
164
166
  const TextInputs: React.ReactNode[] = [];
165
167
  const textColors = theme.colors.text;
166
168
  for (let i = 0; i < inputLength; i++) {
167
- TextInputs.push(
168
- <View key={`otp-input-${i}`}>
169
- {focused && value.length === i ? (
170
- <OTPCaret index={i} length={inputLength} />
171
- ) : (
172
- <Text
173
- style={i !== inputLength - 1 && { marginRight: Spacing.L }}
174
- color={value[i] ? textColors.default : textColors.hint}
175
- typography={value[i] ? 'header_m_bold' : 'body_default_regular'}
176
- >
177
- {value[i] || '-'}
178
- </Text>
179
- )}
180
- </View>,
181
- );
169
+ let slot: React.ReactNode;
170
+ if (focused && value.length === i) {
171
+ slot = <OTPCaret index={i} length={inputLength} />;
172
+ } else {
173
+ slot = (
174
+ <Text
175
+ style={i !== inputLength - 1 && { marginRight: Spacing.L }}
176
+ color={value[i] ? textColors.default : textColors.hint}
177
+ typography={value[i] ? 'header_m_bold' : 'body_default_regular'}
178
+ >
179
+ {value[i] || '-'}
180
+ </Text>
181
+ );
182
+ }
183
+ TextInputs.push(<View key={`otp-input-${i}`}>{slot}</View>);
182
184
  }
183
185
  return TextInputs;
184
186
  };