@momo-kits/foundation 0.168.1-beta.3 → 0.168.1-beta.5

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.
@@ -8,6 +8,7 @@ import React, {
8
8
  useState,
9
9
  } from 'react';
10
10
  import {
11
+ Platform,
11
12
  TextInput,
12
13
  TextInputFocusEvent,
13
14
  TouchableOpacity,
@@ -31,9 +32,26 @@ import {
31
32
  } from './common';
32
33
  import { InputMoneyProps } from './index';
33
34
  import styles from './styles';
34
- import { formatNumberToMoney } from './utils';
35
35
  import SystemTextInput from './SystemTextInput';
36
36
 
37
+ // Finds the common prefix and suffix of `old` and `text`; `size` counts the
38
+ // characters removed plus inserted between them.
39
+ const findEdit = (old: string, text: string) => {
40
+ let start = 0;
41
+ while (start < text.length && old[start] === text[start]) {
42
+ start += 1;
43
+ }
44
+ let suffix = 0;
45
+ while (
46
+ suffix < Math.min(text.length, old.length) - start &&
47
+ old[old.length - 1 - suffix] === text[text.length - 1 - suffix]
48
+ ) {
49
+ suffix += 1;
50
+ }
51
+ const size = old.length + text.length - 2 * (start + suffix);
52
+ return { old, start, suffix, size };
53
+ };
54
+
37
55
  const InputMoney = forwardRef(
38
56
  (
39
57
  {
@@ -74,6 +92,12 @@ const InputMoney = forwardRef(
74
92
  const [numericValue, setNumericValue] = useState('');
75
93
  const [lastDisplayValue, setLastDisplayValue] = useState('');
76
94
  const inputRef = useRef<TextInput>(null);
95
+ const caretDigitsAfter = useRef<number | undefined>(undefined);
96
+ const lastRawText = useRef('');
97
+ const [caretSelection, setCaretSelection] = useState<{
98
+ start: number;
99
+ end: number;
100
+ }>();
77
101
  const componentName = 'InputMoney';
78
102
 
79
103
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
@@ -91,6 +115,7 @@ const InputMoney = forwardRef(
91
115
  if (value !== undefined && typeof value === 'string') {
92
116
  const cleanValue = value.replace(/\D/g, '').replace(/^0+(?=\d)/, '');
93
117
  if (cleanValue !== numericValue) {
118
+ caretDigitsAfter.current = undefined;
94
119
  setNumericValue(cleanValue);
95
120
  updateDisplayValue(cleanValue);
96
121
  }
@@ -101,13 +126,36 @@ const InputMoney = forwardRef(
101
126
  setLastDisplayValue(displayValue);
102
127
  }, [displayValue]);
103
128
 
129
+ // The selection only rides along with the render that reformats the text,
130
+ // then it is released so the user can move the caret freely.
131
+ useEffect(() => {
132
+ if (!caretSelection) return;
133
+ // A newer keystroke may already have queued its own selection; keep it.
134
+ setCaretSelection(current =>
135
+ current === caretSelection ? undefined : current,
136
+ );
137
+ }, [caretSelection]);
138
+
104
139
  const updateDisplayValue = (numeric: string) => {
105
- if (!numeric) {
106
- setDisplayValue('');
107
- return;
140
+ // Group the digit string directly: Number() loses precision past 15
141
+ // digits and switches to "1e+21" notation past 20.
142
+ const formatted = numeric.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
143
+ const next = formatted ? `${formatted}${currency}` : '';
144
+ setDisplayValue(next);
145
+
146
+ // Android moves the caret proportionally when the text is replaced, so
147
+ // it lands before the typed digit once a separator is added. Send the
148
+ // caret together with the text so a fast keystroke can't land in between.
149
+ const digitsAfter = caretDigitsAfter.current;
150
+ caretDigitsAfter.current = undefined;
151
+ if (Platform.OS !== 'android' || digitsAfter === undefined) return;
152
+ let before = parseInputText(next).length - digitsAfter;
153
+ let caret = 0;
154
+ while (before > 0 && caret < next.length) {
155
+ if (/\d/.test(next[caret])) before -= 1;
156
+ caret += 1;
108
157
  }
109
- const formatted = formatNumberToMoney(Number(numeric));
110
- setDisplayValue(formatted ? `${formatted}${currency}` : '');
158
+ setCaretSelection({ start: caret, end: caret });
111
159
  };
112
160
 
113
161
  const parseInputText = (text: string): string => {
@@ -137,24 +185,25 @@ const InputMoney = forwardRef(
137
185
 
138
186
  const _onChangeText = (text: string) => {
139
187
  if (!text) {
188
+ lastRawText.current = '';
140
189
  setDisplayValue('');
141
190
  setNumericValue('');
142
191
  onChangeText?.('');
143
192
  return;
144
193
  }
145
- if (text.length < lastDisplayValue.length) {
146
- const old = lastDisplayValue;
147
- let start = 0;
148
- while (start < text.length && old[start] === text[start]) {
149
- start += 1;
150
- }
151
- let suffix = 0;
152
- while (
153
- suffix < text.length - start &&
154
- old[old.length - 1 - suffix] === text[text.length - 1 - suffix]
155
- ) {
156
- suffix += 1;
157
- }
194
+ // When typing outpaces JS (slow Android devices) native rejects the stale
195
+ // formatted text and keeps the raw one, so the edit may apply to either.
196
+ // Pick the one that differs from the new text by a single edit.
197
+ const raw = lastRawText.current;
198
+ lastRawText.current = text;
199
+ const formattedEdit = findEdit(lastDisplayValue, text);
200
+ const rawEdit = findEdit(raw, text);
201
+ const { old, start, suffix } =
202
+ raw && rawEdit.size < formattedEdit.size ? rawEdit : formattedEdit;
203
+ caretDigitsAfter.current = text
204
+ .slice(text.length - suffix)
205
+ .replace(/\D/g, '').length;
206
+ if (text.length < old.length) {
158
207
  const removed = old.slice(start, old.length - suffix);
159
208
  if (!/\d/.test(removed)) {
160
209
  const index = removed.includes('.')
@@ -183,6 +232,7 @@ const InputMoney = forwardRef(
183
232
  };
184
233
 
185
234
  const _setText = (text: string) => {
235
+ caretDigitsAfter.current = undefined;
186
236
  if (!text) {
187
237
  setDisplayValue('');
188
238
  setNumericValue('');
@@ -256,6 +306,7 @@ const InputMoney = forwardRef(
256
306
  allowFontScaling={false}
257
307
  textBreakStrategy="highQuality"
258
308
  value={displayValue}
309
+ selection={caretSelection ?? props.selection}
259
310
  onChangeText={_onChangeText}
260
311
  onFocus={_onFocus}
261
312
  onBlur={_onBlur}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.168.1-beta.3",
3
+ "version": "0.168.1-beta.5",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},