@momo-kits/foundation 0.167.1-duo.1 → 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,14 +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 { useDefaultRegularFontFamily } from '../Consts/designSystemFont';
24
26
  import Localize from './Localize';
25
27
  import { Colors, defaultTheme } from '../Consts';
26
28
  import { HeaderLeft } from './Components/HeaderLeft';
@@ -28,6 +30,7 @@ import { HeaderRight } from './Components/HeaderRight';
28
30
  import { HeaderTitle } from './Components/HeaderTitle';
29
31
  import { HeaderBackground } from './Components/HeaderBackground';
30
32
  import { type EdgeInsets } from 'react-native-safe-area-context';
33
+ import { useScreenLayout } from '../Layout';
31
34
 
32
35
  const Stack = createStackNavigator();
33
36
 
@@ -101,10 +104,11 @@ const Navigation: React.FC<any> = ({
101
104
  const navigationRef = useRef<any>(null);
102
105
  const isReady = useRef(false);
103
106
  const navigator = useRef(new Navigator(navigationRef, isReady));
104
- const [showGrid, setShowGrid] = useState(false);
105
- const insets = useSafeAreaInsets();
106
- const defaultRegularFontFamily = useDefaultRegularFontFamily();
107
+ const { headerHeight, headerStatusBarHeight } = useScreenLayout();
107
108
  const syncCanGoBack = useNativeCanGoBack();
109
+ const isWhiteList = isWhiteListDesignSystem(context);
110
+ const fontFamily = isWhiteList ? 'MomoTrustSans' : 'SFProText';
111
+ const navigationFontFamily = isWhiteList ? 'MomoTrustSans-Regular' : 'SFProText';
108
112
  let headerBackground = context?.designSystemConfig?.config?.headerBar;
109
113
  let headerGradient = theme.colors?.gradient;
110
114
 
@@ -120,23 +124,6 @@ const Navigation: React.FC<any> = ({
120
124
  */
121
125
  navigator.current.maxApi = maxApi;
122
126
 
123
- /**
124
- * handle mini language & listen change
125
- * engine only shake to enable grid view
126
- */
127
- useEffect(() => {
128
- const subscription = DeviceEventEmitter.addListener(
129
- 'onChangeGrid',
130
- enable => {
131
- setShowGrid(!!enable);
132
- },
133
- );
134
-
135
- return () => {
136
- subscription?.remove?.();
137
- };
138
- }, []);
139
-
140
127
  /**
141
128
  * translate context method
142
129
  * @param data
@@ -156,6 +143,7 @@ const Navigation: React.FC<any> = ({
156
143
  navigator: navigator.current,
157
144
  theme: {
158
145
  ...theme,
146
+ font: fontFamily,
159
147
  colors: {
160
148
  ...theme.colors,
161
149
  gradient: headerGradient,
@@ -165,7 +153,6 @@ const Navigation: React.FC<any> = ({
165
153
  headerBackground,
166
154
  },
167
155
  },
168
- showGrid,
169
156
  translate,
170
157
  }}
171
158
  >
@@ -183,19 +170,19 @@ const Navigation: React.FC<any> = ({
183
170
  },
184
171
  fonts: {
185
172
  regular: {
186
- fontFamily: defaultRegularFontFamily,
173
+ fontFamily: navigationFontFamily,
187
174
  fontWeight: '400',
188
175
  },
189
176
  medium: {
190
- fontFamily: defaultRegularFontFamily,
177
+ fontFamily: navigationFontFamily,
191
178
  fontWeight: '400',
192
179
  },
193
180
  bold: {
194
- fontFamily: defaultRegularFontFamily,
181
+ fontFamily: navigationFontFamily,
195
182
  fontWeight: '400',
196
183
  },
197
184
  heavy: {
198
- fontFamily: defaultRegularFontFamily,
185
+ fontFamily: navigationFontFamily,
199
186
  fontWeight: '400',
200
187
  },
201
188
  },
@@ -215,8 +202,9 @@ const Navigation: React.FC<any> = ({
215
202
  headerTitleAlign: 'center',
216
203
  headerTintColor: theme.colors.text.default,
217
204
  headerStyle: {
218
- height: 52 + insets.top,
205
+ height: headerHeight,
219
206
  },
207
+ headerStatusBarHeight,
220
208
  }}
221
209
  >
222
210
  <Stack.Screen
@@ -239,11 +227,13 @@ const Navigation: React.FC<any> = ({
239
227
  cardStyle: {
240
228
  backgroundColor: theme.colors.background.default,
241
229
  },
242
- headerLeft: (props: any) => <HeaderLeft {...props} />,
230
+ headerLeft: (props: any) => <HeaderLeft {...props} floating />,
243
231
  headerBackground: (props: any) => (
244
232
  <HeaderBackground {...props} />
245
233
  ),
246
- headerRight: (props: any) => <HeaderRight {...props} />,
234
+ headerRight: (props: any) => (
235
+ <HeaderRight {...props} floating />
236
+ ),
247
237
  headerTintColor: Colors.black_17,
248
238
  ...TransitionPresets.ModalTransition,
249
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,7 +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 { useDefaultRegularFontFamily } from '../Consts/designSystemFont';
7
6
  import Localize from './Localize';
8
7
  import { defaultTheme } from '../Consts';
9
8
  import {
@@ -11,7 +10,7 @@ import {
11
10
  NavigationContainerRef,
12
11
  NavigationIndependentTree,
13
12
  } from '@react-navigation/native';
14
- import { getModalOptions } from './utils';
13
+ import { getModalOptions, isWhiteListDesignSystem } from './utils';
15
14
  import { createStackNavigator } from '@react-navigation/stack';
16
15
  import ModalScreen from './ModalScreen';
17
16
  import { View } from 'react-native';
@@ -43,7 +42,6 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
43
42
  const isReady = useRef(false);
44
43
  const navigator = useRef(new Navigator(navigationRef, isReady, true));
45
44
  const [currentContext, setCurrentContext] = useState({});
46
- const defaultRegularFontFamily = useDefaultRegularFontFamily();
47
45
 
48
46
  useEffect(() => {
49
47
  const sub = maxApi?.observer?.('font_scale_config', (data: any) => {
@@ -80,6 +78,9 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
80
78
  ...context,
81
79
  ...currentContext,
82
80
  };
81
+ const isWhiteList = isWhiteListDesignSystem(mergedContext);
82
+ const fontFamily = isWhiteList ? 'MomoTrustSans' : 'SFProText';
83
+ const navigationFontFamily = isWhiteList ? 'MomoTrustSans-Regular' : 'SFProText';
83
84
 
84
85
  return (
85
86
  <View style={{ height: height ?? '100%', minHeight: 1 }}>
@@ -90,6 +91,7 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
90
91
  navigator: navigator.current,
91
92
  theme: {
92
93
  ...theme,
94
+ font: fontFamily,
93
95
  colors: {
94
96
  ...theme.colors,
95
97
  gradient: headerGradient,
@@ -116,19 +118,19 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
116
118
  },
117
119
  fonts: {
118
120
  regular: {
119
- fontFamily: defaultRegularFontFamily,
121
+ fontFamily: navigationFontFamily,
120
122
  fontWeight: '400',
121
123
  },
122
124
  medium: {
123
- fontFamily: defaultRegularFontFamily,
125
+ fontFamily: navigationFontFamily,
124
126
  fontWeight: '400',
125
127
  },
126
128
  bold: {
127
- fontFamily: defaultRegularFontFamily,
129
+ fontFamily: navigationFontFamily,
128
130
  fontWeight: '400',
129
131
  },
130
132
  heavy: {
131
- fontFamily: defaultRegularFontFamily,
133
+ fontFamily: navigationFontFamily,
132
134
  fontWeight: '400',
133
135
  },
134
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,
@@ -51,7 +51,7 @@ const defaultTheme: Theme = {
51
51
  container: Colors.blue_08,
52
52
  },
53
53
  },
54
- font: 'MomoTrustSans',
54
+ font: 'SFProText',
55
55
  assets: {},
56
56
  };
57
57
 
@@ -105,17 +105,16 @@ const defaultDarkTheme: Theme = {
105
105
  container: Colors.blue_08,
106
106
  },
107
107
  },
108
- font: 'MomoTrustSans',
108
+ font: 'SFProText',
109
109
  assets: {},
110
110
  };
111
111
 
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
  };