@momo-kits/foundation 0.167.1-beta.3 → 0.167.1-duo.1
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,11 +58,13 @@ 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();
|
|
61
62
|
const useFloating = floatingButton && state.routes.length % 2 === 0;
|
|
62
|
-
const itemWidth =
|
|
63
|
+
const itemWidth =
|
|
64
|
+
(widthDevice - insets.left - insets.right) /
|
|
65
|
+
(state.routes.length + (useFloating ? 1 : 0));
|
|
63
66
|
// const buildLink = useLinkBuilder();
|
|
64
67
|
const indicatorAnimated = useSharedValue(0);
|
|
65
|
-
const insets = useSafeAreaInsets();
|
|
66
68
|
|
|
67
69
|
const indicatorStyle = useAnimatedStyle(() => ({
|
|
68
70
|
width: itemWidth,
|
|
@@ -246,7 +248,8 @@ export default function BottomTabBar({
|
|
|
246
248
|
},
|
|
247
249
|
{
|
|
248
250
|
paddingBottom: paddingBottom,
|
|
249
|
-
|
|
251
|
+
paddingLeft: insets.left,
|
|
252
|
+
paddingRight: insets.right,
|
|
250
253
|
},
|
|
251
254
|
style,
|
|
252
255
|
]}
|
package/Input/InputMoney.tsx
CHANGED
|
@@ -4,13 +4,16 @@ import React, {
|
|
|
4
4
|
useContext,
|
|
5
5
|
useEffect,
|
|
6
6
|
useImperativeHandle,
|
|
7
|
+
useLayoutEffect,
|
|
7
8
|
useRef,
|
|
8
9
|
useState,
|
|
9
10
|
} from 'react';
|
|
10
11
|
import {
|
|
11
12
|
NativeSyntheticEvent,
|
|
13
|
+
Platform,
|
|
12
14
|
TextInput,
|
|
13
15
|
TextInputFocusEventData,
|
|
16
|
+
TextInputSelectionChangeEventData,
|
|
14
17
|
TouchableOpacity,
|
|
15
18
|
View,
|
|
16
19
|
} from 'react-native';
|
|
@@ -32,9 +35,124 @@ import {
|
|
|
32
35
|
} from './common';
|
|
33
36
|
import { InputMoneyProps } from './index';
|
|
34
37
|
import styles from './styles';
|
|
35
|
-
import { formatNumberToMoney } from './utils';
|
|
36
38
|
import SystemTextInput from './SystemTextInput';
|
|
37
39
|
|
|
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
|
+
|
|
38
156
|
const InputMoney = forwardRef(
|
|
39
157
|
(
|
|
40
158
|
{
|
|
@@ -73,7 +191,14 @@ const InputMoney = forwardRef(
|
|
|
73
191
|
const [focused, setFocused] = useState(false);
|
|
74
192
|
const [displayValue, setDisplayValue] = useState('');
|
|
75
193
|
const [numericValue, setNumericValue] = useState('');
|
|
76
|
-
const [
|
|
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);
|
|
77
202
|
const inputRef = useRef<TextInput>(null);
|
|
78
203
|
const componentName = 'InputMoney';
|
|
79
204
|
|
|
@@ -91,25 +216,36 @@ const InputMoney = forwardRef(
|
|
|
91
216
|
|
|
92
217
|
useEffect(() => {
|
|
93
218
|
if (value !== undefined && typeof value === 'string') {
|
|
94
|
-
const cleanValue = value.replace(/\D/g, '');
|
|
219
|
+
const cleanValue = value.replace(/\D/g, '').replace(/^0+(?=\d)/, '');
|
|
95
220
|
if (cleanValue !== numericValue) {
|
|
96
|
-
|
|
97
|
-
updateDisplayValue(cleanValue);
|
|
221
|
+
commit(cleanValue, cleanValue.length);
|
|
98
222
|
}
|
|
99
223
|
}
|
|
100
224
|
}, [value]);
|
|
101
225
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
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);
|
|
110
235
|
}
|
|
111
|
-
|
|
112
|
-
|
|
236
|
+
}, [commitCount]);
|
|
237
|
+
|
|
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;
|
|
113
249
|
};
|
|
114
250
|
|
|
115
251
|
const parseInputText = (text: string): string => {
|
|
@@ -119,47 +255,49 @@ const InputMoney = forwardRef(
|
|
|
119
255
|
return textWithoutCurrency.replace(/[^\d]/g, '');
|
|
120
256
|
};
|
|
121
257
|
|
|
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
|
-
|
|
132
258
|
const onClearText = () => {
|
|
133
259
|
inputRef?.current?.clear();
|
|
134
260
|
inputRef.current?.setNativeProps({ text: '' });
|
|
135
|
-
|
|
136
|
-
setNumericValue('');
|
|
261
|
+
commit('', 0);
|
|
137
262
|
onChangeText?.('');
|
|
138
263
|
};
|
|
139
264
|
|
|
140
265
|
const _onChangeText = (text: string) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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) {
|
|
145
292
|
return;
|
|
146
293
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const currentNumericValue = numericValue;
|
|
154
|
-
if (currentNumericValue.length > 0) {
|
|
155
|
-
const newNumericValue = currentNumericValue.slice(0, -1);
|
|
156
|
-
updateNumericValue(newNumericValue);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
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);
|
|
160
300
|
}
|
|
161
|
-
const newNumericValue = parseInputText(text);
|
|
162
|
-
updateNumericValue(newNumericValue);
|
|
163
301
|
};
|
|
164
302
|
|
|
165
303
|
const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
|
@@ -174,13 +312,16 @@ const InputMoney = forwardRef(
|
|
|
174
312
|
|
|
175
313
|
const _setText = (text: string) => {
|
|
176
314
|
if (!text) {
|
|
177
|
-
|
|
178
|
-
setNumericValue('');
|
|
315
|
+
commit('', 0);
|
|
179
316
|
onChangeText?.('');
|
|
180
317
|
return;
|
|
181
318
|
}
|
|
182
|
-
const
|
|
183
|
-
|
|
319
|
+
const digits = parseInputText(text)
|
|
320
|
+
.replace(/^0+(?=\d)/, '')
|
|
321
|
+
.slice(0, digitLimit);
|
|
322
|
+
if (commit(digits, digits.length)) {
|
|
323
|
+
onChangeText?.(digits);
|
|
324
|
+
}
|
|
184
325
|
};
|
|
185
326
|
|
|
186
327
|
useImperativeHandle(ref, () => {
|
|
@@ -247,6 +388,7 @@ const InputMoney = forwardRef(
|
|
|
247
388
|
textBreakStrategy="highQuality"
|
|
248
389
|
value={displayValue}
|
|
249
390
|
onChangeText={_onChangeText}
|
|
391
|
+
onSelectionChange={_onSelectionChange}
|
|
250
392
|
onFocus={_onFocus}
|
|
251
393
|
onBlur={_onBlur}
|
|
252
394
|
placeholder={placeholder}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@momo-kits/foundation",
|
|
3
|
-
"version": "0.167.1-
|
|
3
|
+
"version": "0.167.1-duo.1",
|
|
4
4
|
"description": "React Native Component Kits",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"scripts": {},
|
|
@@ -32,4 +32,4 @@
|
|
|
32
32
|
"registry": "https://registry.npmjs.org/"
|
|
33
33
|
},
|
|
34
34
|
"license": "MoMo"
|
|
35
|
-
}
|
|
35
|
+
}
|