numora-react 2.0.5 → 3.0.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.
- package/README.md +34 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.js +82 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +84 -62
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/handlers.ts +2 -4
- package/src/index.tsx +98 -76
package/src/index.tsx
CHANGED
|
@@ -2,13 +2,14 @@ import React, {
|
|
|
2
2
|
useRef,
|
|
3
3
|
useState,
|
|
4
4
|
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
5
6
|
forwardRef,
|
|
6
|
-
|
|
7
|
+
useCallback,
|
|
7
8
|
} from 'react';
|
|
8
9
|
import {
|
|
9
10
|
FormatOn,
|
|
10
11
|
ThousandStyle,
|
|
11
|
-
|
|
12
|
+
formatValueForDisplay,
|
|
12
13
|
type CaretPositionInfo,
|
|
13
14
|
type FormattingOptions,
|
|
14
15
|
} from 'numora';
|
|
@@ -19,13 +20,13 @@ import {
|
|
|
19
20
|
handleNumoraOnPaste,
|
|
20
21
|
} from './handlers';
|
|
21
22
|
|
|
22
|
-
interface NumoraInputProps
|
|
23
|
+
export interface NumoraInputProps
|
|
23
24
|
extends Omit<
|
|
24
25
|
React.InputHTMLAttributes<HTMLInputElement>,
|
|
25
26
|
'onChange' | 'type' | 'inputMode'
|
|
26
27
|
> {
|
|
27
28
|
maxDecimals?: number;
|
|
28
|
-
onChange?: (e: React.ChangeEvent<HTMLInputElement>
|
|
29
|
+
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
29
30
|
|
|
30
31
|
formatOn?: FormatOn;
|
|
31
32
|
thousandSeparator?: string;
|
|
@@ -60,10 +61,11 @@ const NumoraInput = forwardRef<HTMLInputElement, NumoraInputProps>((props, ref)
|
|
|
60
61
|
...rest
|
|
61
62
|
} = props;
|
|
62
63
|
|
|
63
|
-
const
|
|
64
|
+
const internalInputRef = useRef<HTMLInputElement>(null);
|
|
64
65
|
const caretInfoRef = useRef<CaretPositionInfo | undefined>(undefined);
|
|
66
|
+
const lastCaretPosRef = useRef<number | null>(null);
|
|
65
67
|
|
|
66
|
-
const formattingOptions: FormattingOptions
|
|
68
|
+
const formattingOptions: FormattingOptions = {
|
|
67
69
|
formatOn,
|
|
68
70
|
thousandSeparator,
|
|
69
71
|
ThousandStyle: thousandStyle,
|
|
@@ -75,130 +77,150 @@ const NumoraInput = forwardRef<HTMLInputElement, NumoraInputProps>((props, ref)
|
|
|
75
77
|
rawValueMode,
|
|
76
78
|
};
|
|
77
79
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
const getInitialValue = (): string => {
|
|
81
|
+
const valueToFormat = controlledValue !== undefined ? controlledValue : defaultValue;
|
|
82
|
+
if (valueToFormat !== undefined) {
|
|
83
|
+
const { formatted } = formatValueForDisplay(String(valueToFormat), maxDecimals, formattingOptions);
|
|
81
84
|
return formatted;
|
|
82
85
|
}
|
|
83
86
|
return '';
|
|
84
87
|
};
|
|
85
88
|
|
|
86
|
-
const
|
|
87
|
-
if (controlledValue !== undefined) {
|
|
88
|
-
const { formatted } = formatValue(String(controlledValue), maxDecimals, formattingOptions);
|
|
89
|
-
return formatted;
|
|
90
|
-
}
|
|
91
|
-
return '';
|
|
92
|
-
};
|
|
89
|
+
const [displayValue, setDisplayValue] = useState<string>(getInitialValue);
|
|
93
90
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
// Sync external ref with internal ref
|
|
92
|
+
useLayoutEffect(() => {
|
|
93
|
+
if (!ref) return;
|
|
94
|
+
if (typeof ref === 'function') {
|
|
95
|
+
ref(internalInputRef.current);
|
|
96
|
+
} else {
|
|
97
|
+
ref.current = internalInputRef.current;
|
|
98
|
+
}
|
|
99
|
+
}, [ref]);
|
|
97
100
|
|
|
98
|
-
// When controlled value changes
|
|
101
|
+
// When controlled value changes from outside, update display value
|
|
99
102
|
useEffect(() => {
|
|
100
103
|
if (controlledValue !== undefined) {
|
|
101
|
-
const { formatted } =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const isControlled = controlledValue !== undefined;
|
|
107
|
-
|
|
108
|
-
const updateValue = (value: string) => {
|
|
109
|
-
if (isControlled) {
|
|
110
|
-
setInternalValue(value);
|
|
104
|
+
const { formatted } = formatValueForDisplay(String(controlledValue), maxDecimals, formattingOptions);
|
|
105
|
+
if (formatted !== displayValue) {
|
|
106
|
+
setDisplayValue(formatted);
|
|
107
|
+
}
|
|
111
108
|
}
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
value: formattedValue,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
if (rawValue !== undefined) {
|
|
125
|
-
Object.defineProperty(target, 'rawValue', {
|
|
126
|
-
writable: true,
|
|
127
|
-
value: rawValue,
|
|
128
|
-
enumerable: true,
|
|
129
|
-
configurable: true,
|
|
130
|
-
});
|
|
109
|
+
}, [controlledValue, maxDecimals, formattingOptions]);
|
|
110
|
+
|
|
111
|
+
// Restore cursor position after render
|
|
112
|
+
useLayoutEffect(() => {
|
|
113
|
+
if (internalInputRef.current && lastCaretPosRef.current !== null) {
|
|
114
|
+
const input = internalInputRef.current;
|
|
115
|
+
const pos = lastCaretPosRef.current;
|
|
116
|
+
input.setSelectionRange(pos, pos);
|
|
117
|
+
lastCaretPosRef.current = null;
|
|
131
118
|
}
|
|
132
|
-
};
|
|
119
|
+
});
|
|
133
120
|
|
|
134
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
121
|
+
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
135
122
|
const { value, rawValue } = handleNumoraOnChange(e, {
|
|
136
123
|
decimalMaxLength: maxDecimals,
|
|
137
124
|
caretPositionBeforeChange: caretInfoRef.current,
|
|
138
125
|
formattingOptions,
|
|
139
126
|
});
|
|
127
|
+
|
|
128
|
+
// Store cursor position AFTER core library has calculated and set it
|
|
129
|
+
// The core library modifies the DOM element directly during handleNumoraOnChange
|
|
130
|
+
// Read from the input ref (which is the same element) to get the position set by the core library
|
|
131
|
+
if (internalInputRef.current) {
|
|
132
|
+
const cursorPos = internalInputRef.current.selectionStart;
|
|
133
|
+
if (cursorPos !== null && cursorPos !== undefined) {
|
|
134
|
+
lastCaretPosRef.current = cursorPos;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
140
138
|
caretInfoRef.current = undefined;
|
|
141
139
|
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
// Add rawValue to the event object without overriding 'value' property
|
|
141
|
+
(e.target as any).rawValue = rawValue;
|
|
142
|
+
|
|
143
|
+
setDisplayValue(value);
|
|
144
144
|
|
|
145
145
|
if (onChange) {
|
|
146
146
|
onChange(e);
|
|
147
147
|
}
|
|
148
|
-
};
|
|
148
|
+
}, [maxDecimals, formattingOptions, onChange]);
|
|
149
|
+
|
|
150
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
151
|
+
const coreCaretInfo = handleNumoraOnKeyDown(e, formattingOptions);
|
|
152
|
+
|
|
153
|
+
// Always capture cursor position info, even if core library doesn't return it
|
|
154
|
+
// This is needed for cursor position calculation during normal typing (not just Delete/Backspace)
|
|
155
|
+
if (!coreCaretInfo && internalInputRef.current) {
|
|
156
|
+
const selectionStart = internalInputRef.current.selectionStart ?? 0;
|
|
157
|
+
const selectionEnd = internalInputRef.current.selectionEnd ?? 0;
|
|
158
|
+
caretInfoRef.current = {
|
|
159
|
+
selectionStart,
|
|
160
|
+
selectionEnd,
|
|
161
|
+
};
|
|
162
|
+
} else {
|
|
163
|
+
caretInfoRef.current = coreCaretInfo;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (onKeyDown) {
|
|
167
|
+
onKeyDown(e);
|
|
168
|
+
}
|
|
169
|
+
}, [formattingOptions, onKeyDown]);
|
|
149
170
|
|
|
150
|
-
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
|
171
|
+
const handlePaste = useCallback((e: React.ClipboardEvent<HTMLInputElement>) => {
|
|
151
172
|
const { value, rawValue } = handleNumoraOnPaste(e, {
|
|
152
173
|
decimalMaxLength: maxDecimals,
|
|
153
174
|
formattingOptions,
|
|
154
175
|
});
|
|
155
176
|
|
|
156
|
-
|
|
157
|
-
|
|
177
|
+
// For paste, we often want to move cursor to the end of pasted content
|
|
178
|
+
// handleNumoraOnPaste already handles DOM value and cursor, but React will overwrite it.
|
|
179
|
+
// So we capture where the core logic set the cursor.
|
|
180
|
+
lastCaretPosRef.current = (e.target as HTMLInputElement).selectionStart;
|
|
181
|
+
(e.target as any).rawValue = rawValue;
|
|
182
|
+
|
|
183
|
+
setDisplayValue(value);
|
|
158
184
|
|
|
159
185
|
if (onPaste) {
|
|
160
186
|
onPaste(e);
|
|
161
187
|
}
|
|
162
|
-
if (onChange) {
|
|
163
|
-
onChange(e);
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
188
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
189
|
+
// Trigger onChange manually because paste event doesn't always trigger a ChangeEvent in all React versions
|
|
190
|
+
// when we preventDefault.
|
|
191
|
+
if (onChange) {
|
|
192
|
+
const changeEvent = e as unknown as React.ChangeEvent<HTMLInputElement>;
|
|
193
|
+
onChange(changeEvent);
|
|
171
194
|
}
|
|
172
|
-
};
|
|
195
|
+
}, [maxDecimals, formattingOptions, onPaste, onChange]);
|
|
173
196
|
|
|
174
|
-
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
197
|
+
const handleBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
|
|
175
198
|
const { value, rawValue } = handleNumoraOnBlur(e, {
|
|
176
199
|
decimalMaxLength: maxDecimals,
|
|
177
200
|
formattingOptions,
|
|
178
201
|
});
|
|
179
202
|
|
|
180
|
-
|
|
181
|
-
|
|
203
|
+
(e.target as any).rawValue = rawValue;
|
|
204
|
+
setDisplayValue(value);
|
|
182
205
|
|
|
183
206
|
if (onBlur) {
|
|
184
207
|
onBlur(e);
|
|
185
208
|
}
|
|
186
|
-
};
|
|
209
|
+
}, [maxDecimals, formattingOptions, onBlur]);
|
|
187
210
|
|
|
188
211
|
return (
|
|
189
212
|
<input
|
|
190
213
|
{...rest}
|
|
191
|
-
ref={
|
|
192
|
-
{
|
|
193
|
-
? { value: internalValue }
|
|
194
|
-
: { defaultValue: getFormattedDefaultValue() }
|
|
195
|
-
)}
|
|
214
|
+
ref={internalInputRef}
|
|
215
|
+
value={displayValue}
|
|
196
216
|
onChange={handleChange}
|
|
197
|
-
onPaste={handlePaste}
|
|
198
217
|
onKeyDown={handleKeyDown}
|
|
218
|
+
onPaste={handlePaste}
|
|
199
219
|
onBlur={handleBlur}
|
|
200
220
|
type="text"
|
|
201
221
|
inputMode="decimal"
|
|
222
|
+
spellCheck={false}
|
|
223
|
+
autoComplete="off"
|
|
202
224
|
/>
|
|
203
225
|
);
|
|
204
226
|
});
|