numora-react 3.0.3 → 3.2.0

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 CHANGED
@@ -3,69 +3,17 @@
3
3
  [![npm version](https://img.shields.io/npm/v/numora-react.svg)](https://www.npmjs.com/package/numora-react)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/numora-react.svg)](https://www.npmjs.com/package/numora-react)
5
5
 
6
- React component wrapper for [Numora](https://github.com/Sharqiewicz/numora) - a precision-first numeric input library for DeFi and financial applications.
6
+ React component wrapper for [numora](../core/README.md) - a precision-first numeric input library.
7
7
 
8
- ## Features
9
-
10
- | Feature | Description |
11
- |---------|-------------|
12
- | **React Component** | Drop-in replacement for `<input>` with numeric formatting |
13
- | **Decimal Precision Control** | Configure maximum decimal places with `maxDecimals` prop |
14
- | **Thousand Separators** | Customizable thousand separators with `thousandSeparator` prop |
15
- | **Grouping Styles** | Support for different grouping styles (`thousand`, `lakh`, `wan`) |
16
- | **Format on Blur/Change** | Choose when to apply formatting: on blur or on change |
17
- | **Compact Notation Expansion** | When enabled via `enableCompactNotation`, expands compact notation during paste (e.g., `"1k"` → `"1000"`, `"1.5m"` → `"1500000"`) |
18
- | **Scientific Notation Expansion** | Always automatically expands scientific notation (e.g., `"1.5e-7"` → `"0.00000015"`, `"2e+5"` → `"200000"`) |
19
- | **Paste Event Handling** | Intelligent paste handling with automatic sanitization, formatting, and cursor positioning |
20
- | **Cursor Position Preservation** | Smart cursor positioning that works with thousand separators, even during formatting |
21
- | **Thousand Separator Skipping** | On delete/backspace, cursor automatically skips over thousand separators for better UX |
22
- | **Mobile Keyboard Optimization** | Automatic `inputmode="decimal"` for mobile numeric keyboards |
23
- | **Mobile Keyboard Filtering** | Automatically filters non-breaking spaces and Unicode whitespace artifacts from mobile keyboards |
24
- | **Non-numeric Character Filtering** | Automatic removal of invalid characters |
25
- | **Comma/Dot Conversion** | When `thousandStyle` is not set (or `None`), typing comma or dot automatically converts to the configured decimal separator |
26
- | **TypeScript Support** | Full TypeScript definitions included |
27
- | **Ref Forwarding** | Supports React ref forwarding for direct input access |
28
- | **Standard Input Props** | Accepts all standard HTMLInputElement props |
29
-
30
- **Note:** Some advanced features from the core package (like `decimalMinLength`, `enableNegative`, `enableLeadingZeros`, `rawValueMode`) are not yet exposed through the React component props. For full control, consider using the core `numora` package directly.
31
-
32
- ## Comparison
33
-
34
- | Feature | numora-react | react-number-format | Native Number Input |
35
- |---------|--------------|---------------------|---------------------|
36
- | **React Component** | ✅ Yes | ✅ Yes | ⚠️ Basic |
37
- | **Decimal Precision Control** | ✅ Max | ✅ Max | ❌ Limited |
38
- | **Thousand Separators** | ✅ Customizable | ✅ Yes | ❌ No |
39
- | **Custom Decimal Separator** | ✅ Yes | ✅ Yes | ❌ No (always `.`) |
40
- | **Formatting Options** | ✅ Blur/Change modes | ✅ Multiple modes | ❌ No |
41
- | **Cursor Preservation** | ✅ Advanced | ✅ Basic | ❌ N/A |
42
- | **Mobile Support** | ✅ Yes | ✅ Yes | ⚠️ Limited |
43
- | **TypeScript Support** | ✅ Yes | ✅ Yes | ⚠️ Partial |
44
- | **Dependencies** | ⚠️ React + numora | ⚠️ React required | ✅ None |
45
- | **Framework Support** | ✅ React | ❌ React only | ✅ All |
46
- | **Scientific Notation** | ✅ Auto-expand | ⚠️ Limited | ❌ No |
47
- | **Compact Notation** | ✅ Yes (on paste when enabled) | ❌ No | ❌ No |
48
- | **Paste Handling** | ✅ Intelligent | ✅ Yes | ⚠️ Basic |
49
- | **Ref Forwarding** | ✅ Yes | ✅ Yes | ✅ Yes |
50
- | **Grouping Styles** | ✅ Thousand/Lakh/Wan | ⚠️ Thousand only | ❌ No |
51
- | **Comma/Dot Conversion** | ✅ Yes | ⚠️ Limited | ❌ No |
52
-
53
- ## Installation
8
+ ## Install
54
9
 
55
10
  ```bash
56
11
  npm install numora-react
57
- # or
58
- yarn add numora-react
59
- # or
60
- pnpm add numora-react
12
+ # or pnpm add numora-react / yarn add numora-react
61
13
  ```
62
14
 
63
- **Note:** `numora-react` depends on `numora` core package, which will be installed automatically.
64
-
65
15
  ## Usage
66
16
 
67
- ### Basic Example
68
-
69
17
  ```tsx
70
18
  import { NumoraInput } from 'numora-react';
71
19
 
@@ -73,304 +21,43 @@ function App() {
73
21
  return (
74
22
  <NumoraInput
75
23
  maxDecimals={2}
76
- onChange={(e) => {
77
- console.log('Value:', e.target.value);
78
- }}
79
- />
80
- );
81
- }
82
- ```
83
-
84
- ### Advanced Example
85
-
86
- ```tsx
87
- import { NumoraInput } from 'numora-react';
88
- import { useRef } from 'react';
89
-
90
- function PaymentForm() {
91
- const inputRef = useRef<HTMLInputElement>(null);
92
-
93
- return (
94
- <NumoraInput
95
- ref={inputRef}
96
- maxDecimals={18}
97
- formatOn="change"
98
24
  thousandSeparator=","
99
25
  thousandStyle="thousand"
100
- enableCompactNotation={true} // Enable compact notation expansion on paste
101
- placeholder="Enter amount"
102
- className="payment-input"
103
- onChange={(e) => {
104
- const value = e.target.value;
105
- console.log('Formatted value:', value);
106
- }}
107
- onFocus={(e) => {
108
- console.log('Input focused');
109
- }}
110
- onBlur={(e) => {
111
- console.log('Input blurred');
112
- }}
113
- />
114
- );
115
- }
116
- ```
117
-
118
- ### Compact Notation Example
119
-
120
- ```tsx
121
- import { NumoraInput } from 'numora-react';
122
-
123
- function App() {
124
- return (
125
- <NumoraInput
126
- maxDecimals={18}
127
- enableCompactNotation={true} // Enable compact notation expansion
128
- onChange={(e) => {
129
- console.log('Value:', e.target.value);
130
- }}
131
- />
132
- );
133
- }
134
-
135
- // When user pastes "1.5k", it automatically expands to "1500"
136
- // Scientific notation like "1.5e-7" is always automatically expanded
137
- ```
138
-
139
- ### Scientific Notation Example
140
-
141
- ```tsx
142
- import { NumoraInput } from 'numora-react';
143
-
144
- function App() {
145
- return (
146
- <NumoraInput
147
- maxDecimals={18}
148
- onChange={(e) => {
149
- console.log('Value:', e.target.value);
150
- }}
151
- />
152
- );
153
- }
154
-
155
- // Scientific notation is ALWAYS automatically expanded
156
- // User can paste "1.5e-7" and it becomes "0.00000015"
157
- // User can paste "2e+5" and it becomes "200000"
158
- ```
159
-
160
- ### With Form Libraries
161
-
162
- #### React Hook Form
163
-
164
- `NumoraInput` works seamlessly with react-hook-form. The recommended approach is to use the `Controller` component, which is react-hook-form's official pattern for controlled components.
165
-
166
- **Recommended: Controller Pattern**
167
- ```tsx
168
- import { useForm, Controller } from 'react-hook-form';
169
- import { NumoraInput } from 'numora-react';
170
-
171
- function Form() {
172
- const { control, handleSubmit, setValue } = useForm();
173
-
174
- return (
175
- <form onSubmit={handleSubmit((data) => console.log(data))}>
176
- <Controller
177
- control={control}
178
- name="amount"
179
- render={({ field: { onChange, name, value } }) => (
180
- <NumoraInput
181
- name={name}
182
- value={value || ''}
183
- onChange={onChange}
184
- maxDecimals={2}
185
- thousandSeparator=","
186
- />
187
- )}
188
- />
189
- <button type="button" onClick={() => setValue('amount', '1000')}>
190
- Set to 1000
191
- </button>
192
- <button type="submit">Submit</button>
193
- </form>
194
- );
195
- }
196
- ```
197
-
198
- **Storing Raw Values** (for calculations):
199
- If you need to store raw values (without thousand separators) in your form state:
200
-
201
- ```tsx
202
- <Controller
203
- control={control}
204
- name="amount"
205
- render={({ field }) => (
206
- <NumoraInput
207
- value={field.value || ''}
208
- onChange={(e) => {
209
- // Store raw value - better for calculations
210
- field.onChange((e.target as any).rawValue);
211
- }}
212
- maxDecimals={2}
213
- thousandSeparator=","
26
+ onChange={(e) => console.log(e.target.value)}
214
27
  />
215
- )}
216
- />
217
- ```
218
-
219
- **Alternative: Register Pattern** (uncontrolled, basic forms only):
220
- ```tsx
221
- import { useForm } from 'react-hook-form';
222
- import { NumoraInput } from 'numora-react';
223
-
224
- function Form() {
225
- const { register, handleSubmit } = useForm();
226
-
227
- return (
228
- <form onSubmit={handleSubmit((data) => console.log(data))}>
229
- <NumoraInput
230
- {...register('amount')}
231
- maxDecimals={2}
232
- thousandSeparator=","
233
- />
234
- <button type="submit">Submit</button>
235
- </form>
236
28
  );
237
29
  }
238
30
  ```
239
31
 
240
- **Note:**
241
- - `numora-react` does not require `react-hook-form` as a dependency. It works with react-hook-form when it's present in your project.
242
- - The `Controller` pattern is recommended because it works seamlessly with `setValue()`, validation, and all react-hook-form features.
243
- - `NumoraInput` provides both formatted (`e.target.value`) and raw (`e.target.rawValue`) values in the onChange event.
244
-
245
- #### Formik
246
-
247
- ```tsx
248
- import { useFormik } from 'formik';
249
- import { NumoraInput } from 'numora-react';
250
-
251
- function Form() {
252
- const formik = useFormik({
253
- initialValues: { amount: '' },
254
- onSubmit: (values) => {
255
- console.log(values);
256
- },
257
- });
258
-
259
- return (
260
- <form onSubmit={formik.handleSubmit}>
261
- <NumoraInput
262
- name="amount"
263
- value={formik.values.amount}
264
- onChange={formik.handleChange}
265
- onBlur={formik.handleBlur}
266
- maxDecimals={2}
267
- thousandSeparator=","
268
- />
269
- <button type="submit">Submit</button>
270
- </form>
271
- );
272
- }
273
- ```
274
-
275
- ### Controlled Component
276
-
277
- ```tsx
278
- import { NumoraInput } from 'numora-react';
279
- import { useState } from 'react';
280
-
281
- function ControlledInput() {
282
- const [value, setValue] = useState('');
283
-
284
- return (
285
- <NumoraInput
286
- value={value}
287
- onChange={(e) => setValue(e.target.value)}
288
- maxDecimals={2}
289
- thousandSeparator=","
290
- />
291
- );
292
- }
293
- ```
294
-
295
- ### Uncontrolled Component
296
-
297
- ```tsx
298
- import { NumoraInput } from 'numora-react';
299
- import { useRef } from 'react';
300
-
301
- function UncontrolledInput() {
302
- const inputRef = useRef<HTMLInputElement>(null);
303
-
304
- const handleSubmit = () => {
305
- const value = inputRef.current?.value;
306
- console.log('Value:', value);
307
- };
32
+ ## Features
308
33
 
309
- return (
310
- <>
311
- <NumoraInput
312
- ref={inputRef}
313
- maxDecimals={2}
314
- thousandSeparator=","
315
- />
316
- <button onClick={handleSubmit}>Get Value</button>
317
- </>
318
- );
319
- }
320
- ```
34
+ - [Sanitization](https://numora.xyz/docs/numora-react/features/sanitization) - filters invalid characters and mobile keyboard artifacts
35
+ - [Formatting](https://numora.xyz/docs/numora-react/features/formatting) - thousand separators (Thousand/Lakh/Wan), format on blur or change
36
+ - [Decimals](https://numora.xyz/docs/numora-react/features/decimals) - configurable max decimal places and custom decimal separator
37
+ - [Compact Notation](https://numora.xyz/docs/numora-react/features/compact-notation) - expands "1k" → "1000" on paste (opt-in)
38
+ - [Scientific Notation](https://numora.xyz/docs/numora-react/features/scientific-notation) - always expands "1.5e-7" → "0.00000015" automatically
39
+ - [Leading Zeros](https://numora.xyz/docs/numora-react/features/leading-zeros) - configurable leading zero behavior
40
+ - [React Hook Form](https://numora.xyz/docs/numora-react/integrations/react-hook-form) - seamless integration via the `Controller` pattern
41
+ - Ref forwarding, controlled and uncontrolled modes, all standard input props
321
42
 
322
43
  ## Props
323
44
 
324
45
  | Prop | Type | Default | Description |
325
46
  |------|------|---------|-------------|
326
- | `maxDecimals` | `number` | `2` | Maximum number of decimal places allowed |
327
- | `formatOn` | `'blur' \| 'change'` | `'blur'` | When to apply formatting: `'blur'` or `'change'` |
328
- | `thousandSeparator` | `string` | `','` | Character used as thousand separator |
329
- | `thousandStyle` | `'thousand' \| 'lakh' \| 'wan'` | `'thousand'` | Grouping style for thousand separators |
330
- | `enableCompactNotation` | `boolean` | `false` | Enable compact notation expansion (e.g., `"1k"` → `"1000"`) on paste |
331
- | `onChange` | `(e: ChangeEvent<HTMLInputElement> \| ClipboardEvent<HTMLInputElement>) => void` | `undefined` | Callback when value changes |
332
- | `additionalStyle` | `string` | `undefined` | Additional CSS styles (deprecated, use `style` prop) |
333
-
334
- All standard HTMLInputElement props are also supported (e.g., `placeholder`, `className`, `disabled`, `id`, `onFocus`, `onBlur`, etc.), except:
335
- - `type` - Always set to `'text'` (required for formatting)
336
- - `inputMode` - Always set to `'decimal'` (for mobile keyboards)
337
-
338
- **Note:** Scientific notation expansion (e.g., `"1.5e-7"` → `"0.00000015"`) always happens automatically and is not configurable.
339
-
340
- ## API Reference
341
-
342
- ### NumoraInput
343
-
344
- A React component that wraps the core Numora functionality in a React-friendly API.
47
+ | `maxDecimals` | `number` | `2` | Maximum decimal places allowed |
48
+ | `formatOn` | `'blur' \| 'change'` | `'blur'` | When to apply formatting |
49
+ | `thousandSeparator` | `string` | `','` | Thousand separator character |
50
+ | `thousandStyle` | `'thousand' \| 'lakh' \| 'wan'` | `'thousand'` | Grouping style |
51
+ | `enableCompactNotation` | `boolean` | `false` | Expand compact notation on paste |
52
+ | `onChange` | `(e: ChangeEvent<HTMLInputElement>) => void` | `undefined` | Called when value changes |
345
53
 
346
- **Props:** See [Props](#props) section above.
54
+ All standard `HTMLInputElement` props are supported except `type` (always `'text'`) and `inputMode` (always `'decimal'`).
347
55
 
348
- **Ref:** The component forwards refs to the underlying `<input>` element.
349
-
350
- ## TypeScript
351
-
352
- Full TypeScript support is included. The component is typed with proper interfaces:
353
-
354
- ```tsx
355
- import { NumoraInput } from 'numora-react';
356
-
357
- // All props are fully typed
358
- const input = (
359
- <NumoraInput
360
- maxDecimals={18} // ✅ TypeScript knows this is a number
361
- formatOn="change" // ✅ TypeScript knows valid values
362
- enableCompactNotation={true} // ✅ TypeScript knows this is boolean
363
- onChange={(e) => {
364
- // ✅ e is properly typed as ChangeEvent<HTMLInputElement>
365
- console.log(e.target.value);
366
- }}
367
- />
368
- );
369
- ```
56
+ Both `e.target.value` (formatted) and `e.target.rawValue` (unformatted) are available in `onChange`.
370
57
 
371
- ## Related Packages
58
+ ## Documentation
372
59
 
373
- - [`numora`](../core/README.md) - Core framework-agnostic library with full feature set and display formatting utilities
60
+ Full docs and live demo at [numora.xyz/docs/numora-react](https://numora.xyz/docs/numora-react).
374
61
 
375
62
  ## License
376
63
 
package/dist/index.cjs ADDED
@@ -0,0 +1,244 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var react = require('react');
5
+ var numora = require('numora');
6
+
7
+ function handleNumoraOnChange(e, options) {
8
+ const { formatted, raw } = numora.handleOnChangeNumoraInput(e.nativeEvent, options.decimalMaxLength, options.caretPositionBeforeChange, options.formattingOptions);
9
+ return {
10
+ value: formatted,
11
+ rawValue: raw,
12
+ };
13
+ }
14
+ function handleNumoraOnPaste(e, options) {
15
+ const { formatted, raw } = numora.handleOnPasteNumoraInput(e.nativeEvent, options.decimalMaxLength, options.formattingOptions);
16
+ return {
17
+ value: formatted,
18
+ rawValue: raw,
19
+ };
20
+ }
21
+ function handleNumoraOnKeyDown(e, formattingOptions) {
22
+ return numora.handleOnKeyDownNumoraInput(e.nativeEvent, formattingOptions);
23
+ }
24
+ function handleNumoraOnBlur(e, options) {
25
+ if (options.formattingOptions.formatOn === numora.FormatOn.Blur) {
26
+ const { formatted, raw } = numora.formatValueForDisplay(e.target.value, options.decimalMaxLength, { ...options.formattingOptions, formatOn: numora.FormatOn.Change });
27
+ return {
28
+ value: formatted,
29
+ rawValue: raw,
30
+ };
31
+ }
32
+ return {
33
+ value: e.target.value,
34
+ rawValue: undefined,
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Creates a complete synthetic change event from a real HTMLInputElement.
40
+ * Used when a change needs to be signalled without an actual DOM change event
41
+ * (e.g. after paste with preventDefault, or after a controlled-value reformat).
42
+ */
43
+ function createSyntheticChangeEvent(input) {
44
+ const nativeEvent = new Event('change', { bubbles: true, cancelable: false });
45
+ return {
46
+ nativeEvent,
47
+ target: input,
48
+ currentTarget: input,
49
+ type: 'change',
50
+ bubbles: true,
51
+ cancelable: false,
52
+ defaultPrevented: false,
53
+ eventPhase: Event.AT_TARGET,
54
+ isTrusted: false,
55
+ timeStamp: Date.now(),
56
+ isDefaultPrevented: () => false,
57
+ isPropagationStopped: () => false,
58
+ persist: () => { },
59
+ preventDefault: () => { },
60
+ stopPropagation: () => { },
61
+ stopImmediatePropagation: () => { },
62
+ };
63
+ }
64
+ const NumoraInput = react.forwardRef((props, ref) => {
65
+ const { maxDecimals = 2, onChange, onPaste, onBlur, onKeyDown, onFocus, onRawValueChange, formatOn = numora.FormatOn.Blur, thousandSeparator, thousandStyle = numora.ThousandStyle.Thousand, decimalSeparator, decimalMinLength, enableCompactNotation = false, enableNegative = false, enableLeadingZeros = false, rawValueMode = false, value: controlledValue, defaultValue, ...rest } = props;
66
+ numora.validateNumoraInputOptions({
67
+ decimalMaxLength: maxDecimals,
68
+ decimalMinLength,
69
+ formatOn,
70
+ thousandSeparator,
71
+ thousandStyle,
72
+ decimalSeparator,
73
+ enableCompactNotation,
74
+ enableNegative,
75
+ enableLeadingZeros,
76
+ rawValueMode,
77
+ });
78
+ const internalInputRef = react.useRef(null);
79
+ const caretInfoRef = react.useRef(undefined);
80
+ const lastCaretPosRef = react.useRef(null);
81
+ // Memoize to give callbacks a stable reference - avoids recreating all
82
+ // useCallback functions on every render when primitive props haven't changed.
83
+ const formattingOptions = react.useMemo(() => {
84
+ const resolved = numora.resolveLocaleOptions({ thousandSeparator, thousandStyle, decimalSeparator });
85
+ return {
86
+ formatOn,
87
+ thousandSeparator: resolved.thousandSeparator,
88
+ ThousandStyle: resolved.thousandStyle,
89
+ decimalSeparator: resolved.decimalSeparator,
90
+ decimalMinLength,
91
+ enableCompactNotation,
92
+ enableNegative,
93
+ enableLeadingZeros,
94
+ rawValueMode,
95
+ };
96
+ }, [formatOn, thousandSeparator, thousandStyle, decimalSeparator, decimalMinLength,
97
+ enableCompactNotation, enableNegative, enableLeadingZeros, rawValueMode]);
98
+ const getInitialValue = () => {
99
+ const valueToFormat = controlledValue !== undefined ? controlledValue : defaultValue;
100
+ if (valueToFormat !== undefined) {
101
+ const { formatted } = numora.formatValueForDisplay(String(valueToFormat), maxDecimals, formattingOptions);
102
+ return formatted;
103
+ }
104
+ return '';
105
+ };
106
+ const [displayValue, setDisplayValue] = react.useState(getInitialValue);
107
+ // Track the current displayValue via a ref so the controlled-value useEffect
108
+ // can compare against it without adding displayValue as a dependency (which
109
+ // would cause the effect to re-run on every keystroke).
110
+ const displayValueRef = react.useRef(displayValue);
111
+ displayValueRef.current = displayValue;
112
+ // Sync external ref with internal ref
113
+ react.useLayoutEffect(() => {
114
+ if (!ref)
115
+ return;
116
+ if (typeof ref === 'function') {
117
+ ref(internalInputRef.current);
118
+ }
119
+ else {
120
+ ref.current = internalInputRef.current;
121
+ }
122
+ }, [ref]);
123
+ // When the controlled value or formatting options change, reformat the display.
124
+ // Uses displayValueRef (not displayValue in deps) to avoid re-running on every keystroke.
125
+ // Does NOT call onChange - that would create a circular loop with react-hook-form Controller.
126
+ react.useEffect(() => {
127
+ if (controlledValue !== undefined) {
128
+ const { formatted, raw } = numora.formatValueForDisplay(String(controlledValue), maxDecimals, formattingOptions);
129
+ if (formatted !== displayValueRef.current) {
130
+ setDisplayValue(formatted);
131
+ if (internalInputRef.current) {
132
+ internalInputRef.current.rawValue = raw;
133
+ }
134
+ onRawValueChange?.(raw);
135
+ }
136
+ }
137
+ }, [controlledValue, maxDecimals, formattingOptions, onRawValueChange]);
138
+ // Restore cursor position after render.
139
+ // No dependency array is intentional: this must run after every render so it catches
140
+ // the re-render triggered by setDisplayValue in handleChange/handlePaste.
141
+ // lastCaretPosRef is a ref (not reactive), so it cannot be a dependency.
142
+ react.useLayoutEffect(() => {
143
+ if (internalInputRef.current && lastCaretPosRef.current !== null) {
144
+ const input = internalInputRef.current;
145
+ const pos = lastCaretPosRef.current;
146
+ input.setSelectionRange(pos, pos);
147
+ lastCaretPosRef.current = null;
148
+ }
149
+ });
150
+ const handleChange = react.useCallback((e) => {
151
+ const { value, rawValue } = handleNumoraOnChange(e, {
152
+ decimalMaxLength: maxDecimals,
153
+ caretPositionBeforeChange: caretInfoRef.current,
154
+ formattingOptions,
155
+ });
156
+ if (internalInputRef.current) {
157
+ const cursorPos = internalInputRef.current.selectionStart;
158
+ if (cursorPos !== null && cursorPos !== undefined) {
159
+ lastCaretPosRef.current = cursorPos;
160
+ }
161
+ }
162
+ caretInfoRef.current = undefined;
163
+ e.target.rawValue = rawValue;
164
+ onRawValueChange?.(rawValue);
165
+ setDisplayValue(value);
166
+ if (onChange) {
167
+ onChange(e);
168
+ }
169
+ }, [maxDecimals, formattingOptions, onChange, onRawValueChange]);
170
+ const handleKeyDown = react.useCallback((e) => {
171
+ const coreCaretInfo = handleNumoraOnKeyDown(e, formattingOptions);
172
+ if (!coreCaretInfo && internalInputRef.current) {
173
+ const selectionStart = internalInputRef.current.selectionStart ?? 0;
174
+ const selectionEnd = internalInputRef.current.selectionEnd ?? 0;
175
+ caretInfoRef.current = {
176
+ selectionStart,
177
+ selectionEnd,
178
+ };
179
+ }
180
+ else {
181
+ caretInfoRef.current = coreCaretInfo;
182
+ }
183
+ if (onKeyDown) {
184
+ onKeyDown(e);
185
+ }
186
+ }, [formattingOptions, onKeyDown]);
187
+ const handlePaste = react.useCallback((e) => {
188
+ const { value, rawValue } = handleNumoraOnPaste(e, {
189
+ decimalMaxLength: maxDecimals,
190
+ formattingOptions,
191
+ });
192
+ lastCaretPosRef.current = e.target.selectionStart;
193
+ e.target.rawValue = rawValue;
194
+ onRawValueChange?.(rawValue);
195
+ setDisplayValue(value);
196
+ if (onPaste) {
197
+ onPaste(e);
198
+ }
199
+ // Paste calls e.preventDefault() internally, so React's onChange never fires.
200
+ // We synthesise a proper change event so consumers see a typed ChangeEvent.
201
+ if (onChange) {
202
+ onChange(createSyntheticChangeEvent(e.target));
203
+ }
204
+ }, [maxDecimals, formattingOptions, onPaste, onChange, onRawValueChange]);
205
+ const handleFocus = react.useCallback((e) => {
206
+ if (formattingOptions.formatOn === numora.FormatOn.Blur &&
207
+ formattingOptions.thousandSeparator &&
208
+ formattingOptions.ThousandStyle !== numora.ThousandStyle.None) {
209
+ // Read directly from the DOM element to avoid a stale displayValue closure
210
+ // and to eliminate displayValue from the deps array (which would recreate
211
+ // this callback on every keystroke).
212
+ const currentValue = e.target.value;
213
+ setDisplayValue(numora.removeThousandSeparators(currentValue, formattingOptions.thousandSeparator));
214
+ }
215
+ if (onFocus) {
216
+ onFocus(e);
217
+ }
218
+ }, [formattingOptions, onFocus]);
219
+ const handleBlur = react.useCallback((e) => {
220
+ const { value, rawValue } = handleNumoraOnBlur(e, {
221
+ decimalMaxLength: maxDecimals,
222
+ formattingOptions,
223
+ });
224
+ e.target.rawValue = rawValue;
225
+ onRawValueChange?.(rawValue);
226
+ setDisplayValue(value);
227
+ if (onBlur) {
228
+ onBlur(e);
229
+ }
230
+ }, [maxDecimals, formattingOptions, onBlur, onRawValueChange]);
231
+ return (jsxRuntime.jsx("input", { ...rest, ref: internalInputRef, value: displayValue, onChange: handleChange, onKeyDown: handleKeyDown, onPaste: handlePaste, onFocus: handleFocus, onBlur: handleBlur, type: "text", inputMode: "decimal", spellCheck: false, autoComplete: "off" }));
232
+ });
233
+ NumoraInput.displayName = 'NumoraInput';
234
+
235
+ Object.defineProperty(exports, "FormatOn", {
236
+ enumerable: true,
237
+ get: function () { return numora.FormatOn; }
238
+ });
239
+ Object.defineProperty(exports, "ThousandStyle", {
240
+ enumerable: true,
241
+ get: function () { return numora.ThousandStyle; }
242
+ });
243
+ exports.NumoraInput = NumoraInput;
244
+ //# sourceMappingURL=index.cjs.map