@momo-kits/foundation 0.167.1-beta.7 → 0.167.1-duo.2

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.
@@ -0,0 +1,128 @@
1
+ import React, { ReactNode, useContext } from 'react';
2
+ import {
3
+ StyleProp,
4
+ StyleSheet,
5
+ TouchableOpacity,
6
+ View,
7
+ ViewStyle,
8
+ } from 'react-native';
9
+ import { NavigationButtonProps } from '../types';
10
+ import { ApplicationContext } from '../../Context';
11
+ import { Colors, Radius, Shadow, Spacing } from '../../Consts';
12
+ import { Icon } from '../../Icon';
13
+ import { BadgeDot } from '../../Badge';
14
+ import { DuoMetrics } from '../../Layout/utils';
15
+
16
+ const SIDE_BAR_ICON_SIZE = 24;
17
+
18
+ /**
19
+ * glass material for side bar controls: dark glass under light icons (banner headers),
20
+ * light glass otherwise
21
+ */
22
+ const getGlassStyle = (tintColor?: string) =>
23
+ tintColor === Colors.black_01 || tintColor === 'white'
24
+ ? styles.glassDark
25
+ : styles.glassLight;
26
+
27
+ /**
28
+ * 48pt glass circle used for a single control in the iPhone Duo side bar
29
+ */
30
+ const SideBarButton: React.FC<NavigationButtonProps> = ({
31
+ icon,
32
+ tintColor,
33
+ showBadge = false,
34
+ style,
35
+ ...props
36
+ }) => {
37
+ const { theme } = useContext(ApplicationContext);
38
+
39
+ return (
40
+ <View>
41
+ <TouchableOpacity
42
+ {...props}
43
+ style={[styles.control, getGlassStyle(tintColor), style]}
44
+ >
45
+ <Icon
46
+ source={icon}
47
+ color={tintColor ?? theme.colors.text.default}
48
+ size={SIDE_BAR_ICON_SIZE}
49
+ />
50
+ </TouchableOpacity>
51
+ {showBadge && <BadgeDot size={'small'} style={styles.badgeDot} />}
52
+ </View>
53
+ );
54
+ };
55
+
56
+ /**
57
+ * 48pt wide glass capsule grouping related controls, one item per 50pt
58
+ */
59
+ const SideBarGroup: React.FC<{
60
+ tintColor?: string;
61
+ style?: StyleProp<ViewStyle>;
62
+ children: ReactNode;
63
+ }> = ({ tintColor, style, children }) => {
64
+ const count = React.Children.toArray(children).length;
65
+ const height =
66
+ count > 1 ? count * DuoMetrics.itemPitch : DuoMetrics.controlSize;
67
+
68
+ return (
69
+ <View style={[styles.control, getGlassStyle(tintColor), { height }, style]}>
70
+ {children}
71
+ </View>
72
+ );
73
+ };
74
+
75
+ /**
76
+ * icon item inside a SideBarGroup
77
+ */
78
+ const SideBarGroupItem: React.FC<NavigationButtonProps> = ({
79
+ icon,
80
+ tintColor,
81
+ ...props
82
+ }) => {
83
+ const { theme } = useContext(ApplicationContext);
84
+
85
+ return (
86
+ <TouchableOpacity {...props} style={styles.groupItem}>
87
+ <Icon
88
+ source={icon}
89
+ color={tintColor ?? theme.colors.text.default}
90
+ size={SIDE_BAR_ICON_SIZE}
91
+ />
92
+ </TouchableOpacity>
93
+ );
94
+ };
95
+
96
+ const styles = StyleSheet.create({
97
+ control: {
98
+ width: DuoMetrics.controlSize,
99
+ height: DuoMetrics.controlSize,
100
+ borderRadius: Radius.XL,
101
+ justifyContent: 'center',
102
+ alignItems: 'center',
103
+ },
104
+ glassLight: {
105
+ backgroundColor: Colors.black_01 + 'D9',
106
+ borderWidth: StyleSheet.hairlineWidth,
107
+ borderColor: Colors.black_20 + '14',
108
+ ...Shadow.Light,
109
+ },
110
+ glassDark: {
111
+ backgroundColor: Colors.black_20 + '80',
112
+ borderWidth: StyleSheet.hairlineWidth,
113
+ borderColor: Colors.black_01 + '33',
114
+ },
115
+ groupItem: {
116
+ flex: 1,
117
+ alignSelf: 'stretch',
118
+ justifyContent: 'center',
119
+ alignItems: 'center',
120
+ },
121
+ badgeDot: {
122
+ position: 'absolute',
123
+ top: Spacing.XXS,
124
+ right: Spacing.XXS,
125
+ },
126
+ });
127
+
128
+ export { SideBarButton, SideBarGroup, SideBarGroupItem };
@@ -16,7 +16,7 @@ import { Styles } from '../Consts';
16
16
  import Navigation from './Navigation';
17
17
  import { ModalParams } from './types';
18
18
  import BottomSheet from './BottomSheet';
19
- import { runOnJS } from 'react-native-reanimated';
19
+ import { scheduleOnRN } from 'react-native-worklets';
20
20
 
21
21
  const ModalScreen: React.FC<any> = props => {
22
22
  if (props.route?.params?.isBottomSheet) {
@@ -106,7 +106,7 @@ const Modal: React.FC<ModalParams> = props => {
106
106
  }),
107
107
  ]).start(() => {
108
108
  navigator?.pop();
109
- runOnJS(callback)();
109
+ scheduleOnRN(callback);
110
110
  });
111
111
  },
112
112
  [navigator, opacity, scale],
@@ -2,7 +2,6 @@ import React, { useContext, useEffect, useRef, useState } from 'react';
2
2
  import {
3
3
  SafeAreaInsetsContext,
4
4
  SafeAreaProvider,
5
- useSafeAreaInsets,
6
5
  } from 'react-native-safe-area-context';
7
6
  import {
8
7
  NavigationContainer as ReactNavigationContainer,
@@ -13,17 +12,17 @@ import {
13
12
  StackNavigationOptions,
14
13
  TransitionPresets,
15
14
  } from '@react-navigation/stack';
16
- import { DeviceEventEmitter } from 'react-native';
17
15
  import StackScreen from './StackScreen';
18
16
  import ModalScreen from './ModalScreen';
19
17
  import Navigator from './Navigator';
20
- import { getModalOptions, getStackOptions, useNativeCanGoBack } from './utils';
18
+ import {
19
+ getModalOptions,
20
+ getStackOptions,
21
+ isWhiteListDesignSystem,
22
+ useNativeCanGoBack,
23
+ } from './utils';
21
24
  import { NavigationContainerProps } from './types';
22
25
  import { ApplicationContext, MiniAppContext } from '../Context';
23
- import {
24
- useDefaultRegularFontFamily,
25
- useResolvedFontFamily,
26
- } from '../Consts/designSystemFont';
27
26
  import Localize from './Localize';
28
27
  import { Colors, defaultTheme } from '../Consts';
29
28
  import { HeaderLeft } from './Components/HeaderLeft';
@@ -31,6 +30,7 @@ import { HeaderRight } from './Components/HeaderRight';
31
30
  import { HeaderTitle } from './Components/HeaderTitle';
32
31
  import { HeaderBackground } from './Components/HeaderBackground';
33
32
  import { type EdgeInsets } from 'react-native-safe-area-context';
33
+ import { useScreenLayout } from '../Layout';
34
34
 
35
35
  const Stack = createStackNavigator();
36
36
 
@@ -104,11 +104,11 @@ const Navigation: React.FC<any> = ({
104
104
  const navigationRef = useRef<any>(null);
105
105
  const isReady = useRef(false);
106
106
  const navigator = useRef(new Navigator(navigationRef, isReady));
107
- const [showGrid, setShowGrid] = useState(false);
108
- const insets = useSafeAreaInsets();
109
- const defaultRegularFontFamily = useDefaultRegularFontFamily();
110
- const themeFont = useResolvedFontFamily(theme.font);
107
+ const { headerHeight, headerStatusBarHeight } = useScreenLayout();
111
108
  const syncCanGoBack = useNativeCanGoBack();
109
+ const isWhiteList = isWhiteListDesignSystem(context);
110
+ const fontFamily = isWhiteList ? 'MomoTrustSans' : 'SFProText';
111
+ const navigationFontFamily = isWhiteList ? 'MomoTrustSans-Regular' : 'SFProText';
112
112
  let headerBackground = context?.designSystemConfig?.config?.headerBar;
113
113
  let headerGradient = theme.colors?.gradient;
114
114
 
@@ -124,23 +124,6 @@ const Navigation: React.FC<any> = ({
124
124
  */
125
125
  navigator.current.maxApi = maxApi;
126
126
 
127
- /**
128
- * handle mini language & listen change
129
- * engine only shake to enable grid view
130
- */
131
- useEffect(() => {
132
- const subscription = DeviceEventEmitter.addListener(
133
- 'onChangeGrid',
134
- enable => {
135
- setShowGrid(!!enable);
136
- },
137
- );
138
-
139
- return () => {
140
- subscription?.remove?.();
141
- };
142
- }, []);
143
-
144
127
  /**
145
128
  * translate context method
146
129
  * @param data
@@ -160,7 +143,7 @@ const Navigation: React.FC<any> = ({
160
143
  navigator: navigator.current,
161
144
  theme: {
162
145
  ...theme,
163
- font: themeFont,
146
+ font: fontFamily,
164
147
  colors: {
165
148
  ...theme.colors,
166
149
  gradient: headerGradient,
@@ -170,7 +153,6 @@ const Navigation: React.FC<any> = ({
170
153
  headerBackground,
171
154
  },
172
155
  },
173
- showGrid,
174
156
  translate,
175
157
  }}
176
158
  >
@@ -188,19 +170,19 @@ const Navigation: React.FC<any> = ({
188
170
  },
189
171
  fonts: {
190
172
  regular: {
191
- fontFamily: defaultRegularFontFamily,
173
+ fontFamily: navigationFontFamily,
192
174
  fontWeight: '400',
193
175
  },
194
176
  medium: {
195
- fontFamily: defaultRegularFontFamily,
177
+ fontFamily: navigationFontFamily,
196
178
  fontWeight: '400',
197
179
  },
198
180
  bold: {
199
- fontFamily: defaultRegularFontFamily,
181
+ fontFamily: navigationFontFamily,
200
182
  fontWeight: '400',
201
183
  },
202
184
  heavy: {
203
- fontFamily: defaultRegularFontFamily,
185
+ fontFamily: navigationFontFamily,
204
186
  fontWeight: '400',
205
187
  },
206
188
  },
@@ -220,8 +202,9 @@ const Navigation: React.FC<any> = ({
220
202
  headerTitleAlign: 'center',
221
203
  headerTintColor: theme.colors.text.default,
222
204
  headerStyle: {
223
- height: 52 + insets.top,
205
+ height: headerHeight,
224
206
  },
207
+ headerStatusBarHeight,
225
208
  }}
226
209
  >
227
210
  <Stack.Screen
@@ -244,11 +227,13 @@ const Navigation: React.FC<any> = ({
244
227
  cardStyle: {
245
228
  backgroundColor: theme.colors.background.default,
246
229
  },
247
- headerLeft: (props: any) => <HeaderLeft {...props} />,
230
+ headerLeft: (props: any) => <HeaderLeft {...props} floating />,
248
231
  headerBackground: (props: any) => (
249
232
  <HeaderBackground {...props} />
250
233
  ),
251
- headerRight: (props: any) => <HeaderRight {...props} />,
234
+ headerRight: (props: any) => (
235
+ <HeaderRight {...props} floating />
236
+ ),
252
237
  headerTintColor: Colors.black_17,
253
238
  ...TransitionPresets.ModalTransition,
254
239
  }}
@@ -118,9 +118,7 @@ class Navigator {
118
118
  navigate = (name: string) => {
119
119
  if (this.isReady.current) {
120
120
  this.ref.current?.dispatch?.(
121
- CommonActions.navigate({
122
- name,
123
- })
121
+ CommonActions.navigate(name)
124
122
  );
125
123
  }
126
124
  };
@@ -4,7 +4,6 @@ import { useHeaderHeight } from '@react-navigation/elements';
4
4
  import { ScreenTrackingParams } from './types';
5
5
  import Navigation from './Navigation';
6
6
  import { ApplicationContext, MiniAppContext, ScreenContext } from '../Context';
7
- import { GridSystem } from '../Layout';
8
7
  import { version } from '../package.json';
9
8
  import { SwipeBackHaptic, useAppState } from './utils';
10
9
  import { TooltipPortalHost, TooltipPortalProvider } from './TooltipPortal';
@@ -17,7 +16,7 @@ const runAfterInteractions = InteractionManager.runAfterInteractions;
17
16
  * @constructor
18
17
  */
19
18
  const StackScreen: React.FC<any> = props => {
20
- const { showGrid, navigator } = useContext(ApplicationContext);
19
+ const { navigator } = useContext(ApplicationContext);
21
20
  const tracking = useRef<any>({
22
21
  mounted: false,
23
22
  timeoutLoad: undefined,
@@ -42,6 +41,7 @@ const StackScreen: React.FC<any> = props => {
42
41
  });
43
42
  const widgets = useRef<any>([]);
44
43
  const context = useContext<any>(MiniAppContext);
44
+
45
45
  const {
46
46
  screen: Component,
47
47
  options,
@@ -411,10 +411,7 @@ const StackScreen: React.FC<any> = props => {
411
411
  >
412
412
  <TooltipPortalProvider>
413
413
  <Component heightHeader={heightHeader} {...data} />
414
- {showGrid && <GridSystem />}
415
414
  <TooltipPortalHost />
416
- {/* a leaf on purpose: it subscribes to CardAnimationContext, which changes on every
417
- Card render, and must not pull this screen into those renders */}
418
415
  <SwipeBackHaptic />
419
416
  </TooltipPortalProvider>
420
417
  </ScreenContext.Provider>
@@ -3,10 +3,6 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
3
3
  import Navigator from './Navigator';
4
4
  import { WidgetContainerProps } from './types';
5
5
  import { ApplicationContext, MiniAppContext } from '../Context';
6
- import {
7
- resolveFontFamily,
8
- useDefaultRegularFontFamily,
9
- } from '../Consts/designSystemFont';
10
6
  import Localize from './Localize';
11
7
  import { defaultTheme } from '../Consts';
12
8
  import {
@@ -14,7 +10,7 @@ import {
14
10
  NavigationContainerRef,
15
11
  NavigationIndependentTree,
16
12
  } from '@react-navigation/native';
17
- import { getModalOptions } from './utils';
13
+ import { getModalOptions, isWhiteListDesignSystem } from './utils';
18
14
  import { createStackNavigator } from '@react-navigation/stack';
19
15
  import ModalScreen from './ModalScreen';
20
16
  import { View } from 'react-native';
@@ -46,7 +42,6 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
46
42
  const isReady = useRef(false);
47
43
  const navigator = useRef(new Navigator(navigationRef, isReady, true));
48
44
  const [currentContext, setCurrentContext] = useState({});
49
- const defaultRegularFontFamily = useDefaultRegularFontFamily();
50
45
 
51
46
  useEffect(() => {
52
47
  const sub = maxApi?.observer?.('font_scale_config', (data: any) => {
@@ -83,6 +78,9 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
83
78
  ...context,
84
79
  ...currentContext,
85
80
  };
81
+ const isWhiteList = isWhiteListDesignSystem(mergedContext);
82
+ const fontFamily = isWhiteList ? 'MomoTrustSans' : 'SFProText';
83
+ const navigationFontFamily = isWhiteList ? 'MomoTrustSans-Regular' : 'SFProText';
86
84
 
87
85
  return (
88
86
  <View style={{ height: height ?? '100%', minHeight: 1 }}>
@@ -93,11 +91,7 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
93
91
  navigator: navigator.current,
94
92
  theme: {
95
93
  ...theme,
96
- font: resolveFontFamily(
97
- theme.font,
98
- mergedContext?.designSystemConfig?.isWhiteListDesignSystem ===
99
- true,
100
- ),
94
+ font: fontFamily,
101
95
  colors: {
102
96
  ...theme.colors,
103
97
  gradient: headerGradient,
@@ -124,19 +118,19 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
124
118
  },
125
119
  fonts: {
126
120
  regular: {
127
- fontFamily: defaultRegularFontFamily,
121
+ fontFamily: navigationFontFamily,
128
122
  fontWeight: '400',
129
123
  },
130
124
  medium: {
131
- fontFamily: defaultRegularFontFamily,
125
+ fontFamily: navigationFontFamily,
132
126
  fontWeight: '400',
133
127
  },
134
128
  bold: {
135
- fontFamily: defaultRegularFontFamily,
129
+ fontFamily: navigationFontFamily,
136
130
  fontWeight: '400',
137
131
  },
138
132
  heavy: {
139
- fontFamily: defaultRegularFontFamily,
133
+ fontFamily: navigationFontFamily,
140
134
  fontWeight: '400',
141
135
  },
142
136
  },
@@ -117,7 +117,6 @@ export type Context = {
117
117
  [key: string]: any;
118
118
  theme: Theme;
119
119
  navigator?: NavigatorProps;
120
- showGrid?: boolean;
121
120
  translate?: (data: string | { vi: string; en: string }) => string;
122
121
  };
123
122
 
@@ -258,6 +257,11 @@ export interface HeaderBackProps extends NavigationButtonProps {
258
257
  preventBack?: PopupNotifyProps;
259
258
  onPressLeftHeader?: () => void;
260
259
  onBackHandler?: (goBack: () => void) => void;
260
+ /**
261
+ * Optional. If `true`, the button floats into the iPhone Duo side bar while the
262
+ * device shows one. Set when the button is rendered by the navigation header.
263
+ */
264
+ floating?: boolean;
261
265
  }
262
266
 
263
267
  export type HeaderBackgroundProps = {
@@ -73,8 +73,8 @@ const getStackOptions = (): StackNavigationOptions => {
73
73
  left: Spacing.S,
74
74
  },
75
75
  headerBackground: (props: any) => <HeaderBackground {...props} />,
76
- headerLeft: (props: any) => <HeaderLeft {...props} />,
77
- headerRight: (props: any) => <HeaderRight {...props} />,
76
+ headerLeft: (props: any) => <HeaderLeft {...props} floating />,
77
+ headerRight: (props: any) => <HeaderRight {...props} floating />,
78
78
  headerTintColor: Colors.black_17,
79
79
  gestureEnabled: Platform.OS === 'ios',
80
80
  ...TransitionPresets.SlideFromRightIOS,
@@ -129,6 +129,7 @@ const getOptions = (
129
129
  preventBack={params.preventBack}
130
130
  onPressLeftHeader={params.onPressLeftHeader}
131
131
  onBackHandler={params.onBackHandler}
132
+ floating
132
133
  />
133
134
  );
134
135
  if (params.hiddenBack) {
@@ -167,7 +168,9 @@ const getOptions = (
167
168
  ...options,
168
169
  ...{
169
170
  headerRight: (props: any) => {
170
- return <HeaderRight {...props} headerRight={params.headerRight} />;
171
+ return (
172
+ <HeaderRight {...props} headerRight={params.headerRight} floating />
173
+ );
171
174
  },
172
175
  },
173
176
  };
@@ -475,6 +478,9 @@ const SwipeBackHaptic = () => {
475
478
  return null;
476
479
  };
477
480
 
481
+ const isWhiteListDesignSystem = (context: any): boolean =>
482
+ context?.designSystemConfig?.isWhiteListDesignSystem === true;
483
+
478
484
  export {
479
485
  getStackOptions,
480
486
  getModalOptions,
@@ -487,4 +493,5 @@ export {
487
493
  useNativeCanGoBack,
488
494
  useNormalizedSharedValue,
489
495
  SwipeBackHaptic,
496
+ isWhiteListDesignSystem,
490
497
  };
package/Consts/index.ts CHANGED
@@ -2,4 +2,3 @@ export * from './colors+spacing+radius';
2
2
  export * from './theme';
3
3
  export * from './styles';
4
4
  export const GRIDVIEW = false;
5
- export * from './designSystemFont';
package/Consts/theme.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type {Context, Theme} from '../Application/types';
2
- import {Colors} from './colors+spacing+radius';
1
+ import type { Context, Theme } from '../Application/types';
2
+ import { Colors } from './colors+spacing+radius';
3
3
 
4
4
  const defaultTheme: Theme = {
5
5
  dark: false,
@@ -112,10 +112,9 @@ const defaultDarkTheme: Theme = {
112
112
  const defaultContext: Context = {
113
113
  theme: defaultTheme,
114
114
  navigator: undefined,
115
- showGrid: false,
116
115
  translate: () => {
117
116
  return '';
118
117
  },
119
118
  };
120
119
 
121
- export {defaultContext, defaultTheme, defaultDarkTheme};
120
+ export { defaultContext, defaultTheme, defaultDarkTheme };
package/Input/Input.tsx CHANGED
@@ -6,9 +6,8 @@ import React, {
6
6
  useState,
7
7
  } from 'react';
8
8
  import {
9
- NativeSyntheticEvent,
10
9
  TextInput,
11
- TextInputFocusEventData,
10
+ TextInputFocusEvent,
12
11
  TouchableOpacity,
13
12
  View,
14
13
  } from 'react-native';
@@ -102,12 +101,12 @@ const Input = forwardRef(
102
101
  onChangeText?.(text);
103
102
  };
104
103
 
105
- const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
104
+ const _onFocus = (e: TextInputFocusEvent) => {
106
105
  setFocused(true);
107
106
  onFocus?.(e);
108
107
  };
109
108
 
110
- const _onBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
109
+ const _onBlur = (e: TextInputFocusEvent) => {
111
110
  setFocused(false);
112
111
  onBlur?.(e);
113
112
  };
@@ -9,11 +9,9 @@ import React, {
9
9
  useState,
10
10
  } from 'react';
11
11
  import {
12
- NativeSyntheticEvent,
13
- Platform,
14
12
  TextInput,
15
- TextInputFocusEventData,
16
- TextInputSelectionChangeEventData,
13
+ TextInputFocusEvent,
14
+ TextInputSelectionChangeEvent,
17
15
  TouchableOpacity,
18
16
  View,
19
17
  } from 'react-native';
@@ -57,13 +55,10 @@ const caretFromDigitIndex = (display: string, digitIndex: number) => {
57
55
  return 0;
58
56
  };
59
57
 
60
- // Finds which range of `old` was replaced to produce `next`. `selection` is
61
- // the pre-edit selection on iOS and already the post-edit caret on Android,
62
- // where onSelectionChange fires before onChangeText.
63
58
  const detectEdit = (old: string, next: string, selection: Selection): Edit => {
64
59
  const { start, end } = selection;
65
60
  const removed = old.length - next.length;
66
- const preEdit: Edit[] = [
61
+ const candidates: Edit[] = [
67
62
  {
68
63
  start,
69
64
  end,
@@ -72,26 +67,6 @@ const detectEdit = (old: string, next: string, selection: Selection): Edit => {
72
67
  { start: start - removed, end: start, inserted: '' },
73
68
  { start, end: start + removed, inserted: '' },
74
69
  ];
75
- const postEdit: Edit[] = [];
76
- const tail = next.length - start;
77
- if (start === end && tail <= old.length) {
78
- let prefix = 0;
79
- while (
80
- prefix < Math.min(start, old.length - tail) &&
81
- old[prefix] === next[prefix]
82
- ) {
83
- prefix += 1;
84
- }
85
- postEdit.push({
86
- start: prefix,
87
- end: old.length - tail,
88
- inserted: next.slice(prefix, start),
89
- });
90
- }
91
- const candidates =
92
- Platform.OS === 'android'
93
- ? [...postEdit, ...preEdit]
94
- : [...preEdit, ...postEdit];
95
70
  const match = candidates.find(
96
71
  e =>
97
72
  e.start >= 0 &&
@@ -196,9 +171,8 @@ const InputMoney = forwardRef(
196
171
  const selectionRef = useRef<Selection>({ start: 0, end: 0 });
197
172
  // Caret to push to native once the committed text is rendered.
198
173
  const pendingCaretRef = useRef<number | null>(null);
199
- // Caret the native input reports for the raw, unformatted text. On iOS that
200
- // event lands after onChangeText and must not override the formatted caret.
201
- const rawCaretRef = useRef<number | null>(null);
174
+ const pendingSelectionRef = useRef<Selection | null>(null);
175
+ const editingRef = useRef(false);
202
176
  const inputRef = useRef<TextInput>(null);
203
177
  const componentName = 'InputMoney';
204
178
 
@@ -265,10 +239,13 @@ const InputMoney = forwardRef(
265
239
  const _onChangeText = (text: string) => {
266
240
  let digits = '';
267
241
  let caretDigit = 0;
268
- rawCaretRef.current = 0;
242
+ pendingSelectionRef.current = null;
243
+ editingRef.current = true;
244
+ setTimeout(() => {
245
+ editingRef.current = false;
246
+ });
269
247
  if (text) {
270
248
  const edit = detectEdit(displayRef.current, text, selectionRef.current);
271
- rawCaretRef.current = edit.start + edit.inserted.length;
272
249
  ({ digits, caretDigit } = applyEdit(
273
250
  displayRef.current,
274
251
  edit,
@@ -281,31 +258,34 @@ const InputMoney = forwardRef(
281
258
  };
282
259
 
283
260
  // Keeps the caret out of the currency suffix.
284
- const _onSelectionChange = (
285
- e: NativeSyntheticEvent<TextInputSelectionChangeEventData>,
286
- ) => {
261
+ const _onSelectionChange = (e: TextInputSelectionChangeEvent) => {
287
262
  props.onSelectionChange?.(e);
288
- const { start, end } = e.nativeEvent.selection;
289
- const rawCaret = rawCaretRef.current;
290
- rawCaretRef.current = null;
291
- if (start === end && start === rawCaret) {
292
- return;
293
- }
294
- const display = displayRef.current;
295
- const max = display ? display.length - currency.length : 0;
296
- const clamped = { start: Math.min(start, max), end: Math.min(end, max) };
297
- selectionRef.current = clamped;
298
- if (clamped.start !== start || clamped.end !== end) {
299
- inputRef.current?.setSelection(clamped.start, clamped.end);
300
- }
263
+ if (editingRef.current) return;
264
+ const selection = e.nativeEvent.selection;
265
+ pendingSelectionRef.current = selection;
266
+ setTimeout(() => {
267
+ if (pendingSelectionRef.current !== selection) return;
268
+ pendingSelectionRef.current = null;
269
+ const { start, end } = selection;
270
+ const display = displayRef.current;
271
+ const max = display ? display.length - currency.length : 0;
272
+ const clamped = {
273
+ start: Math.min(start, max),
274
+ end: Math.min(end, max),
275
+ };
276
+ selectionRef.current = clamped;
277
+ if (clamped.start !== start || clamped.end !== end) {
278
+ inputRef.current?.setSelection(clamped.start, clamped.end);
279
+ }
280
+ });
301
281
  };
302
282
 
303
- const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
283
+ const _onFocus = (e: TextInputFocusEvent) => {
304
284
  setFocused(true);
305
285
  onFocus?.(e);
306
286
  };
307
287
 
308
- const _onBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
288
+ const _onBlur = (e: TextInputFocusEvent) => {
309
289
  setFocused(false);
310
290
  onBlur?.(e);
311
291
  };