@momo-kits/foundation 0.169.7-beta.4 → 0.169.7-beta.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 (143) hide show
  1. package/Application/BottomSheet.hooks.ts +8 -8
  2. package/Application/BottomSheet.parts.tsx +44 -34
  3. package/Application/BottomSheet.tsx +40 -37
  4. package/Application/BottomTab/ActiveIndicator.tsx +20 -15
  5. package/Application/BottomTab/BottomTabBar.styles.ts +18 -1
  6. package/Application/BottomTab/BottomTabBar.tsx +57 -72
  7. package/Application/BottomTab/BottomTabInset.tsx +8 -4
  8. package/Application/BottomTab/CurvedContainer.tsx +22 -14
  9. package/Application/BottomTab/CustomBottomTabItem.tsx +15 -15
  10. package/Application/BottomTab/DuoBarCompression.tsx +13 -19
  11. package/Application/BottomTab/DuoRailTabBar.tsx +20 -24
  12. package/Application/BottomTab/DuoTabBar.tsx +23 -13
  13. package/Application/BottomTab/TabBarIcon.tsx +15 -29
  14. package/Application/BottomTab/TabScreen.tsx +1 -5
  15. package/Application/BottomTab/index.tsx +33 -30
  16. package/Application/BottomTab/metrics.ts +46 -26
  17. package/Application/BottomTab/tabItems.ts +1 -1
  18. package/Application/BottomTab/usePressLift.ts +2 -2
  19. package/Application/Components/BackgroundImageView.tsx +4 -0
  20. package/Application/Components/DuoGlassBackground.tsx +5 -4
  21. package/Application/Components/HeaderAnimated.tsx +4 -0
  22. package/Application/Components/HeaderBackground.tsx +4 -0
  23. package/Application/Components/HeaderExtendHeader.tsx +11 -10
  24. package/Application/Components/HeaderLeft.tsx +2 -1
  25. package/Application/Components/HeaderRight.tsx +2 -1
  26. package/Application/Components/HeaderTitle.shared.ts +11 -9
  27. package/Application/Components/HeaderTitle.tsx +4 -3
  28. package/Application/Components/HeaderToolkitAction.tsx +17 -20
  29. package/Application/Components/NavigationButton.tsx +2 -1
  30. package/Application/Components/SearchHeader.tsx +17 -10
  31. package/Application/Components/TitleJourney.tsx +2 -2
  32. package/Application/Components/TitleLocation.tsx +2 -2
  33. package/Application/Components/TitleUser.tsx +2 -2
  34. package/Application/Components/useToolkitActions.tsx +17 -32
  35. package/Application/Localize.ts +7 -0
  36. package/Application/ModalScreen.tsx +10 -10
  37. package/Application/Navigation.ts +14 -16
  38. package/Application/NavigationContainer.tsx +19 -18
  39. package/Application/Navigator.ts +20 -48
  40. package/Application/ScaleSizeProvider.tsx +13 -6
  41. package/Application/StackScreen.tracking.ts +11 -33
  42. package/Application/StackScreen.tsx +10 -54
  43. package/Application/TooltipPortal.tsx +12 -5
  44. package/Application/WidgetContainer.tsx +10 -4
  45. package/Application/modalLayout.ts +13 -14
  46. package/Application/navigationOptions.tsx +21 -17
  47. package/Application/types.tabs.ts +0 -1
  48. package/Badge/Badge.tsx +5 -0
  49. package/Badge/BadgeDot.tsx +1 -0
  50. package/Badge/BadgeDotAnimation.tsx +15 -7
  51. package/Badge/BadgeRibbon.tsx +46 -38
  52. package/Badge/Shape.tsx +2 -0
  53. package/Badge/types.ts +2 -2
  54. package/Button/index.tsx +5 -16
  55. package/Button/utils.ts +9 -15
  56. package/CheckBox/index.tsx +4 -0
  57. package/Consts/theme.ts +10 -10
  58. package/Context/index.ts +5 -1
  59. package/Divider/DashDivider.tsx +1 -0
  60. package/Divider/index.tsx +4 -0
  61. package/FoundationList/index.tsx +4 -0
  62. package/Icon/index.tsx +6 -6
  63. package/Icon/types.ts +1 -1
  64. package/IconButton/index.tsx +4 -12
  65. package/Image/index.tsx +9 -7
  66. package/Input/Input.tsx +8 -9
  67. package/Input/InputDropDown.tsx +10 -12
  68. package/Input/InputMoney.tsx +12 -8
  69. package/Input/InputMoneyView.tsx +14 -7
  70. package/Input/InputOTP.tsx +35 -33
  71. package/Input/InputPhoneNumber.tsx +8 -9
  72. package/Input/InputSearch.tsx +4 -6
  73. package/Input/InputTextArea.tsx +4 -0
  74. package/Input/SearchTextTyping.tsx +4 -0
  75. package/Input/SystemTextInput.tsx +7 -2
  76. package/Input/TextTyping.tsx +4 -0
  77. package/Input/common.tsx +26 -12
  78. package/Input/index.tsx +17 -20
  79. package/Input/moneyUtils.ts +10 -4
  80. package/Input/styles.ts +0 -5
  81. package/Input/utils.ts +14 -12
  82. package/Layout/Card.tsx +7 -4
  83. package/Layout/FloatingButton.tsx +4 -0
  84. package/Layout/Item.tsx +6 -2
  85. package/Layout/ItemList.tsx +9 -7
  86. package/Layout/ItemSectionList.tsx +2 -1
  87. package/Layout/ReservedRegions.tsx +25 -25
  88. package/Layout/Screen.tsx +56 -58
  89. package/Layout/Section.tsx +8 -5
  90. package/Layout/TrackingScope.tsx +1 -0
  91. package/Layout/screenContent.tsx +4 -2
  92. package/Layout/screenFooter.tsx +28 -18
  93. package/Layout/screenHeader.tsx +34 -31
  94. package/Layout/screenQuickReturn.tsx +17 -20
  95. package/Layout/screenScroll.ts +4 -7
  96. package/Layout/screenTypes.ts +3 -2
  97. package/Layout/types.ts +2 -4
  98. package/Layout/utils.ts +31 -37
  99. package/Loader/DotLoader.tsx +4 -0
  100. package/Loader/Loader.tsx +4 -0
  101. package/Loader/ProgressBar.tsx +1 -0
  102. package/Loader/Spinner.tsx +4 -0
  103. package/Loader/utils.ts +11 -7
  104. package/Pagination/Dot.tsx +4 -0
  105. package/Pagination/PaginationDot.tsx +1 -0
  106. package/Pagination/PaginationNumber.tsx +5 -3
  107. package/Pagination/PaginationScroll.tsx +5 -0
  108. package/Pagination/PaginationWhiteDot.tsx +1 -0
  109. package/Pagination/index.tsx +4 -0
  110. package/Pagination/types.ts +2 -2
  111. package/Popup/PopupNotify.tsx +44 -48
  112. package/Popup/PopupPromotion.tsx +4 -7
  113. package/Popup/types.ts +4 -4
  114. package/Popup/utils.ts +4 -0
  115. package/Radio/index.tsx +4 -0
  116. package/Skeleton/index.tsx +8 -0
  117. package/Switch/index.tsx +1 -0
  118. package/Tag/index.tsx +5 -1
  119. package/Tag/types.ts +3 -3
  120. package/Text/index.tsx +14 -3
  121. package/Text/styles.ts +1 -1
  122. package/Text/types.ts +1 -6
  123. package/Text/utils.ts +20 -4
  124. package/Title/index.tsx +41 -32
  125. package/ViewBoundary/index.tsx +4 -2
  126. package/index.ts +0 -6
  127. package/package.json +1 -1
  128. package/Application/KitAdoption/AdoptionBadge.tsx +0 -119
  129. package/Application/KitAdoption/AdoptionProbe.tsx +0 -166
  130. package/Application/KitAdoption/AdoptionStore.ts +0 -80
  131. package/Application/KitAdoption/Authorship/utils.ts +0 -137
  132. package/Application/KitAdoption/Slots/utils.ts +0 -68
  133. package/Application/KitAdoption/constants.ts +0 -120
  134. package/Application/KitAdoption/index.ts +0 -12
  135. package/Application/KitAdoption/styles.ts +0 -55
  136. package/Application/KitAdoption/types.ts +0 -61
  137. package/Application/KitAdoption/utils.ts +0 -376
  138. package/Application/__tests__/KitAdoption.integration.test.tsx +0 -253
  139. package/Application/__tests__/KitAdoption.test.ts +0 -283
  140. package/Application/__tests__/KitAdoptionAuthorship.test.ts +0 -132
  141. package/Application/__tests__/KitAdoptionStore.test.ts +0 -110
  142. package/Application/__tests__/KitAdoptionStructural.test.ts +0 -193
  143. package/Application/__tests__/modalLayout.test.ts +0 -128
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,11 +5,15 @@ 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
12
  const SkeletonContext = createContext({ loading: false });
12
- /** @deprecated Max font scale is fixed at 1.5; this context is no longer read. */
13
+ /**
14
+ * @deprecated Not read: wrap the subtree in `ScaleSizeProvider`, which caps the
15
+ * font scale through MiniAppContext. Kept for mini apps built on older kits.
16
+ */
13
17
  const ScaleSizeContext = createContext<{ scaleSizeMaxRate?: number }>({
14
18
  scaleSizeMaxRate: undefined,
15
19
  });
@@ -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
  };
@@ -34,7 +34,8 @@ import { Image } from '../Image';
34
34
  import { Typography } from '../Text/types';
35
35
 
36
36
  /**
37
- * Input default component
37
+ * Vietnamese phone number input with a fixed flag and `+84` prefix, number pad
38
+ * by default, plus clear and trailing icons.
38
39
  */
39
40
  const InputPhoneNumber = forwardRef(
40
41
  (
@@ -146,9 +147,6 @@ const InputPhoneNumber = forwardRef(
146
147
  typography = 'header_m_bold';
147
148
  }
148
149
 
149
- /**
150
- * Render input view
151
- */
152
150
  const renderInputView = () => {
153
151
  const disabledColor = theme.colors.text.disable;
154
152
  const secure = secureTextEntry;
@@ -156,6 +154,11 @@ const InputPhoneNumber = forwardRef(
156
154
  let placeholderColor = theme.colors.text.hint;
157
155
  let iconTintColor = trailingColor ?? theme.colors.text.default;
158
156
  const borderWidth = focused ? 1.5 : 1;
157
+ let secureTextStyle = styles.secureTextLarge;
158
+
159
+ if (size === 'small') {
160
+ secureTextStyle = styles.secureTextSmall;
161
+ }
159
162
 
160
163
  if (disabled) {
161
164
  textColor = disabledColor;
@@ -214,11 +217,7 @@ const InputPhoneNumber = forwardRef(
214
217
  color: textColor,
215
218
  },
216
219
  inputTextStyles,
217
- secure &&
218
- haveValue &&
219
- (size === 'small'
220
- ? styles.secureTextSmall
221
- : styles.secureTextLarge),
220
+ secure && haveValue && secureTextStyle,
222
221
  ]}
223
222
  defaultValue={defaultValue}
224
223
  allowFontScaling={false}
@@ -26,6 +26,10 @@ import styles from './styles';
26
26
  import { Styles } from '../Consts';
27
27
  import SystemTextInput from './SystemTextInput';
28
28
 
29
+ /**
30
+ * Search field with search icon, clear icon, optional trailing icon and a
31
+ * cancel button; with `typingData` it shows an animated placeholder instead.
32
+ */
29
33
  const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
30
34
  {
31
35
  placeholder,
@@ -106,9 +110,6 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
106
110
  };
107
111
  });
108
112
 
109
- /**
110
- * Render the input view
111
- */
112
113
  const renderInputView = () => {
113
114
  if (typingData && typingData?.length > 0) {
114
115
  return (
@@ -149,9 +150,6 @@ const InputSearch: ForwardRefRenderFunction<InputRef, InputSearchProps> = (
149
150
  );
150
151
  };
151
152
 
152
- /**
153
- * Render trailing icon or text
154
- */
155
153
  const renderTrailing = () => {
156
154
  return (
157
155
  <View style={Styles.row}>