@momo-kits/foundation 0.167.1-beta.2 → 0.167.1-beta.3

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.
@@ -58,13 +58,11 @@ export default function BottomTabBar({
58
58
  const { theme } = useContext(ApplicationContext);
59
59
  const { routes } = state;
60
60
  const widthDevice = useWindowDimensions()?.width;
61
- const insets = useSafeAreaInsets();
62
61
  const useFloating = floatingButton && state.routes.length % 2 === 0;
63
- const itemWidth =
64
- (widthDevice - insets.left - insets.right) /
65
- (state.routes.length + (useFloating ? 1 : 0));
62
+ const itemWidth = widthDevice / (state.routes.length + (useFloating ? 1 : 0));
66
63
  // const buildLink = useLinkBuilder();
67
64
  const indicatorAnimated = useSharedValue(0);
65
+ const insets = useSafeAreaInsets();
68
66
 
69
67
  const indicatorStyle = useAnimatedStyle(() => ({
70
68
  width: itemWidth,
@@ -248,8 +246,7 @@ export default function BottomTabBar({
248
246
  },
249
247
  {
250
248
  paddingBottom: paddingBottom,
251
- paddingLeft: insets.left,
252
- paddingRight: insets.right,
249
+ paddingHorizontal: Math.max(insets.left, insets.right),
253
250
  },
254
251
  style,
255
252
  ]}
@@ -1,6 +1,6 @@
1
1
  import React, { Ref, useContext } from 'react';
2
2
  import LinearGradient from 'react-native-linear-gradient';
3
- import { Platform, StyleSheet, View } from 'react-native';
3
+ import { Dimensions, Platform, StyleSheet, View } from 'react-native';
4
4
  import Animated, {
5
5
  Extrapolation,
6
6
  interpolate,
@@ -18,11 +18,12 @@ import {
18
18
  AnimatedCompatValue,
19
19
  useNormalizedSharedValue,
20
20
  } from '../utils';
21
- import { useHorizontalLayout } from '../../Layout/utils';
22
21
 
23
22
  const SCREEN_PADDING = 12;
24
23
  const BACK_WIDTH = 28;
25
24
 
25
+ const { width: screenWidth } = Dimensions.get('window');
26
+
26
27
  const HeaderExtendHeader: React.FC<{
27
28
  headerType?: HeaderType;
28
29
  animatedValue: AnimatedCompatValue;
@@ -51,8 +52,6 @@ const HeaderExtendHeader: React.FC<{
51
52
  const gradientColor = customGradientColor ?? theme.colors.gradient;
52
53
  const headerBackground = customBackground ?? theme.assets?.headerBackground;
53
54
  const leftPosition = inputSearchProps?.leftPosition || BACK_WIDTH + 20;
54
- const layout = useHorizontalLayout(SCREEN_PADDING);
55
- const { insets } = layout;
56
55
 
57
56
  let useShadowHeader = useShadowHeaderProp;
58
57
  if (inputSearchProps && Platform.OS === 'android') {
@@ -80,7 +79,7 @@ const HeaderExtendHeader: React.FC<{
80
79
  translateX: interpolate(
81
80
  sharedValue.value,
82
81
  [0, 100],
83
- [layout.marginLeft, leftPosition + insets.left],
82
+ [SCREEN_PADDING, leftPosition],
84
83
  Extrapolation.CLAMP,
85
84
  ),
86
85
  },
@@ -89,8 +88,8 @@ const HeaderExtendHeader: React.FC<{
89
88
  sharedValue.value,
90
89
  [0, 100],
91
90
  [
92
- layout.width,
93
- layout.width + SCREEN_PADDING - leftPosition - headerRightWidth,
91
+ screenWidth - SCREEN_PADDING * 2,
92
+ screenWidth - leftPosition - 12 - headerRightWidth,
94
93
  ],
95
94
  Extrapolation.CLAMP,
96
95
  ),
@@ -1,13 +1,17 @@
1
1
  import { Colors, Radius, Spacing, Styles } from '../../Consts';
2
2
  import { InputRef, InputSearch } from '../../Input';
3
- import { StyleSheet, TouchableOpacity, View } from 'react-native';
3
+ import {
4
+ Dimensions,
5
+ StyleSheet,
6
+ TouchableOpacity,
7
+ View,
8
+ } from 'react-native';
4
9
  import Animated, { useAnimatedStyle } from 'react-native-reanimated';
5
10
  import React, { useContext } from 'react';
6
11
  import { SearchHeaderProps } from '../types';
7
12
  import { ApplicationContext, MiniAppContext } from '../../Context';
8
13
  import { Text } from '../../Text';
9
14
  import { useNormalizedSharedValue } from '../utils';
10
- import { useHorizontalLayout } from '../../Layout/utils';
11
15
 
12
16
  const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
13
17
  (
@@ -23,7 +27,7 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
23
27
  const { theme, navigator } = useContext(ApplicationContext);
24
28
  const context = useContext<any>(MiniAppContext);
25
29
  const BACK_WIDTH = 28;
26
- const { width: safeWidth } = useHorizontalLayout(0);
30
+ const { width: screenWidth } = Dimensions.get('window');
27
31
 
28
32
  const sharedValue = useNormalizedSharedValue(animatedValue);
29
33
  const leftPosition = props?.leftPosition ?? BACK_WIDTH + 20;
@@ -73,7 +77,7 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
73
77
  style={[
74
78
  Styles.flex,
75
79
  renderButtons && {
76
- maxWidth: safeWidth - leftPosition - 12 - headerRightWidth,
80
+ maxWidth: screenWidth - leftPosition - 12 - headerRightWidth,
77
81
  },
78
82
  ]}
79
83
  >
@@ -4,16 +4,13 @@ import React, {
4
4
  useContext,
5
5
  useEffect,
6
6
  useImperativeHandle,
7
- useLayoutEffect,
8
7
  useRef,
9
8
  useState,
10
9
  } from 'react';
11
10
  import {
12
11
  NativeSyntheticEvent,
13
- Platform,
14
12
  TextInput,
15
13
  TextInputFocusEventData,
16
- TextInputSelectionChangeEventData,
17
14
  TouchableOpacity,
18
15
  View,
19
16
  } from 'react-native';
@@ -35,124 +32,9 @@ import {
35
32
  } from './common';
36
33
  import { InputMoneyProps } from './index';
37
34
  import styles from './styles';
35
+ import { formatNumberToMoney } from './utils';
38
36
  import SystemTextInput from './SystemTextInput';
39
37
 
40
- type Selection = { start: number; end: number };
41
- type Edit = { start: number; end: number; inserted: string };
42
-
43
- const countDigits = (text: string) => text.replace(/\D/g, '').length;
44
-
45
- const groupDigits = (digits: string) =>
46
- digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
47
-
48
- // Caret position right after the `digitIndex`-th digit of `display`.
49
- const caretFromDigitIndex = (display: string, digitIndex: number) => {
50
- let count = 0;
51
- for (let i = 0; i < display.length && count < digitIndex; i++) {
52
- if (/\d/.test(display[i])) {
53
- count += 1;
54
- if (count === digitIndex) return i + 1;
55
- }
56
- }
57
- return 0;
58
- };
59
-
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
- const detectEdit = (old: string, next: string, selection: Selection): Edit => {
64
- const { start, end } = selection;
65
- const removed = old.length - next.length;
66
- const preEdit: Edit[] = [
67
- {
68
- start,
69
- end,
70
- inserted: next.slice(start, next.length - (old.length - end)),
71
- },
72
- { start: start - removed, end: start, inserted: '' },
73
- { start, end: start + removed, inserted: '' },
74
- ];
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
- const match = candidates.find(
96
- e =>
97
- e.start >= 0 &&
98
- e.end >= e.start &&
99
- e.end <= old.length &&
100
- old.slice(0, e.start) + e.inserted + old.slice(e.end) === next,
101
- );
102
- if (match) return match;
103
-
104
- let prefix = 0;
105
- while (
106
- prefix < Math.min(old.length, next.length) &&
107
- old[prefix] === next[prefix]
108
- ) {
109
- prefix += 1;
110
- }
111
- let suffix = 0;
112
- while (
113
- suffix < Math.min(old.length, next.length) - prefix &&
114
- old[old.length - 1 - suffix] === next[next.length - 1 - suffix]
115
- ) {
116
- suffix += 1;
117
- }
118
- return {
119
- start: prefix,
120
- end: old.length - suffix,
121
- inserted: next.slice(prefix, next.length - suffix),
122
- };
123
- };
124
-
125
- // Applies `edit` to the digits of `display`. Separators and currency are never
126
- // deleted: removing a separator deletes the digit before it instead, removing
127
- // only the currency leaves the digits untouched.
128
- const applyEdit = (display: string, edit: Edit, digitLimit: number) => {
129
- const digits = display.replace(/\D/g, '');
130
- let left = countDigits(display.slice(0, edit.start));
131
- let removed = countDigits(display.slice(edit.start, edit.end));
132
- const inserted = edit.inserted.replace(/\D/g, '');
133
- if (
134
- !removed &&
135
- !inserted &&
136
- left > 0 &&
137
- display.slice(edit.start, edit.end).includes('.')
138
- ) {
139
- left -= 1;
140
- removed = 1;
141
- }
142
- const next = digits.slice(0, left) + inserted + digits.slice(left + removed);
143
- const trimmed = next.replace(/^0+(?=\d)/, '');
144
- if ((!removed && !inserted) || trimmed.length > digitLimit) {
145
- return { digits, caretDigit: left };
146
- }
147
- return {
148
- digits: trimmed,
149
- caretDigit: Math.max(
150
- 0,
151
- left + inserted.length - (next.length - trimmed.length),
152
- ),
153
- };
154
- };
155
-
156
38
  const InputMoney = forwardRef(
157
39
  (
158
40
  {
@@ -191,14 +73,7 @@ const InputMoney = forwardRef(
191
73
  const [focused, setFocused] = useState(false);
192
74
  const [displayValue, setDisplayValue] = useState('');
193
75
  const [numericValue, setNumericValue] = useState('');
194
- const [commitCount, setCommitCount] = useState(0);
195
- const displayRef = useRef('');
196
- const selectionRef = useRef<Selection>({ start: 0, end: 0 });
197
- // Caret to push to native once the committed text is rendered.
198
- 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);
76
+ const [lastDisplayValue, setLastDisplayValue] = useState('');
202
77
  const inputRef = useRef<TextInput>(null);
203
78
  const componentName = 'InputMoney';
204
79
 
@@ -216,36 +91,25 @@ const InputMoney = forwardRef(
216
91
 
217
92
  useEffect(() => {
218
93
  if (value !== undefined && typeof value === 'string') {
219
- const cleanValue = value.replace(/\D/g, '').replace(/^0+(?=\d)/, '');
94
+ const cleanValue = value.replace(/\D/g, '');
220
95
  if (cleanValue !== numericValue) {
221
- commit(cleanValue, cleanValue.length);
96
+ setNumericValue(cleanValue);
97
+ updateDisplayValue(cleanValue);
222
98
  }
223
99
  }
224
100
  }, [value]);
225
101
 
226
- // Always set the caret imperatively: a controlled `selection` prop is only
227
- // sent to native when it differs from the last native selection, so it is
228
- // skipped whenever a stale selection event already reported the same value.
229
- useLayoutEffect(() => {
230
- const caret = pendingCaretRef.current;
231
- if (caret === null) return;
232
- pendingCaretRef.current = null;
233
- if (inputRef.current?.isFocused()) {
234
- inputRef.current.setSelection(caret, caret);
235
- }
236
- }, [commitCount]);
102
+ useEffect(() => {
103
+ setLastDisplayValue(displayValue);
104
+ }, [displayValue]);
237
105
 
238
- const commit = (digits: string, caretDigit: number) => {
239
- const display = digits ? `${groupDigits(digits)}${currency}` : '';
240
- const caret = caretFromDigitIndex(display, caretDigit);
241
- const changed = digits !== numericValue;
242
- displayRef.current = display;
243
- selectionRef.current = { start: caret, end: caret };
244
- pendingCaretRef.current = caret;
245
- setCommitCount(count => count + 1);
246
- setDisplayValue(display);
247
- setNumericValue(digits);
248
- return changed;
106
+ const updateDisplayValue = (numeric: string) => {
107
+ if (!numeric) {
108
+ setDisplayValue('');
109
+ return;
110
+ }
111
+ const formatted = formatNumberToMoney(Number(numeric));
112
+ setDisplayValue(formatted ? `${formatted}${currency}` : '');
249
113
  };
250
114
 
251
115
  const parseInputText = (text: string): string => {
@@ -255,49 +119,47 @@ const InputMoney = forwardRef(
255
119
  return textWithoutCurrency.replace(/[^\d]/g, '');
256
120
  };
257
121
 
122
+ const updateNumericValue = (val: string) => {
123
+ let next = val.replace(/[^\d]/g, '');
124
+ if (next.length > digitLimit) next = next.slice(0, digitLimit);
125
+ if (/^\d*$/.test(next)) {
126
+ setNumericValue(next);
127
+ updateDisplayValue(next);
128
+ onChangeText?.(next);
129
+ }
130
+ };
131
+
258
132
  const onClearText = () => {
259
133
  inputRef?.current?.clear();
260
134
  inputRef.current?.setNativeProps({ text: '' });
261
- commit('', 0);
135
+ setDisplayValue('');
136
+ setNumericValue('');
262
137
  onChangeText?.('');
263
138
  };
264
139
 
265
140
  const _onChangeText = (text: string) => {
266
- let digits = '';
267
- let caretDigit = 0;
268
- rawCaretRef.current = 0;
269
- if (text) {
270
- const edit = detectEdit(displayRef.current, text, selectionRef.current);
271
- rawCaretRef.current = edit.start + edit.inserted.length;
272
- ({ digits, caretDigit } = applyEdit(
273
- displayRef.current,
274
- edit,
275
- digitLimit,
276
- ));
277
- }
278
- if (commit(digits, caretDigit)) {
279
- onChangeText?.(digits);
280
- }
281
- };
282
-
283
- // Keeps the caret out of the currency suffix.
284
- const _onSelectionChange = (
285
- e: NativeSyntheticEvent<TextInputSelectionChangeEventData>,
286
- ) => {
287
- 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) {
141
+ if (!text) {
142
+ setDisplayValue('');
143
+ setNumericValue('');
144
+ onChangeText?.('');
292
145
  return;
293
146
  }
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);
147
+ if (text.length < lastDisplayValue.length) {
148
+ const lastChar = lastDisplayValue.charAt(lastDisplayValue.length - 1);
149
+ const isRemovingCurrency =
150
+ lastDisplayValue.endsWith(currency) || lastChar === ' ';
151
+ const isRemovingDot = lastChar === '.';
152
+ if (isRemovingCurrency || isRemovingDot) {
153
+ const currentNumericValue = numericValue;
154
+ if (currentNumericValue.length > 0) {
155
+ const newNumericValue = currentNumericValue.slice(0, -1);
156
+ updateNumericValue(newNumericValue);
157
+ return;
158
+ }
159
+ }
300
160
  }
161
+ const newNumericValue = parseInputText(text);
162
+ updateNumericValue(newNumericValue);
301
163
  };
302
164
 
303
165
  const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
@@ -312,16 +174,13 @@ const InputMoney = forwardRef(
312
174
 
313
175
  const _setText = (text: string) => {
314
176
  if (!text) {
315
- commit('', 0);
177
+ setDisplayValue('');
178
+ setNumericValue('');
316
179
  onChangeText?.('');
317
180
  return;
318
181
  }
319
- const digits = parseInputText(text)
320
- .replace(/^0+(?=\d)/, '')
321
- .slice(0, digitLimit);
322
- if (commit(digits, digits.length)) {
323
- onChangeText?.(digits);
324
- }
182
+ const newNumericValue = parseInputText(text);
183
+ updateNumericValue(newNumericValue);
325
184
  };
326
185
 
327
186
  useImperativeHandle(ref, () => {
@@ -388,7 +247,6 @@ const InputMoney = forwardRef(
388
247
  textBreakStrategy="highQuality"
389
248
  value={displayValue}
390
249
  onChangeText={_onChangeText}
391
- onSelectionChange={_onSelectionChange}
392
250
  onFocus={_onFocus}
393
251
  onBlur={_onBlur}
394
252
  placeholder={placeholder}
package/Layout/Card.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import React, { useContext } from 'react';
2
- import { View, ViewProps } from 'react-native';
3
- import { useGridSystem, useHorizontalLayout } from './utils';
2
+ import { Dimensions, View, ViewProps } from 'react-native';
3
+ import { useGridSystem } from './utils';
4
4
  import { GridContextProps } from './types';
5
5
  import { GridContext, GridSystem } from './index';
6
6
  import { Image } from '../Image';
@@ -32,8 +32,7 @@ const Card: React.FC<CardProps> = ({
32
32
 
33
33
  const gutterSize = 8;
34
34
  const margin = 12;
35
- const layout = useHorizontalLayout(margin);
36
- const widthSection = layout.width;
35
+ const widthSection = Dimensions.get('window').width - margin * 2;
37
36
  let sizePerSpan =
38
37
  (widthSection - margin * 2 - gutterSize * (numberOfColumns - 1)) /
39
38
  numberOfColumns -
@@ -77,8 +76,7 @@ const Card: React.FC<CardProps> = ({
77
76
  styles.card,
78
77
  {
79
78
  width: widthSection,
80
- marginLeft: layout.marginLeft,
81
- marginRight: layout.marginRight,
79
+ marginHorizontal: margin,
82
80
  gap: gutterSize,
83
81
  },
84
82
  !!backgroundImage && { backgroundColor: undefined },
@@ -19,7 +19,6 @@ import {
19
19
  AnimatedCompatValue,
20
20
  useNormalizedSharedValue,
21
21
  } from '../Application/utils';
22
- import { useHorizontalLayout } from './utils';
23
22
 
24
23
  export interface FloatingButtonProps {
25
24
  label?: string;
@@ -57,7 +56,6 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
57
56
  const lastOffset = useSharedValue(0);
58
57
  const lastDirection = useSharedValue<string | null>(null);
59
58
  const [showText, setShowText] = useState(true);
60
- const { marginRight } = useHorizontalLayout(12);
61
59
 
62
60
  useEffect(() => {
63
61
  if (!label) return;
@@ -121,7 +119,7 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
121
119
  style={[
122
120
  styles.container,
123
121
  {
124
- right: position === 'right' ? marginRight : undefined,
122
+ right: position === 'right' ? 12 : undefined,
125
123
  alignSelf: position === 'center' ? 'center' : 'flex-end',
126
124
  bottom,
127
125
  },
@@ -138,7 +136,7 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
138
136
  style={[
139
137
  styles.container,
140
138
  {
141
- right: position === 'right' ? marginRight : undefined,
139
+ right: position === 'right' ? 12 : undefined,
142
140
  alignSelf: position === 'center' ? 'center' : 'flex-end',
143
141
  height: size === 'small' ? 36 : 48,
144
142
  backgroundColor: theme.colors.primary,
@@ -1,6 +1,6 @@
1
1
  import React, { useContext } from 'react';
2
- import { useWindowDimensions, View, ViewProps } from 'react-native';
3
- import { useGridSystem, useHorizontalLayout } from './utils';
2
+ import { Dimensions, View, ViewProps } from 'react-native';
3
+ import { useGridSystem } from './utils';
4
4
  import { GridContextProps } from './types';
5
5
  import { GridContext } from './index';
6
6
  import { ApplicationContext, MiniAppContext } from '../Context';
@@ -38,9 +38,14 @@ const Section: React.FC<SectionProps> = ({
38
38
  const { numberOfColumns } = useGridSystem();
39
39
 
40
40
  const gutterSize = 12;
41
- const layout = useHorizontalLayout(12);
42
- const { width: windowWidth } = useWindowDimensions();
43
- const widthSection = useMargin ? layout.width : windowWidth;
41
+ let margin = 0;
42
+ if (useMargin) {
43
+ margin = 12;
44
+ }
45
+ let widthSection = Dimensions.get('window').width;
46
+ if (useMargin) {
47
+ widthSection = widthSection - margin * 2;
48
+ }
44
49
 
45
50
  const sizePerSpan =
46
51
  (widthSection - gutterSize * (numberOfColumns - 1)) / numberOfColumns -
@@ -72,8 +77,7 @@ const Section: React.FC<SectionProps> = ({
72
77
  styles.protectedStyle,
73
78
  {
74
79
  width: widthSection,
75
- marginLeft: useMargin ? layout.marginLeft : 0,
76
- marginRight: useMargin ? layout.marginRight : 0,
80
+ marginHorizontal: margin,
77
81
  flexDirection: 'row',
78
82
  flexWrap: 'wrap',
79
83
  gap: gutterSize,
package/Layout/utils.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import React, { Fragment, ReactElement } from 'react';
2
2
  import { useWindowDimensions } from 'react-native';
3
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
3
 
5
4
  /**
6
5
  * validate children type
@@ -45,18 +44,4 @@ const useGridSystem = () => {
45
44
  return { numberOfColumns };
46
45
  };
47
46
 
48
- // Side insets are 0 on portrait iPhones; iPhone Duo and landscape report them
49
- const useHorizontalLayout = (margin: number) => {
50
- const { width } = useWindowDimensions();
51
- const insets = useSafeAreaInsets();
52
- const marginLeft = margin + insets.left;
53
- const marginRight = margin + insets.right;
54
- return {
55
- marginLeft,
56
- marginRight,
57
- width: width - marginLeft - marginRight,
58
- insets,
59
- };
60
- };
61
-
62
- export { useGridSystem, useHorizontalLayout, validateChildren };
47
+ export { useGridSystem, validateChildren };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.167.1-beta.2",
3
+ "version": "0.167.1-beta.3",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},