@momo-kits/foundation 0.168.1-beta.2 → 0.168.1-beta.4

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 (2) hide show
  1. package/Input/InputMoney.tsx +99 -171
  2. package/package.json +1 -1
@@ -1,16 +1,16 @@
1
+ /* eslint-disable react-hooks/exhaustive-deps */
1
2
  import React, {
2
3
  forwardRef,
3
4
  useContext,
4
5
  useEffect,
5
6
  useImperativeHandle,
6
- useLayoutEffect,
7
7
  useRef,
8
8
  useState,
9
9
  } from 'react';
10
10
  import {
11
+ Platform,
11
12
  TextInput,
12
13
  TextInputFocusEvent,
13
- TextInputSelectionChangeEvent,
14
14
  TouchableOpacity,
15
15
  View,
16
16
  } from 'react-native';
@@ -32,97 +32,9 @@ import {
32
32
  } from './common';
33
33
  import { InputMoneyProps } from './index';
34
34
  import styles from './styles';
35
+ import { formatNumberToMoney } from './utils';
35
36
  import SystemTextInput from './SystemTextInput';
36
37
 
37
- type Selection = { start: number; end: number };
38
- type Edit = { start: number; end: number; inserted: string };
39
-
40
- const countDigits = (text: string) => text.replace(/\D/g, '').length;
41
-
42
- const groupDigits = (digits: string) =>
43
- digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
44
-
45
- const caretFromDigitIndex = (display: string, digitIndex: number) => {
46
- let count = 0;
47
- for (let i = 0; i < display.length && count < digitIndex; i++) {
48
- if (/\d/.test(display[i])) {
49
- count += 1;
50
- if (count === digitIndex) return i + 1;
51
- }
52
- }
53
- return 0;
54
- };
55
-
56
- const detectEdit = (old: string, next: string, selection: Selection): Edit => {
57
- const { start, end } = selection;
58
- const removed = old.length - next.length;
59
- const candidates: Edit[] = [
60
- {
61
- start,
62
- end,
63
- inserted: next.slice(start, next.length - (old.length - end)),
64
- },
65
- { start: start - removed, end: start, inserted: '' },
66
- { start, end: start + removed, inserted: '' },
67
- ];
68
- const match = candidates.find(
69
- e =>
70
- e.start >= 0 &&
71
- e.end >= e.start &&
72
- e.end <= old.length &&
73
- old.slice(0, e.start) + e.inserted + old.slice(e.end) === next,
74
- );
75
- if (match) return match;
76
-
77
- let prefix = 0;
78
- while (
79
- prefix < Math.min(old.length, next.length) &&
80
- old[prefix] === next[prefix]
81
- ) {
82
- prefix += 1;
83
- }
84
- let suffix = 0;
85
- while (
86
- suffix < Math.min(old.length, next.length) - prefix &&
87
- old[old.length - 1 - suffix] === next[next.length - 1 - suffix]
88
- ) {
89
- suffix += 1;
90
- }
91
- return {
92
- start: prefix,
93
- end: old.length - suffix,
94
- inserted: next.slice(prefix, next.length - suffix),
95
- };
96
- };
97
-
98
- const applyEdit = (display: string, edit: Edit, digitLimit: number) => {
99
- const digits = display.replace(/\D/g, '');
100
- let left = countDigits(display.slice(0, edit.start));
101
- let removed = countDigits(display.slice(edit.start, edit.end));
102
- const inserted = edit.inserted.replace(/\D/g, '');
103
- if (
104
- !removed &&
105
- !inserted &&
106
- left > 0 &&
107
- display.slice(edit.start, edit.end).includes('.')
108
- ) {
109
- left -= 1;
110
- removed = 1;
111
- }
112
- const next = digits.slice(0, left) + inserted + digits.slice(left + removed);
113
- const trimmed = next.replace(/^0+(?=\d)/, '');
114
- if ((!removed && !inserted) || trimmed.length > digitLimit) {
115
- return { digits, caretDigit: left };
116
- }
117
- return {
118
- digits: trimmed,
119
- caretDigit: Math.max(
120
- 0,
121
- left + inserted.length - (next.length - trimmed.length),
122
- ),
123
- };
124
- };
125
-
126
38
  const InputMoney = forwardRef(
127
39
  (
128
40
  {
@@ -160,14 +72,10 @@ const InputMoney = forwardRef(
160
72
  const inputFontFamily = exportFontFamily('bold');
161
73
  const [focused, setFocused] = useState(false);
162
74
  const [displayValue, setDisplayValue] = useState('');
163
- const [commitCount, setCommitCount] = useState(0);
164
- const displayRef = useRef('');
165
- const digitsRef = useRef('');
166
- const selectionRef = useRef<Selection>({ start: 0, end: 0 });
167
- const pendingCaretRef = useRef<number | null>(null);
168
- const pendingSelectionRef = useRef<Selection | null>(null);
169
- const editingRef = useRef(false);
75
+ const [numericValue, setNumericValue] = useState('');
76
+ const [lastDisplayValue, setLastDisplayValue] = useState('');
170
77
  const inputRef = useRef<TextInput>(null);
78
+ const caretDigitsAfter = useRef<number | undefined>(undefined);
171
79
  const componentName = 'InputMoney';
172
80
 
173
81
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
@@ -177,94 +85,114 @@ const InputMoney = forwardRef(
177
85
  props.accessibilityLabel,
178
86
  );
179
87
 
180
- const digitLimit = props.maxLength ?? Number.POSITIVE_INFINITY;
88
+ const maxLength = (props as any)?.maxLength;
89
+ const hasMaxLength = typeof maxLength === 'number';
90
+ const digitLimit = hasMaxLength ? maxLength : Number.POSITIVE_INFINITY;
181
91
 
182
92
  useEffect(() => {
183
93
  if (value !== undefined && typeof value === 'string') {
184
94
  const cleanValue = value.replace(/\D/g, '').replace(/^0+(?=\d)/, '');
185
- if (cleanValue !== digitsRef.current) {
186
- commit(cleanValue, cleanValue.length);
95
+ if (cleanValue !== numericValue) {
96
+ caretDigitsAfter.current = undefined;
97
+ setNumericValue(cleanValue);
98
+ updateDisplayValue(cleanValue);
187
99
  }
188
100
  }
189
- // eslint-disable-next-line react-hooks/exhaustive-deps
190
101
  }, [value]);
191
102
 
192
- useLayoutEffect(() => {
193
- const caret = pendingCaretRef.current;
194
- if (caret === null || !inputRef.current?.isFocused()) return;
195
- pendingCaretRef.current = null;
196
- inputRef.current.setSelection(caret, caret);
197
- }, [commitCount]);
103
+ // Android moves the caret proportionally when the text is replaced, so it
104
+ // lands before the typed digit once a separator is added. Put it back after
105
+ // the same digit.
106
+ useEffect(() => {
107
+ setLastDisplayValue(displayValue);
108
+ const digitsAfter = caretDigitsAfter.current;
109
+ caretDigitsAfter.current = undefined;
110
+ if (Platform.OS !== 'android' || digitsAfter === undefined) return;
111
+ let before = parseInputText(displayValue).length - digitsAfter;
112
+ let caret = 0;
113
+ while (before > 0 && caret < displayValue.length) {
114
+ if (/\d/.test(displayValue[caret])) before -= 1;
115
+ caret += 1;
116
+ }
117
+ inputRef.current?.setSelection(caret, caret);
118
+ }, [displayValue]);
119
+
120
+ const updateDisplayValue = (numeric: string) => {
121
+ if (!numeric) {
122
+ setDisplayValue('');
123
+ return;
124
+ }
125
+ const formatted = formatNumberToMoney(Number(numeric));
126
+ setDisplayValue(formatted ? `${formatted}${currency}` : '');
127
+ };
128
+
129
+ const parseInputText = (text: string): string => {
130
+ if (!text) return '';
131
+ const currencyRegex = new RegExp(` ?${currency}$`, 'g');
132
+ const textWithoutCurrency = text.replace(currencyRegex, '').trim();
133
+ return textWithoutCurrency.replace(/[^\d]/g, '');
134
+ };
198
135
 
199
- const commit = (digits: string, caretDigit: number) => {
200
- const display = digits ? `${groupDigits(digits)}${currency}` : '';
201
- const caret = caretFromDigitIndex(display, caretDigit);
202
- const changed = digits !== digitsRef.current;
203
- displayRef.current = display;
204
- digitsRef.current = digits;
205
- selectionRef.current = { start: caret, end: caret };
206
- pendingCaretRef.current = caret;
207
- setCommitCount(count => count + 1);
208
- setDisplayValue(display);
209
- return changed;
136
+ const updateNumericValue = (val: string) => {
137
+ let next = val.replace(/[^\d]/g, '').replace(/^0+(?=\d)/, '');
138
+ if (next.length > digitLimit) next = next.slice(0, digitLimit);
139
+ if (/^\d*$/.test(next)) {
140
+ setNumericValue(next);
141
+ updateDisplayValue(next);
142
+ onChangeText?.(next);
143
+ }
210
144
  };
211
145
 
212
146
  const onClearText = () => {
213
- commit('', 0);
147
+ inputRef?.current?.clear();
148
+ inputRef.current?.setNativeProps({ text: '' });
149
+ setDisplayValue('');
150
+ setNumericValue('');
214
151
  onChangeText?.('');
215
152
  };
216
153
 
217
154
  const _onChangeText = (text: string) => {
218
- let digits = '';
219
- let caretDigit = 0;
220
- pendingSelectionRef.current = null;
221
- editingRef.current = true;
222
- setTimeout(() => {
223
- editingRef.current = false;
224
- });
225
- if (text) {
226
- const edit = detectEdit(displayRef.current, text, selectionRef.current);
227
- ({ digits, caretDigit } = applyEdit(
228
- displayRef.current,
229
- edit,
230
- digitLimit,
231
- ));
155
+ if (!text) {
156
+ setDisplayValue('');
157
+ setNumericValue('');
158
+ onChangeText?.('');
159
+ return;
232
160
  }
233
- if (commit(digits, caretDigit)) {
234
- onChangeText?.(digits);
161
+ const old = lastDisplayValue;
162
+ let start = 0;
163
+ while (start < text.length && old[start] === text[start]) {
164
+ start += 1;
235
165
  }
236
- };
237
-
238
- const _onSelectionChange = (e: TextInputSelectionChangeEvent) => {
239
- props.onSelectionChange?.(e);
240
- if (editingRef.current) return;
241
- const selection = e.nativeEvent.selection;
242
- pendingSelectionRef.current = selection;
243
- setTimeout(() => {
244
- if (pendingSelectionRef.current !== selection) return;
245
- pendingSelectionRef.current = null;
246
- const { start, end } = selection;
247
- const display = displayRef.current;
248
- const max = display ? display.length - currency.length : 0;
249
- const clamped = {
250
- start: Math.min(start, max),
251
- end: Math.min(end, max),
252
- };
253
- selectionRef.current = clamped;
254
- if (clamped.start !== start || clamped.end !== end) {
255
- inputRef.current?.setSelection(clamped.start, clamped.end);
166
+ let suffix = 0;
167
+ while (
168
+ suffix < Math.min(text.length, old.length) - start &&
169
+ old[old.length - 1 - suffix] === text[text.length - 1 - suffix]
170
+ ) {
171
+ suffix += 1;
172
+ }
173
+ caretDigitsAfter.current = text
174
+ .slice(text.length - suffix)
175
+ .replace(/\D/g, '').length;
176
+ if (text.length < old.length) {
177
+ const removed = old.slice(start, old.length - suffix);
178
+ if (!/\d/.test(removed)) {
179
+ const index = removed.includes('.')
180
+ ? parseInputText(old.slice(0, start)).length - 1
181
+ : numericValue.length - 1;
182
+ if (index >= 0) {
183
+ updateNumericValue(
184
+ numericValue.slice(0, index) + numericValue.slice(index + 1),
185
+ );
186
+ }
187
+ return;
256
188
  }
257
- });
189
+ }
190
+ const newNumericValue = parseInputText(text);
191
+ updateNumericValue(newNumericValue);
258
192
  };
259
193
 
260
194
  const _onFocus = (e: TextInputFocusEvent) => {
261
195
  setFocused(true);
262
- const caret = pendingCaretRef.current;
263
- pendingCaretRef.current = null;
264
- if (caret !== null && !props.selectTextOnFocus) {
265
- selectionRef.current = { start: caret, end: caret };
266
- inputRef.current?.setSelection(caret, caret);
267
- }
268
196
  onFocus?.(e);
269
197
  };
270
198
 
@@ -274,13 +202,15 @@ const InputMoney = forwardRef(
274
202
  };
275
203
 
276
204
  const _setText = (text: string) => {
277
- const digits = text
278
- .replace(/\D/g, '')
279
- .replace(/^0+(?=\d)/, '')
280
- .slice(0, digitLimit);
281
- if (commit(digits, digits.length)) {
282
- onChangeText?.(digits);
205
+ caretDigitsAfter.current = undefined;
206
+ if (!text) {
207
+ setDisplayValue('');
208
+ setNumericValue('');
209
+ onChangeText?.('');
210
+ return;
283
211
  }
212
+ const newNumericValue = parseInputText(text);
213
+ updateNumericValue(newNumericValue);
284
214
  };
285
215
 
286
216
  useImperativeHandle(ref, () => {
@@ -326,7 +256,6 @@ const InputMoney = forwardRef(
326
256
  accessibilityLabel={`${componentId}`}
327
257
  textAlignVertical="center"
328
258
  ref={inputRef}
329
- editable={props.editable !== false && !disabled}
330
259
  accessibilityState={{
331
260
  disabled: disabled,
332
261
  ...props.accessibilityState,
@@ -348,7 +277,6 @@ const InputMoney = forwardRef(
348
277
  textBreakStrategy="highQuality"
349
278
  value={displayValue}
350
279
  onChangeText={_onChangeText}
351
- onSelectionChange={_onSelectionChange}
352
280
  onFocus={_onFocus}
353
281
  onBlur={_onBlur}
354
282
  placeholder={placeholder}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.168.1-beta.2",
3
+ "version": "0.168.1-beta.4",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},