numeric-input-react 1.0.22 → 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.
@@ -0,0 +1,15 @@
1
+ import type { ComponentProps } from 'react';
2
+ export type NumericInputValue = {
3
+ value: number;
4
+ formattedValue: string;
5
+ };
6
+ export type NumericInputProps = ComponentProps<'input'> & {
7
+ onValueChange: (valueObject: NumericInputValue) => void;
8
+ separator?: string;
9
+ allowDecimal?: boolean;
10
+ allowNegative?: boolean;
11
+ minValue?: number;
12
+ maxValue?: number;
13
+ maxDecimalPlaces?: number;
14
+ };
15
+ //# sourceMappingURL=numeric-input.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"numeric-input.types.d.ts","sourceRoot":"","sources":["../src/numeric-input.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAE3C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,cAAc,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAC,OAAO,CAAC,GAAG;IACxD,aAAa,EAAE,CAAC,WAAW,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACvD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=numeric-input.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"numeric-input.types.js","sourceRoot":"","sources":["../src/numeric-input.types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Converts full-width Japanese characters to half-width equivalents
3
+ * Supports: numbers (0-9), period (.), comma (,), minus (-)
4
+ */
5
+ export declare const convertFullWidthToHalfWidth: (str: string) => string;
6
+ /**
7
+ * Escapes special regex characters in a string
8
+ */
9
+ export declare const escapeRegex: (str: string) => string;
10
+ /**
11
+ * Checks if a string is a minus sign (half-width, full-width, katakana, or mathematical)
12
+ */
13
+ export declare const isMinusSign: (str: string) => boolean;
14
+ /**
15
+ * Converts any minus sign variant to half-width minus
16
+ */
17
+ export declare const normalizeMinusSign: (str: string) => string;
18
+ /**
19
+ * Parses a value prop (string or number) to a number, removing separator if present
20
+ */
21
+ export declare const parseValueProp: (value: string | number | readonly string[] | null | undefined, separator?: string) => number;
22
+ /**
23
+ * Normalizes the input string by removing invalid characters
24
+ * and ensuring proper decimal point handling
25
+ */
26
+ export declare const normalizeNumericInput: (input: string, allowDecimal: boolean, allowNegative: boolean, maxLength?: number) => string;
27
+ //# sourceMappingURL=numeric-input.utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"numeric-input.utils.d.ts","sourceRoot":"","sources":["../src/numeric-input.utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,2BAA2B,GAAI,KAAK,MAAM,KAAG,MAWzD,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAEzC,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,OAEzC,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,KAAK,MAAM,KAAG,MAEhD,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,EAC7D,YAAY,MAAM,KACjB,MA2BF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAChC,OAAO,MAAM,EACb,cAAc,OAAO,EACrB,eAAe,OAAO,EACtB,YAAY,MAAM,KACjB,MAiFF,CAAA"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Converts full-width Japanese characters to half-width equivalents
3
+ * Supports: numbers (0-9), period (.), comma (,), minus (-)
4
+ */
5
+ export const convertFullWidthToHalfWidth = (str) => {
6
+ return str
7
+ .replace(/[0-9]/g, (char) => {
8
+ // Convert full-width numbers (0-9) to half-width (0-9)
9
+ return String.fromCharCode(char.charCodeAt(0) - 0xfee0);
10
+ })
11
+ .replace(/[.]/g, '.') // Convert full-width period (.) to half-width (.)
12
+ .replace(/[,]/g, ',') // Convert full-width comma (,) to half-width (,)
13
+ .replace(/[-]/g, '-') // Convert full-width minus (-, U+FF0D) to half-width (-)
14
+ .replace(/[ー]/g, '-') // Convert katakana long vowel mark (ー, U+30FC) to minus (-) when used as minus
15
+ .replace(/[−]/g, '-'); // Convert mathematical minus sign (−, U+2212) to half-width (-)
16
+ };
17
+ /**
18
+ * Escapes special regex characters in a string
19
+ */
20
+ export const escapeRegex = (str) => {
21
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
22
+ };
23
+ /**
24
+ * Checks if a string is a minus sign (half-width, full-width, katakana, or mathematical)
25
+ */
26
+ export const isMinusSign = (str) => {
27
+ return str === '-' || str === '-' || str === 'ー' || str === '−';
28
+ };
29
+ /**
30
+ * Converts any minus sign variant to half-width minus
31
+ */
32
+ export const normalizeMinusSign = (str) => {
33
+ return isMinusSign(str) ? '-' : str;
34
+ };
35
+ /**
36
+ * Parses a value prop (string or number) to a number, removing separator if present
37
+ */
38
+ export const parseValueProp = (value, separator) => {
39
+ if (value === null || value === undefined || value === '') {
40
+ return NaN;
41
+ }
42
+ if (typeof value === 'number') {
43
+ return value;
44
+ }
45
+ // Handle array case (shouldn't happen for numeric input, but handle gracefully)
46
+ if (Array.isArray(value)) {
47
+ const firstValue = value[0];
48
+ if (!firstValue) {
49
+ return NaN;
50
+ }
51
+ const cleanedValue = separator
52
+ ? firstValue.replace(new RegExp(`[${escapeRegex(separator)}]`, 'g'), '')
53
+ : firstValue;
54
+ return Number(cleanedValue);
55
+ }
56
+ // At this point, value must be a string
57
+ const cleanedValue = separator
58
+ ? value.replace(new RegExp(`[${escapeRegex(separator)}]`, 'g'), '')
59
+ : value;
60
+ return Number(cleanedValue);
61
+ };
62
+ /**
63
+ * Normalizes the input string by removing invalid characters
64
+ * and ensuring proper decimal point handling
65
+ */
66
+ export const normalizeNumericInput = (input, allowDecimal, allowNegative, maxLength) => {
67
+ let normalized = input;
68
+ // Remove all characters except digits, decimal point, and optionally minus sign
69
+ const allowedChars = allowDecimal
70
+ ? allowNegative
71
+ ? /[^0-9.\-]/g
72
+ : /[^0-9.]/g
73
+ : allowNegative
74
+ ? /[^0-9\-]/g
75
+ : /[^0-9]/g;
76
+ normalized = normalized.replace(allowedChars, '');
77
+ // Handle negative sign: only allow at the start
78
+ if (allowNegative) {
79
+ const minusCount = (normalized.match(/-/g) || []).length;
80
+ if (minusCount > 1) {
81
+ // Keep only the first minus sign
82
+ normalized = normalized.replace(/-/g, (match, offset) => {
83
+ return offset === 0 ? match : '';
84
+ });
85
+ }
86
+ // If minus is not at the start, move it to the start
87
+ if (normalized.includes('-') && !normalized.startsWith('-')) {
88
+ normalized = `-${normalized.replace(/-/g, '')}`;
89
+ }
90
+ }
91
+ else {
92
+ normalized = normalized.replace(/-/g, '');
93
+ }
94
+ // Handle decimal point: only allow one, and only if decimals are allowed
95
+ if (allowDecimal) {
96
+ const decimalCount = (normalized.match(/\./g) || []).length;
97
+ if (decimalCount > 1) {
98
+ // Keep only the first decimal point
99
+ const firstDecimalIndex = normalized.indexOf('.');
100
+ normalized =
101
+ normalized.slice(0, firstDecimalIndex + 1) +
102
+ normalized.slice(firstDecimalIndex + 1).replace(/\./g, '');
103
+ }
104
+ }
105
+ else {
106
+ normalized = normalized.replace(/\./g, '');
107
+ }
108
+ // Apply maxLength if specified
109
+ // maxLength should only apply to digits, not to minus sign or decimal point
110
+ if (maxLength) {
111
+ // Count only digits in the normalized string
112
+ const digitCount = (normalized.match(/\d/g) || []).length;
113
+ if (digitCount > maxLength) {
114
+ // Remove excess digits from the end, preserving minus sign and decimal point
115
+ const hasMinus = normalized.startsWith('-');
116
+ let result = hasMinus ? '-' : '';
117
+ let digitsSeen = 0;
118
+ let decimalAdded = false;
119
+ // Start from after minus sign if present
120
+ for (let i = hasMinus ? 1 : 0; i < normalized.length; i++) {
121
+ const char = normalized[i];
122
+ if (/\d/.test(char)) {
123
+ // Only keep digits up to maxLength
124
+ if (digitsSeen < maxLength) {
125
+ result += char;
126
+ digitsSeen++;
127
+ }
128
+ // Skip excess digits
129
+ }
130
+ else if (char === '.' && !decimalAdded) {
131
+ // Only keep decimal point if we have at least one digit and haven't added one yet
132
+ if (digitsSeen > 0) {
133
+ result += char;
134
+ decimalAdded = true;
135
+ }
136
+ }
137
+ }
138
+ normalized = result;
139
+ }
140
+ }
141
+ return normalized;
142
+ };
143
+ //# sourceMappingURL=numeric-input.utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"numeric-input.utils.js","sourceRoot":"","sources":["../src/numeric-input.utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { type CompositionEvent, type FocusEvent } from 'react';
2
+ import type { NumericInputValue, NumericInputProps } from './numeric-input.types';
3
+ type UseNumericInputOptions = {
4
+ value: NumericInputProps['value'];
5
+ separator?: string;
6
+ allowDecimal?: boolean;
7
+ allowNegative?: boolean;
8
+ minValue?: number;
9
+ maxValue?: number;
10
+ maxDecimalPlaces?: number;
11
+ maxLength?: number;
12
+ onValueChange: (valueObject: NumericInputValue) => void;
13
+ onCompositionStart?: NumericInputProps['onCompositionStart'];
14
+ onCompositionEnd?: NumericInputProps['onCompositionEnd'];
15
+ onBlur?: NumericInputProps['onBlur'];
16
+ };
17
+ export declare const useNumericInput: (options: UseNumericInputOptions) => {
18
+ inputRef: import("react").RefObject<HTMLInputElement | null>;
19
+ inputMode: "decimal" | "numeric";
20
+ displayValue: string;
21
+ hasProcessedComposition: import("react").RefObject<boolean>;
22
+ handleBlur: (e: FocusEvent<HTMLInputElement>) => void;
23
+ handleValueChange: (inputValue: string, skipCompositionCheck?: boolean) => void;
24
+ handleCompositionEnd: (e: CompositionEvent<HTMLInputElement>) => void;
25
+ handleCompositionStart: (e: CompositionEvent<HTMLInputElement>) => void;
26
+ };
27
+ export {};
28
+ //# sourceMappingURL=use-numeric-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-numeric-input.d.ts","sourceRoot":"","sources":["../src/use-numeric-input.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAMhB,MAAM,OAAO,CAAA;AAQd,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAEjF,KAAK,sBAAsB,GAAG;IAC5B,KAAK,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAA;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,CAAC,WAAW,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACvD,kBAAkB,CAAC,EAAE,iBAAiB,CAAC,oBAAoB,CAAC,CAAA;IAC5D,gBAAgB,CAAC,EAAE,iBAAiB,CAAC,kBAAkB,CAAC,CAAA;IACxD,MAAM,CAAC,EAAE,iBAAiB,CAAC,QAAQ,CAAC,CAAA;CACrC,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,SAAS,sBAAsB;;;;;oBAwTvD,UAAU,CAAC,gBAAgB,CAAC;oCAzPnB,MAAM;8BAqOf,gBAAgB,CAAC,gBAAgB,CAAC;gCA/BlC,gBAAgB,CAAC,gBAAgB,CAAC;CAgQzC,CAAA"}
@@ -0,0 +1,420 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
2
+ import { convertFullWidthToHalfWidth, isMinusSign, normalizeMinusSign, normalizeNumericInput, parseValueProp, } from './numeric-input.utils';
3
+ export const useNumericInput = (options) => {
4
+ const { value, maxValue, minValue, separator, maxLength, maxDecimalPlaces, allowDecimal = false, allowNegative = false, onBlur, onValueChange, onCompositionEnd, onCompositionStart, } = options;
5
+ // Validate min/max values
6
+ if (minValue !== undefined && maxValue !== undefined && minValue > maxValue) {
7
+ console.warn('NumericInput: minValue should be less than or equal to maxValue');
8
+ }
9
+ const isComposing = useRef(false);
10
+ const inputRef = useRef(null);
11
+ // Store the raw input value during IME composition
12
+ const [composingValue, setComposingValue] = useState('');
13
+ // Track if we've already processed the value from composition end
14
+ const hasProcessedComposition = useRef(false);
15
+ // Store the raw input string to preserve leading zeros
16
+ const [rawInputValue, setRawInputValue] = useState('');
17
+ const formatValue = useCallback((numValue) => {
18
+ if (Number.isNaN(numValue) || !Number.isFinite(numValue)) {
19
+ return '';
20
+ }
21
+ const valueStr = numValue.toString();
22
+ // If no separator, return as is
23
+ if (!separator) {
24
+ return valueStr;
25
+ }
26
+ // Split into integer and decimal parts
27
+ const [integerPart, decimalPart] = valueStr.split('.');
28
+ // Format integer part with separator (thousands separator)
29
+ const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
30
+ // Combine with decimal part if exists
31
+ return decimalPart !== undefined
32
+ ? `${formattedInteger}.${decimalPart}`
33
+ : formattedInteger;
34
+ }, [separator]);
35
+ const handleValueChange = useCallback((inputValue, skipCompositionCheck = false) => {
36
+ // During IME composition, update the composing value for display
37
+ // Don't convert full-width to half-width yet - wait for composition end
38
+ if (!skipCompositionCheck && isComposing.current) {
39
+ setComposingValue(inputValue);
40
+ // Store raw input value (could be full-width) for later processing
41
+ setRawInputValue(inputValue);
42
+ // Still notify parent but don't process the value
43
+ onValueChange({
44
+ value: 0,
45
+ formattedValue: inputValue,
46
+ });
47
+ return;
48
+ }
49
+ // Convert full-width Japanese characters to half-width
50
+ let rawValue = convertFullWidthToHalfWidth(inputValue);
51
+ // Remove scientific notation (e.g., "1e10", "1E10")
52
+ // This prevents unexpected number conversions
53
+ rawValue = rawValue.replace(/[eE]/g, '');
54
+ // Normalize the input (remove invalid chars, handle decimals, negatives)
55
+ rawValue = normalizeNumericInput(rawValue, allowDecimal, allowNegative, maxLength);
56
+ // Limit decimal places if specified
57
+ if (maxDecimalPlaces !== undefined && allowDecimal) {
58
+ const decimalIndex = rawValue.indexOf('.');
59
+ if (decimalIndex !== -1) {
60
+ const integerPart = rawValue.slice(0, decimalIndex);
61
+ const decimalPart = rawValue.slice(decimalIndex + 1);
62
+ if (decimalPart.length > maxDecimalPlaces) {
63
+ rawValue = `${integerPart}.${decimalPart.slice(0, maxDecimalPlaces)}`;
64
+ }
65
+ }
66
+ }
67
+ // Handle empty input first (before processing leading zeros)
68
+ if (rawValue === '') {
69
+ setRawInputValue('');
70
+ onValueChange({
71
+ value: 0,
72
+ formattedValue: '',
73
+ });
74
+ return;
75
+ }
76
+ // Handle only minus sign (half-width or full-width converted): preserve it if allowNegative is true
77
+ if (rawValue === '-') {
78
+ if (allowNegative) {
79
+ setRawInputValue('-');
80
+ onValueChange({
81
+ value: 0,
82
+ formattedValue: '-',
83
+ });
84
+ return;
85
+ }
86
+ else {
87
+ // If negative is not allowed, treat as empty
88
+ setRawInputValue('');
89
+ onValueChange({
90
+ value: 0,
91
+ formattedValue: '',
92
+ });
93
+ return;
94
+ }
95
+ }
96
+ // Remove leading zeros except for single "0" or "0." patterns
97
+ const shouldKeepSingleZero = rawValue === '0' ||
98
+ rawValue === '-0' ||
99
+ rawValue === '0.' ||
100
+ rawValue === '-0.';
101
+ if (!shouldKeepSingleZero) {
102
+ const hasMinus = rawValue.startsWith('-');
103
+ const withoutMinus = hasMinus ? rawValue.slice(1) : rawValue;
104
+ const hasDecimal = withoutMinus.includes('.');
105
+ if (hasDecimal) {
106
+ const [integerPart, decimalPart] = withoutMinus.split('.');
107
+ const cleanedInteger = integerPart.replace(/^0+/, '');
108
+ const prefix = hasMinus ? '-' : '';
109
+ if (cleanedInteger === '' && decimalPart) {
110
+ rawValue = `${prefix}0.${decimalPart}`;
111
+ }
112
+ else if (cleanedInteger === '') {
113
+ rawValue = `${prefix}0`;
114
+ }
115
+ else {
116
+ rawValue = `${prefix}${cleanedInteger}.${decimalPart}`;
117
+ }
118
+ }
119
+ else {
120
+ const cleaned = withoutMinus.replace(/^0+/, '');
121
+ const prefix = hasMinus ? '-' : '';
122
+ rawValue = cleaned === '' ? `${prefix}0` : `${prefix}${cleaned}`;
123
+ }
124
+ }
125
+ // Store the raw input value to preserve single "0" only
126
+ setRawInputValue(rawValue);
127
+ // Convert to number
128
+ const valueAsNumber = Number(rawValue);
129
+ // Handle invalid numbers
130
+ if (Number.isNaN(valueAsNumber) || !Number.isFinite(valueAsNumber)) {
131
+ setRawInputValue('');
132
+ onValueChange({
133
+ value: 0,
134
+ formattedValue: '',
135
+ });
136
+ return;
137
+ }
138
+ // Handle value exceeding MAX_SAFE_INTEGER
139
+ if (Math.abs(valueAsNumber) > Number.MAX_SAFE_INTEGER) {
140
+ const clampedValue = valueAsNumber > 0 ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER;
141
+ const clampedString = clampedValue.toString();
142
+ setRawInputValue(clampedString);
143
+ onValueChange({
144
+ value: clampedValue,
145
+ formattedValue: formatValue(clampedValue),
146
+ });
147
+ return;
148
+ }
149
+ // Only preserve single "0" or "0." patterns (not multiple leading zeros like "01", "0123")
150
+ const isSingleZero = rawValue === '0' ||
151
+ rawValue === '-0' ||
152
+ rawValue.startsWith('0.') ||
153
+ rawValue.startsWith('-0.');
154
+ // Check if the value ends with a decimal point (e.g., "2.", "-2.", "123.")
155
+ // This allows users to continue typing decimal digits
156
+ const endsWithDecimalPoint = allowDecimal && rawValue.endsWith('.') && !rawValue.endsWith('..');
157
+ // Apply min/max validation only for complete numbers (not intermediate typing states)
158
+ // Allow intermediate values while typing (e.g., allow "1000" if max is 100, user might be typing "100")
159
+ let finalValue = valueAsNumber;
160
+ let finalRawValue = rawValue;
161
+ let shouldClamp = false;
162
+ // Only clamp if the value is complete (not ending with decimal point and not a single zero pattern)
163
+ if (!isSingleZero && !endsWithDecimalPoint) {
164
+ if (minValue !== undefined && finalValue < minValue) {
165
+ finalValue = minValue;
166
+ finalRawValue = minValue.toString();
167
+ shouldClamp = true;
168
+ }
169
+ if (maxValue !== undefined && finalValue > maxValue) {
170
+ finalValue = maxValue;
171
+ finalRawValue = maxValue.toString();
172
+ shouldClamp = true;
173
+ }
174
+ }
175
+ // If clamped, update rawInputValue
176
+ if (shouldClamp) {
177
+ setRawInputValue(finalRawValue);
178
+ }
179
+ // If it's a single zero pattern or ends with decimal point, use the raw value for display
180
+ if (isSingleZero || endsWithDecimalPoint) {
181
+ // Use the raw value as-is to preserve single "0" or trailing decimal point
182
+ onValueChange({
183
+ value: finalValue,
184
+ formattedValue: shouldClamp ? formatValue(finalValue) : rawValue,
185
+ });
186
+ return;
187
+ }
188
+ // Valid number without leading zeros - format and return
189
+ onValueChange({
190
+ value: finalValue,
191
+ formattedValue: formatValue(finalValue),
192
+ });
193
+ }, [
194
+ allowDecimal,
195
+ allowNegative,
196
+ maxLength,
197
+ onValueChange,
198
+ formatValue,
199
+ separator,
200
+ minValue,
201
+ maxValue,
202
+ maxDecimalPlaces,
203
+ ]);
204
+ const handleCompositionStart = useCallback((e) => {
205
+ isComposing.current = true;
206
+ hasProcessedComposition.current = false;
207
+ // Store the current input value when composition starts
208
+ setComposingValue(e.currentTarget.value);
209
+ // Handle custom onCompositionStart
210
+ if (onCompositionStart) {
211
+ onCompositionStart(e);
212
+ }
213
+ }, [onCompositionStart]);
214
+ // Helper to process value after conversion (used in composition end and blur)
215
+ const processConvertedValue = useCallback((convertedValue) => {
216
+ if (allowNegative && convertedValue === '-') {
217
+ setRawInputValue('-');
218
+ onValueChange({
219
+ value: 0,
220
+ formattedValue: '-',
221
+ });
222
+ }
223
+ else {
224
+ handleValueChange(convertedValue, true);
225
+ }
226
+ }, [allowNegative, handleValueChange, onValueChange]);
227
+ const handleCompositionEnd = useCallback((e) => {
228
+ isComposing.current = false;
229
+ const finalValue = e.currentTarget.value;
230
+ setComposingValue('');
231
+ hasProcessedComposition.current = true;
232
+ if (onCompositionEnd) {
233
+ onCompositionEnd(e);
234
+ }
235
+ requestAnimationFrame(() => {
236
+ const convertedValue = convertFullWidthToHalfWidth(finalValue);
237
+ processConvertedValue(convertedValue);
238
+ hasProcessedComposition.current = false;
239
+ });
240
+ }, [onCompositionEnd, processConvertedValue]);
241
+ const handleBlur = useCallback((e) => {
242
+ const currentValue = e.target.value;
243
+ const shouldPreserveMinus = allowNegative && (isMinusSign(rawInputValue) || isMinusSign(currentValue));
244
+ // Handle composition states
245
+ if (isComposing.current) {
246
+ isComposing.current = false;
247
+ const convertedValue = convertFullWidthToHalfWidth(e.target.value);
248
+ setComposingValue('');
249
+ hasProcessedComposition.current = true;
250
+ processConvertedValue(convertedValue);
251
+ }
252
+ else if (composingValue !== '') {
253
+ const convertedValue = convertFullWidthToHalfWidth(composingValue);
254
+ processConvertedValue(convertedValue);
255
+ setComposingValue('');
256
+ }
257
+ else if (!hasProcessedComposition.current && e.target.value) {
258
+ const convertedValue = convertFullWidthToHalfWidth(e.target.value);
259
+ handleValueChange(convertedValue, true);
260
+ }
261
+ // Apply min/max validation on blur
262
+ if (rawInputValue !== '' && !(allowNegative && isMinusSign(rawInputValue))) {
263
+ const convertedValue = convertFullWidthToHalfWidth(rawInputValue);
264
+ const numValue = Number(convertedValue);
265
+ if (!Number.isNaN(numValue) && Number.isFinite(numValue)) {
266
+ let clampedValue = numValue;
267
+ let shouldUpdate = false;
268
+ if (minValue !== undefined && clampedValue < minValue) {
269
+ clampedValue = minValue;
270
+ shouldUpdate = true;
271
+ }
272
+ if (maxValue !== undefined && clampedValue > maxValue) {
273
+ clampedValue = maxValue;
274
+ shouldUpdate = true;
275
+ }
276
+ if (shouldUpdate) {
277
+ const clampedString = clampedValue.toString();
278
+ setRawInputValue(clampedString);
279
+ onValueChange({
280
+ value: clampedValue,
281
+ formattedValue: formatValue(clampedValue),
282
+ });
283
+ }
284
+ }
285
+ }
286
+ // Normalize minus sign to half-width if needed
287
+ if (shouldPreserveMinus && isMinusSign(rawInputValue) && rawInputValue !== '-') {
288
+ setRawInputValue('-');
289
+ onValueChange({
290
+ value: 0,
291
+ formattedValue: '-',
292
+ });
293
+ }
294
+ hasProcessedComposition.current = false;
295
+ if (onBlur) {
296
+ onBlur(e);
297
+ }
298
+ }, [
299
+ composingValue,
300
+ onBlur,
301
+ handleValueChange,
302
+ rawInputValue,
303
+ minValue,
304
+ maxValue,
305
+ formatValue,
306
+ allowNegative,
307
+ processConvertedValue,
308
+ ]);
309
+ // Reset rawInputValue when value prop changes externally (e.g., form reset)
310
+ useEffect(() => {
311
+ const numValue = parseValueProp(value, separator);
312
+ if (Number.isNaN(numValue)) {
313
+ // Preserve minus sign if allowNegative is true and user is typing
314
+ if (allowNegative && isMinusSign(rawInputValue)) {
315
+ return;
316
+ }
317
+ setRawInputValue('');
318
+ return;
319
+ }
320
+ // If the value is 0, preserve rawInputValue if it's a special pattern
321
+ if (numValue === 0) {
322
+ const isSingleZero = rawInputValue === '0' ||
323
+ rawInputValue === '-0' ||
324
+ isMinusSign(rawInputValue) ||
325
+ rawInputValue.startsWith('0.') ||
326
+ rawInputValue.startsWith('-0.');
327
+ // Check if rawInputValue is a negative number (preserve it when allowNegative is true)
328
+ if (allowNegative && rawInputValue !== '') {
329
+ const convertedRawValue = convertFullWidthToHalfWidth(rawInputValue);
330
+ const rawAsNumber = Number(convertedRawValue);
331
+ if (!Number.isNaN(rawAsNumber) &&
332
+ Number.isFinite(rawAsNumber) &&
333
+ rawAsNumber < 0) {
334
+ return;
335
+ }
336
+ }
337
+ if (!isSingleZero) {
338
+ setRawInputValue('0');
339
+ }
340
+ return;
341
+ }
342
+ // For non-zero values, check if the numeric value matches what we'd get from rawInputValue
343
+ if (rawInputValue !== '') {
344
+ // Preserve minus sign only if allowNegative is true
345
+ if (allowNegative && isMinusSign(rawInputValue)) {
346
+ return;
347
+ }
348
+ const convertedRawValue = convertFullWidthToHalfWidth(rawInputValue);
349
+ const rawAsNumber = Number(convertedRawValue);
350
+ if (allowNegative && convertedRawValue.startsWith('-')) {
351
+ if (rawAsNumber === numValue) {
352
+ return;
353
+ }
354
+ else if (numValue > 0 && Math.abs(rawAsNumber) === numValue) {
355
+ setRawInputValue('');
356
+ }
357
+ else {
358
+ return;
359
+ }
360
+ }
361
+ else if (rawAsNumber !== numValue) {
362
+ setRawInputValue('');
363
+ }
364
+ }
365
+ }, [value, separator, rawInputValue, allowNegative]);
366
+ // Format the display value
367
+ const displayValue = useMemo(() => {
368
+ if (composingValue !== '') {
369
+ return composingValue;
370
+ }
371
+ if (rawInputValue === '') {
372
+ const numValue = parseValueProp(value, separator);
373
+ if (Number.isNaN(numValue)) {
374
+ return '';
375
+ }
376
+ if (numValue === 0) {
377
+ return '0';
378
+ }
379
+ if (Number.isFinite(numValue)) {
380
+ return formatValue(numValue);
381
+ }
382
+ return '';
383
+ }
384
+ if (rawInputValue !== '') {
385
+ const isSingleZero = rawInputValue === '0' ||
386
+ rawInputValue === '-0' ||
387
+ rawInputValue.startsWith('0.') ||
388
+ rawInputValue.startsWith('-0.');
389
+ const isMinusOnly = allowNegative && isMinusSign(rawInputValue);
390
+ const endsWithDecimalPoint = allowDecimal &&
391
+ rawInputValue.endsWith('.') &&
392
+ !rawInputValue.endsWith('..');
393
+ if (isSingleZero || isMinusOnly || endsWithDecimalPoint) {
394
+ return normalizeMinusSign(rawInputValue);
395
+ }
396
+ const rawAsNumber = Number(rawInputValue);
397
+ if (!Number.isNaN(rawAsNumber) && Number.isFinite(rawAsNumber)) {
398
+ return formatValue(rawAsNumber);
399
+ }
400
+ }
401
+ const numValue = parseValueProp(value, separator);
402
+ if (Number.isNaN(numValue) || !Number.isFinite(numValue)) {
403
+ return '';
404
+ }
405
+ return formatValue(numValue);
406
+ }, [value, formatValue, separator, composingValue, rawInputValue, allowNegative, allowDecimal]);
407
+ // Determine appropriate inputMode for mobile keyboards
408
+ const inputMode = allowDecimal ? 'decimal' : 'numeric';
409
+ return {
410
+ inputRef,
411
+ inputMode,
412
+ displayValue,
413
+ hasProcessedComposition,
414
+ handleBlur,
415
+ handleValueChange,
416
+ handleCompositionEnd,
417
+ handleCompositionStart,
418
+ };
419
+ };
420
+ //# sourceMappingURL=use-numeric-input.js.map