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 +23 -336
- package/dist/index.cjs +244 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.mjs +91 -471
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -6
- package/rollup.config.mjs +2 -3
- package/src/index.tsx +117 -55
- package/tsconfig.json +1 -1
package/README.md
CHANGED
|
@@ -3,69 +3,17 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/numora-react)
|
|
4
4
|
[](https://www.npmjs.com/package/numora-react)
|
|
5
5
|
|
|
6
|
-
React component wrapper for [
|
|
6
|
+
React component wrapper for [numora](../core/README.md) - a precision-first numeric input library.
|
|
7
7
|
|
|
8
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
327
|
-
| `formatOn` | `'blur' \| 'change'` | `'blur'` | When to apply formatting
|
|
328
|
-
| `thousandSeparator` | `string` | `','` |
|
|
329
|
-
| `thousandStyle` | `'thousand' \| 'lakh' \| 'wan'` | `'thousand'` | Grouping style
|
|
330
|
-
| `enableCompactNotation` | `boolean` | `false` |
|
|
331
|
-
| `onChange` | `(e: ChangeEvent<HTMLInputElement>
|
|
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
|
-
|
|
54
|
+
All standard `HTMLInputElement` props are supported except `type` (always `'text'`) and `inputMode` (always `'decimal'`).
|
|
347
55
|
|
|
348
|
-
|
|
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
|
-
##
|
|
58
|
+
## Documentation
|
|
372
59
|
|
|
373
|
-
|
|
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
|