numeric-input-react 1.0.21 → 1.0.23

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.
@@ -1,645 +1,21 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
4
- /**
5
- * Converts full-width Japanese characters to half-width equivalents
6
- * Supports: numbers (0-9), period (.), comma (,), minus (-)
7
- */
8
- const convertFullWidthToHalfWidth = (str) => {
9
- return str
10
- .replace(/[0-9]/g, (char) => {
11
- // Convert full-width numbers (0-9) to half-width (0-9)
12
- return String.fromCharCode(char.charCodeAt(0) - 0xfee0);
13
- })
14
- .replace(/[.]/g, '.') // Convert full-width period (.) to half-width (.)
15
- .replace(/[,]/g, ',') // Convert full-width comma (,) to half-width (,)
16
- .replace(/[-]/g, '-') // Convert full-width minus (-, U+FF0D) to half-width (-)
17
- .replace(/[ー]/g, '-') // Convert katakana long vowel mark (ー, U+30FC) to minus (-) when used as minus
18
- .replace(/[−]/g, '-'); // Convert mathematical minus sign (−, U+2212) to half-width (-)
19
- };
20
- /**
21
- * Escapes special regex characters in a string
22
- */
23
- const escapeRegex = (str) => {
24
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
25
- };
26
- /**
27
- * Normalizes the input string by removing invalid characters
28
- * and ensuring proper decimal point handling
29
- */
30
- const normalizeNumericInput = (input, allowDecimal, allowNegative, maxLength) => {
31
- let normalized = input;
32
- // Remove all characters except digits, decimal point, and optionally minus sign
33
- const allowedChars = allowDecimal
34
- ? allowNegative
35
- ? /[^0-9.\-]/g
36
- : /[^0-9.]/g
37
- : allowNegative
38
- ? /[^0-9\-]/g
39
- : /[^0-9]/g;
40
- normalized = normalized.replace(allowedChars, '');
41
- // Handle negative sign: only allow at the start
42
- if (allowNegative) {
43
- const minusCount = (normalized.match(/-/g) || []).length;
44
- if (minusCount > 1) {
45
- // Keep only the first minus sign
46
- normalized = normalized.replace(/-/g, (match, offset) => {
47
- return offset === 0 ? match : '';
48
- });
49
- }
50
- // If minus is not at the start, move it to the start
51
- if (normalized.includes('-') && !normalized.startsWith('-')) {
52
- normalized = `-${normalized.replace(/-/g, '')}`;
53
- }
54
- }
55
- else {
56
- normalized = normalized.replace(/-/g, '');
57
- }
58
- // Handle decimal point: only allow one, and only if decimals are allowed
59
- if (allowDecimal) {
60
- const decimalCount = (normalized.match(/\./g) || []).length;
61
- if (decimalCount > 1) {
62
- // Keep only the first decimal point
63
- const firstDecimalIndex = normalized.indexOf('.');
64
- normalized =
65
- normalized.slice(0, firstDecimalIndex + 1) +
66
- normalized.slice(firstDecimalIndex + 1).replace(/\./g, '');
67
- }
68
- }
69
- else {
70
- normalized = normalized.replace(/\./g, '');
71
- }
72
- // Apply maxLength if specified
73
- // maxLength should only apply to digits, not to minus sign or decimal point
74
- if (maxLength) {
75
- // Count only digits in the normalized string
76
- const digitCount = (normalized.match(/\d/g) || []).length;
77
- if (digitCount > maxLength) {
78
- // Remove excess digits from the end, preserving minus sign and decimal point
79
- const hasMinus = normalized.startsWith('-');
80
- let result = hasMinus ? '-' : '';
81
- let digitsSeen = 0;
82
- let decimalAdded = false;
83
- // Start from after minus sign if present
84
- for (let i = hasMinus ? 1 : 0; i < normalized.length; i++) {
85
- const char = normalized[i];
86
- if (/\d/.test(char)) {
87
- // Only keep digits up to maxLength
88
- if (digitsSeen < maxLength) {
89
- result += char;
90
- digitsSeen++;
91
- }
92
- // Skip excess digits
93
- }
94
- else if (char === '.' && !decimalAdded) {
95
- // Only keep decimal point if we have at least one digit and haven't added one yet
96
- if (digitsSeen > 0) {
97
- result += char;
98
- decimalAdded = true;
99
- }
100
- }
101
- }
102
- normalized = result;
103
- }
104
- }
105
- return normalized;
106
- };
107
- function NumericInput({ value, className, separator, onValueChange, onCompositionStart, onCompositionEnd, onBlur, maxLength, allowDecimal = false, allowNegative = false, minValue, maxValue, maxDecimalPlaces, ...props }) {
108
- // Validate min/max values
109
- if (minValue !== undefined && maxValue !== undefined && minValue > maxValue) {
110
- console.warn('NumericInput: minValue should be less than or equal to maxValue');
111
- }
112
- const isComposing = useRef(false);
113
- const inputRef = useRef(null);
114
- // Store the raw input value during IME composition
115
- const [composingValue, setComposingValue] = useState('');
116
- // Track if we've already processed the value from composition end
117
- const hasProcessedComposition = useRef(false);
118
- // Store the raw input string to preserve leading zeros
119
- const [rawInputValue, setRawInputValue] = useState('');
120
- const formatValue = useCallback((numValue) => {
121
- if (Number.isNaN(numValue) || !Number.isFinite(numValue)) {
122
- return '';
123
- }
124
- const valueStr = numValue.toString();
125
- // If no separator, return as is
126
- if (!separator) {
127
- return valueStr;
128
- }
129
- // Split into integer and decimal parts
130
- const [integerPart, decimalPart] = valueStr.split('.');
131
- // Format integer part with separator (thousands separator)
132
- const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
133
- // Combine with decimal part if exists
134
- return decimalPart !== undefined
135
- ? `${formattedInteger}.${decimalPart}`
136
- : formattedInteger;
137
- }, [separator]);
138
- const handleValueChange = useCallback((inputValue, skipCompositionCheck = false) => {
139
- // During IME composition, update the composing value for display
140
- // Don't convert full-width to half-width yet - wait for composition end
141
- if (!skipCompositionCheck && isComposing.current) {
142
- setComposingValue(inputValue);
143
- // Store raw input value (could be full-width) for later processing
144
- setRawInputValue(inputValue);
145
- // Still notify parent but don't process the value
146
- onValueChange({
147
- value: 0,
148
- formattedValue: inputValue,
149
- });
150
- return;
151
- }
152
- // Convert full-width Japanese characters to half-width
153
- let rawValue = convertFullWidthToHalfWidth(inputValue);
154
- // Remove scientific notation (e.g., "1e10", "1E10")
155
- // This prevents unexpected number conversions
156
- rawValue = rawValue.replace(/[eE]/g, '');
157
- // Normalize the input (remove invalid chars, handle decimals, negatives)
158
- rawValue = normalizeNumericInput(rawValue, allowDecimal, allowNegative, maxLength);
159
- // Limit decimal places if specified
160
- if (maxDecimalPlaces !== undefined && allowDecimal) {
161
- const decimalIndex = rawValue.indexOf('.');
162
- if (decimalIndex !== -1) {
163
- const integerPart = rawValue.slice(0, decimalIndex);
164
- const decimalPart = rawValue.slice(decimalIndex + 1);
165
- if (decimalPart.length > maxDecimalPlaces) {
166
- rawValue = `${integerPart}.${decimalPart.slice(0, maxDecimalPlaces)}`;
167
- }
168
- }
169
- }
170
- // Handle empty input first (before processing leading zeros)
171
- if (rawValue === '') {
172
- setRawInputValue('');
173
- onValueChange({
174
- value: 0,
175
- formattedValue: '',
176
- });
177
- return;
178
- }
179
- // Handle only minus sign (half-width or full-width converted): preserve it if allowNegative is true
180
- if (rawValue === '-') {
181
- if (allowNegative) {
182
- setRawInputValue('-');
183
- onValueChange({
184
- value: 0,
185
- formattedValue: '-',
186
- });
187
- return;
188
- }
189
- else {
190
- // If negative is not allowed, treat as empty
191
- setRawInputValue('');
192
- onValueChange({
193
- value: 0,
194
- formattedValue: '',
195
- });
196
- return;
197
- }
198
- }
199
- // Remove leading zeros except for single "0" or "0." patterns
200
- // Only allow "0", "-0", "0.", "-0." to keep leading zero
201
- // For cases like "01", "0123", "09999", "00.1" → remove leading zeros
202
- const shouldKeepSingleZero = rawValue === '0' ||
203
- rawValue === '-0' ||
204
- rawValue === '0.' ||
205
- rawValue === '-0.';
206
- if (!shouldKeepSingleZero) {
207
- // Remove leading zeros (but keep the minus sign if present)
208
- if (rawValue.startsWith('-')) {
209
- const withoutMinus = rawValue.slice(1);
210
- // Split by decimal point to handle cases like "00.1"
211
- if (withoutMinus.includes('.')) {
212
- const [integerPart, decimalPart] = withoutMinus.split('.');
213
- const cleanedInteger = integerPart.replace(/^0+/, '');
214
- // If cleanedInteger is empty and there's a decimal part, keep "0"
215
- if (cleanedInteger === '' && decimalPart) {
216
- rawValue = `-0.${decimalPart}`;
217
- }
218
- else if (cleanedInteger === '') {
219
- rawValue = '-0';
220
- }
221
- else {
222
- rawValue = `-${cleanedInteger}.${decimalPart}`;
223
- }
224
- }
225
- else {
226
- const withoutLeadingZeros = withoutMinus.replace(/^0+/, '');
227
- rawValue =
228
- withoutLeadingZeros === '' ? '-0' : `-${withoutLeadingZeros}`;
229
- }
230
- }
231
- else {
232
- // Split by decimal point to handle cases like "00.1"
233
- if (rawValue.includes('.')) {
234
- const [integerPart, decimalPart] = rawValue.split('.');
235
- const cleanedInteger = integerPart.replace(/^0+/, '');
236
- // If cleanedInteger is empty and there's a decimal part, keep "0"
237
- if (cleanedInteger === '' && decimalPart) {
238
- rawValue = `0.${decimalPart}`;
239
- }
240
- else if (cleanedInteger === '') {
241
- rawValue = '0';
242
- }
243
- else {
244
- rawValue = `${cleanedInteger}.${decimalPart}`;
245
- }
246
- }
247
- else {
248
- const cleaned = rawValue.replace(/^0+/, '');
249
- rawValue = cleaned === '' ? '0' : cleaned;
250
- }
251
- }
252
- }
253
- // Store the raw input value to preserve single "0" only
254
- setRawInputValue(rawValue);
255
- // Convert to number
256
- const valueAsNumber = Number(rawValue);
257
- // Handle invalid numbers
258
- if (Number.isNaN(valueAsNumber) || !Number.isFinite(valueAsNumber)) {
259
- setRawInputValue('');
260
- onValueChange({
261
- value: 0,
262
- formattedValue: '',
263
- });
264
- return;
265
- }
266
- // Handle value exceeding MAX_SAFE_INTEGER
267
- if (Math.abs(valueAsNumber) > Number.MAX_SAFE_INTEGER) {
268
- const clampedValue = valueAsNumber > 0 ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER;
269
- const clampedString = clampedValue.toString();
270
- setRawInputValue(clampedString);
271
- onValueChange({
272
- value: clampedValue,
273
- formattedValue: formatValue(clampedValue),
274
- });
275
- return;
276
- }
277
- // Only preserve single "0" or "0." patterns (not multiple leading zeros like "01", "0123")
278
- const isSingleZero = rawValue === '0' ||
279
- rawValue === '-0' ||
280
- rawValue.startsWith('0.') ||
281
- rawValue.startsWith('-0.');
282
- // Check if the value ends with a decimal point (e.g., "2.", "-2.", "123.")
283
- // This allows users to continue typing decimal digits
284
- const endsWithDecimalPoint = allowDecimal && rawValue.endsWith('.') && !rawValue.endsWith('..');
285
- // Apply min/max validation only for complete numbers (not intermediate typing states)
286
- // Allow intermediate values while typing (e.g., allow "1000" if max is 100, user might be typing "100")
287
- let finalValue = valueAsNumber;
288
- let finalRawValue = rawValue;
289
- let shouldClamp = false;
290
- // Only clamp if the value is complete (not ending with decimal point and not a single zero pattern)
291
- if (!isSingleZero && !endsWithDecimalPoint) {
292
- if (minValue !== undefined && finalValue < minValue) {
293
- finalValue = minValue;
294
- finalRawValue = minValue.toString();
295
- shouldClamp = true;
296
- }
297
- if (maxValue !== undefined && finalValue > maxValue) {
298
- finalValue = maxValue;
299
- finalRawValue = maxValue.toString();
300
- shouldClamp = true;
301
- }
302
- }
303
- // If clamped, update rawInputValue
304
- if (shouldClamp) {
305
- setRawInputValue(finalRawValue);
306
- }
307
- // If it's a single zero pattern or ends with decimal point, use the raw value for display
308
- if (isSingleZero || endsWithDecimalPoint) {
309
- // Use the raw value as-is to preserve single "0" or trailing decimal point
310
- onValueChange({
311
- value: finalValue,
312
- formattedValue: shouldClamp ? formatValue(finalValue) : rawValue,
313
- });
314
- return;
315
- }
316
- // Valid number without leading zeros - format and return
317
- onValueChange({
318
- value: finalValue,
319
- formattedValue: formatValue(finalValue),
320
- });
321
- }, [
322
- allowDecimal,
323
- allowNegative,
324
- maxLength,
325
- onValueChange,
326
- formatValue,
327
- separator,
3
+ import { useNumericInput } from './use-numeric-input';
4
+ const NumericInput = ({ value, maxValue, minValue, separator, maxLength, className, maxDecimalPlaces, allowDecimal = false, allowNegative = false, onBlur, onValueChange, onCompositionEnd, onCompositionStart, ...props }) => {
5
+ const { inputRef, inputMode, displayValue, hasProcessedComposition, handleBlur, handleValueChange, handleCompositionEnd, handleCompositionStart, } = useNumericInput({
6
+ value,
328
7
  minValue,
329
8
  maxValue,
9
+ separator,
10
+ maxLength,
11
+ allowDecimal,
12
+ allowNegative,
330
13
  maxDecimalPlaces,
331
- ]);
332
- const handleCompositionStart = useCallback((e) => {
333
- isComposing.current = true;
334
- hasProcessedComposition.current = false;
335
- // Store the current input value when composition starts
336
- setComposingValue(e.currentTarget.value);
337
- // Handle custom onCompositionStart
338
- if (onCompositionStart) {
339
- onCompositionStart(e);
340
- }
341
- }, [onCompositionStart]);
342
- const handleCompositionEnd = useCallback((e) => {
343
- isComposing.current = false;
344
- const finalValue = e.currentTarget.value;
345
- // Clear the composing value
346
- setComposingValue('');
347
- // Mark that we've processed composition to prevent duplicate processing in onChange
348
- hasProcessedComposition.current = true;
349
- // Handle custom onCompositionEnd
350
- if (onCompositionEnd) {
351
- onCompositionEnd(e);
352
- }
353
- // Process the value after composition ends
354
- // Convert full-width to half-width and preserve minus sign if needed
355
- // Use requestAnimationFrame to ensure it happens after any pending onChange events
356
- requestAnimationFrame(() => {
357
- // Convert full-width to half-width before processing
358
- const convertedValue = convertFullWidthToHalfWidth(finalValue);
359
- // If the converted value is just a minus sign, preserve it
360
- if (allowNegative && convertedValue === '-') {
361
- setRawInputValue('-');
362
- onValueChange({
363
- value: 0,
364
- formattedValue: '-',
365
- });
366
- }
367
- else {
368
- // Process normally
369
- handleValueChange(convertedValue, true);
370
- }
371
- // Reset flag after processing
372
- hasProcessedComposition.current = false;
373
- });
374
- }, [onCompositionEnd, handleValueChange, allowNegative]);
375
- const handleBlur = useCallback((e) => {
376
- // Check if we need to preserve minus sign before processing
377
- // Check both half-width and full-width minus in rawInputValue and e.target.value
378
- // Also check katakana long vowel mark (ー) and mathematical minus sign (−) which can be used as minus
379
- const currentValue = e.target.value;
380
- const isCurrentValueMinus = currentValue === '-' || currentValue === '-' || currentValue === 'ー' || currentValue === '−';
381
- const isRawInputMinus = rawInputValue === '-' || rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−';
382
- const shouldPreserveMinus = allowNegative && (isRawInputMinus || isCurrentValueMinus);
383
- // If still composing when blur happens, force end composition
384
- if (isComposing.current) {
385
- isComposing.current = false;
386
- const finalValue = e.target.value;
387
- setComposingValue('');
388
- hasProcessedComposition.current = true;
389
- // Convert full-width to half-width before processing
390
- const convertedValue = convertFullWidthToHalfWidth(finalValue);
391
- // If the converted value is just a minus sign, preserve it
392
- if (allowNegative && convertedValue === '-') {
393
- setRawInputValue('-');
394
- onValueChange({
395
- value: 0,
396
- formattedValue: '-',
397
- });
398
- }
399
- else {
400
- // Process the value immediately
401
- handleValueChange(convertedValue, true);
402
- }
403
- }
404
- else if (composingValue !== '') {
405
- // If there's a composing value but not composing, process it
406
- // Convert full-width to half-width before processing
407
- const convertedValue = convertFullWidthToHalfWidth(composingValue);
408
- // If the converted value is just a minus sign, preserve it
409
- if (allowNegative && convertedValue === '-') {
410
- setRawInputValue('-');
411
- onValueChange({
412
- value: 0,
413
- formattedValue: '-',
414
- });
415
- }
416
- else {
417
- handleValueChange(convertedValue, true);
418
- }
419
- setComposingValue('');
420
- }
421
- else if (!hasProcessedComposition.current && e.target.value) {
422
- // If we haven't processed composition and there's a value, process it
423
- // Convert full-width to half-width before processing
424
- const convertedValue = convertFullWidthToHalfWidth(e.target.value);
425
- // Process the value - handleValueChange will preserve minus sign if present
426
- handleValueChange(convertedValue, true);
427
- }
428
- // Apply min/max validation on blur for any intermediate values
429
- // This ensures values are clamped even if user was typing an out-of-range value
430
- // But preserve intermediate states like "-" (minus sign only, half-width or full-width)
431
- if (rawInputValue !== '') {
432
- // Preserve minus sign only if allowNegative is true - skip clamp validation
433
- // Check half-width, full-width minus, katakana long vowel mark (ー), and mathematical minus sign (−)
434
- const isMinusOnly = allowNegative && (rawInputValue === '-' || rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−');
435
- if (!isMinusOnly) {
436
- // Convert to half-width for number conversion
437
- const convertedValue = convertFullWidthToHalfWidth(rawInputValue);
438
- const numValue = Number(convertedValue);
439
- if (!Number.isNaN(numValue) && Number.isFinite(numValue)) {
440
- let clampedValue = numValue;
441
- let shouldUpdate = false;
442
- if (minValue !== undefined && clampedValue < minValue) {
443
- clampedValue = minValue;
444
- shouldUpdate = true;
445
- }
446
- if (maxValue !== undefined && clampedValue > maxValue) {
447
- clampedValue = maxValue;
448
- shouldUpdate = true;
449
- }
450
- if (shouldUpdate) {
451
- const clampedString = clampedValue.toString();
452
- setRawInputValue(clampedString);
453
- onValueChange({
454
- value: clampedValue,
455
- formattedValue: formatValue(clampedValue),
456
- });
457
- }
458
- }
459
- }
460
- }
461
- // If we need to preserve minus sign (only when value is just minus, no numbers), ensure it's still set as half-width
462
- // Check both current rawInputValue and the value from input element
463
- // Only preserve if the value is just a minus sign, not if it has numbers (those are handled by handleValueChange)
464
- if (shouldPreserveMinus) {
465
- // Check if rawInputValue is just a minus sign (not a number with minus)
466
- const isJustMinus = rawInputValue === '-' || rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−';
467
- if (isJustMinus) {
468
- // Convert any full-width minus to half-width
469
- const finalMinusValue = '-';
470
- if (rawInputValue !== finalMinusValue) {
471
- setRawInputValue(finalMinusValue);
472
- onValueChange({
473
- value: 0,
474
- formattedValue: finalMinusValue,
475
- });
476
- }
477
- }
478
- // If rawInputValue has numbers (e.g., "-123"), handleValueChange already processed it correctly
479
- }
480
- // Reset the flag
481
- hasProcessedComposition.current = false;
482
- // Call custom onBlur if provided
483
- if (onBlur) {
484
- onBlur(e);
485
- }
486
- }, [composingValue, onBlur, handleValueChange, rawInputValue, minValue, maxValue, formatValue, allowNegative]);
487
- // Reset rawInputValue when value prop changes externally (e.g., form reset)
488
- useEffect(() => {
489
- if (value === null || value === undefined || value === '') {
490
- // Preserve "-", "-", "ー", or "−" if allowNegative is true and user is typing negative number
491
- if (allowNegative && (rawInputValue === '-' || rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−')) {
492
- return;
493
- }
494
- setRawInputValue('');
495
- return;
496
- }
497
- // Convert value to number if it's a string
498
- // Escape separator for regex if it exists
499
- const numValue = typeof value === 'string'
500
- ? Number(value.replace(new RegExp(`[${separator ? escapeRegex(separator) : ''}]`, 'g'), ''))
501
- : Number(value);
502
- // If the value is 0, preserve rawInputValue if it's "0", "-0", "0.", "-0.", "-", "-", "ー", or "−"
503
- // Also preserve negative numbers when allowNegative is true (user might be typing)
504
- // Otherwise, if value prop is 0 (controlled from outside), set rawInputValue to "0" to display it
505
- if (numValue === 0) {
506
- const isSingleZero = rawInputValue === '0' ||
507
- rawInputValue === '-0' ||
508
- rawInputValue === '-' ||
509
- rawInputValue === '-' ||
510
- rawInputValue === 'ー' ||
511
- rawInputValue === '−' ||
512
- rawInputValue.startsWith('0.') ||
513
- rawInputValue.startsWith('-0.');
514
- // Check if rawInputValue is a negative number (preserve it when allowNegative is true)
515
- if (allowNegative && rawInputValue !== '') {
516
- const convertedRawValue = convertFullWidthToHalfWidth(rawInputValue);
517
- const rawAsNumber = Number(convertedRawValue);
518
- // If it's a valid negative number, preserve it
519
- if (!Number.isNaN(rawAsNumber) && Number.isFinite(rawAsNumber) && rawAsNumber < 0) {
520
- return;
521
- }
522
- }
523
- if (!isSingleZero) {
524
- // If value prop is 0 from outside, we should display "0"
525
- // Set rawInputValue to "0" so it can be displayed
526
- setRawInputValue('0');
527
- }
528
- return;
529
- }
530
- // For non-zero values, check if the numeric value matches what we'd get from rawInputValue
531
- // But preserve intermediate states like "-", "-", or "ー" (minus sign only)
532
- // Also preserve negative numbers that start with minus sign when allowNegative is true
533
- if (rawInputValue !== '') {
534
- // Preserve minus sign only if allowNegative is true (half-width, full-width, katakana, and mathematical minus)
535
- if (allowNegative && (rawInputValue === '-' || rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−')) {
536
- // Don't clear rawInputValue if it's just a minus sign
537
- return;
538
- }
539
- // Convert to half-width for number comparison
540
- const convertedRawValue = convertFullWidthToHalfWidth(rawInputValue);
541
- const rawAsNumber = Number(convertedRawValue);
542
- // If rawInputValue starts with minus and allowNegative is true, preserve it
543
- // This handles cases where user is typing negative numbers and value prop might not match yet
544
- if (allowNegative && convertedRawValue.startsWith('-')) {
545
- // Always preserve negative numbers when allowNegative is true
546
- // Only clear if value prop is a positive number that clearly doesn't match
547
- // (e.g., rawInputValue is "-123" but numValue is 123 - signs differ)
548
- if (rawAsNumber === numValue) {
549
- // They match, keep rawInputValue
550
- return;
551
- }
552
- else if (numValue > 0 && Math.abs(rawAsNumber) === numValue) {
553
- // Value prop is positive but rawInputValue is negative with same absolute value
554
- // This means parent explicitly set a positive value, so clear rawInputValue
555
- setRawInputValue('');
556
- }
557
- else {
558
- // In all other cases (numValue is 0, negative, or doesn't match), preserve rawInputValue
559
- // This ensures user's typing is not lost
560
- return;
561
- }
562
- }
563
- else {
564
- // For non-negative values, check if they match
565
- if (rawAsNumber !== numValue) {
566
- // Value changed externally, clear rawInputValue
567
- setRawInputValue('');
568
- }
569
- }
570
- }
571
- }, [value, separator, rawInputValue, allowNegative]);
572
- // Format the display value
573
- const displayValue = useMemo(() => {
574
- // If currently composing, use the composing value (this allows IME input to display)
575
- if (composingValue !== '') {
576
- return composingValue;
577
- }
578
- // If rawInputValue is empty, check if we should display the value prop
579
- // This handles both: value prop from outside, and user deleting content
580
- if (rawInputValue === '') {
581
- if (value === null || value === undefined || value === '') {
582
- return '';
583
- }
584
- // Convert value to number if it's a string
585
- // Escape separator for regex if it exists
586
- const numValue = typeof value === 'string'
587
- ? Number(value.replace(new RegExp(`[${separator ? escapeRegex(separator) : ''}]`, 'g'), ''))
588
- : Number(value);
589
- // If value is 0 and rawInputValue is empty, show "0" (value prop from outside)
590
- // This allows displaying 0 when it's passed as a prop
591
- // Note: If user deletes content, onValueChange is called with formattedValue: '',
592
- // and parent should update value prop to null/undefined/'' to hide "0"
593
- if (numValue === 0) {
594
- return '0';
595
- }
596
- // For non-zero values, format and display them
597
- if (!Number.isNaN(numValue) && Number.isFinite(numValue)) {
598
- return formatValue(numValue);
599
- }
600
- return '';
601
- }
602
- // If we have a raw input value with single zero, minus sign only, or ending with decimal point, use it for display
603
- if (rawInputValue !== '') {
604
- const isSingleZero = rawInputValue === '0' ||
605
- rawInputValue === '-0' ||
606
- rawInputValue.startsWith('0.') ||
607
- rawInputValue.startsWith('-0.');
608
- // Check half-width, full-width minus, katakana long vowel mark (ー), and mathematical minus sign (−)
609
- const isMinusOnly = allowNegative && (rawInputValue === '-' || rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−');
610
- const endsWithDecimalPoint = allowDecimal &&
611
- rawInputValue.endsWith('.') &&
612
- !rawInputValue.endsWith('..');
613
- if (isSingleZero || isMinusOnly || endsWithDecimalPoint) {
614
- // If it's full-width minus, katakana long vowel mark, or mathematical minus sign, convert to half-width for display
615
- if (rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−') {
616
- return '-';
617
- }
618
- return rawInputValue;
619
- }
620
- // If rawInputValue is not empty and doesn't match special cases, use it to calculate display value
621
- // This handles the case where user is typing but value prop hasn't been updated yet
622
- const rawAsNumber = Number(rawInputValue);
623
- if (!Number.isNaN(rawAsNumber) && Number.isFinite(rawAsNumber)) {
624
- return formatValue(rawAsNumber);
625
- }
626
- }
627
- if (value === null || value === undefined || value === '') {
628
- return '';
629
- }
630
- // Convert value to number if it's a string
631
- // Escape separator for regex if it exists
632
- const numValue = typeof value === 'string'
633
- ? Number(value.replace(new RegExp(`[${separator ? escapeRegex(separator) : ''}]`, 'g'), ''))
634
- : Number(value);
635
- if (Number.isNaN(numValue)) {
636
- return '';
637
- }
638
- // Format and return the value
639
- return formatValue(numValue);
640
- }, [value, formatValue, separator, composingValue, rawInputValue, allowNegative, allowDecimal]);
641
- // Determine appropriate inputMode for mobile keyboards
642
- const inputMode = allowDecimal ? 'decimal' : 'numeric';
14
+ onBlur,
15
+ onValueChange,
16
+ onCompositionEnd,
17
+ onCompositionStart,
18
+ });
643
19
  return (_jsx("input", { ref: inputRef, type: "text", inputMode: inputMode, value: displayValue, className: className, onCompositionEnd: handleCompositionEnd, onCompositionStart: handleCompositionStart, onBlur: handleBlur, onChange: (e) => {
644
20
  // Skip onChange if we just processed composition to avoid duplicate processing
645
21
  // This prevents duplicate when composition end and onChange fire in quick succession
@@ -648,6 +24,6 @@ function NumericInput({ value, className, separator, onValueChange, onCompositio
648
24
  }
649
25
  handleValueChange(e.target.value);
650
26
  }, ...props }));
651
- }
27
+ };
652
28
  export { NumericInput };
653
29
  //# sourceMappingURL=numeric-input.js.map