@carbon/react 1.99.0-rc.0 → 1.99.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/.playwright/INTERNAL_AVT_REPORT_DO_NOT_USE.json +964 -964
- package/es/components/NumberInput/NumberInput.d.ts +9 -0
- package/es/components/NumberInput/NumberInput.js +206 -38
- package/lib/components/NumberInput/NumberInput.d.ts +9 -0
- package/lib/components/NumberInput/NumberInput.js +206 -37
- package/package.json +7 -7
|
@@ -207,6 +207,15 @@ export interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInp
|
|
|
207
207
|
*/
|
|
208
208
|
warnText?: ReactNode;
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Converts a string with any Unicode numeral system to a JavaScript number.
|
|
212
|
+
* Handles all numeral systems supported by Intl.NumberFormat.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} input - The input string with numerals in any Unicode system
|
|
215
|
+
* @param {string} locale - The locale for parsing separators
|
|
216
|
+
* @returns {number} The parsed number, or NaN if invalid
|
|
217
|
+
*/
|
|
218
|
+
export declare const parseNumberWithLocale: (input: string, locale: string) => number;
|
|
210
219
|
export declare const validateNumberSeparators: (input: string, locale: string) => boolean;
|
|
211
220
|
declare const NumberInput: React.ForwardRefExoticComponent<NumberInputProps & React.RefAttributes<HTMLInputElement>>;
|
|
212
221
|
export { NumberInput };
|
|
@@ -43,8 +43,41 @@ const getSeparators = locale => {
|
|
|
43
43
|
const numberWithGroupAndDecimal = 1234567.89;
|
|
44
44
|
const formatted = new Intl.NumberFormat(locale).format(numberWithGroupAndDecimal);
|
|
45
45
|
|
|
46
|
-
//
|
|
47
|
-
|
|
46
|
+
// Comprehensive Unicode digit pattern that includes all common numeral systems
|
|
47
|
+
// supported by Intl.NumberFormat across different locales
|
|
48
|
+
const digitPattern = '[' + '\\u0030-\\u0039' +
|
|
49
|
+
// Western
|
|
50
|
+
'\\u0660-\\u0669' +
|
|
51
|
+
// Eastern Arabic
|
|
52
|
+
'\\u0966-\\u096F' +
|
|
53
|
+
// Devanagari
|
|
54
|
+
'\\u09E6-\\u09EF' +
|
|
55
|
+
// Bengali
|
|
56
|
+
'\\uFF10-\\uFF19' +
|
|
57
|
+
// Fullwidth Japanese 0-9
|
|
58
|
+
'一二三四五六七八九〇零' +
|
|
59
|
+
// Kanji digits
|
|
60
|
+
']';
|
|
61
|
+
|
|
62
|
+
// Non-digit pattern that excludes ALL digit types (not just ASCII 0-9)
|
|
63
|
+
const nonDigitPattern = '[^' + '\\u0030-\\u0039' +
|
|
64
|
+
// Western
|
|
65
|
+
'\\u0660-\\u0669' +
|
|
66
|
+
// Eastern Arabic
|
|
67
|
+
'\\u0966-\\u096F' +
|
|
68
|
+
// Devanagari
|
|
69
|
+
'\\u09E6-\\u09EF' +
|
|
70
|
+
// Bengali
|
|
71
|
+
'\\uFF10-\\uFF19' +
|
|
72
|
+
// Fullwidth Japanese 0-9
|
|
73
|
+
'一二三四五六七八九〇零' +
|
|
74
|
+
// Kanji digits
|
|
75
|
+
']+';
|
|
76
|
+
|
|
77
|
+
// Extract separators using regex that handles all numeral systems
|
|
78
|
+
// Use nonDigitPattern instead of \D+ to correctly identify separators
|
|
79
|
+
const regex = new RegExp(`(${nonDigitPattern})${digitPattern}{3}(${nonDigitPattern})${digitPattern}{2}$`);
|
|
80
|
+
const match = formatted.match(regex);
|
|
48
81
|
if (match) {
|
|
49
82
|
const groupSeparator = match[1];
|
|
50
83
|
const decimalSeparator = match[2];
|
|
@@ -59,11 +92,116 @@ const getSeparators = locale => {
|
|
|
59
92
|
};
|
|
60
93
|
}
|
|
61
94
|
};
|
|
95
|
+
|
|
96
|
+
// Normalizes all Unicode minus variants to ASCII hyphen-minus (-)
|
|
97
|
+
const normalizeMinus = value => value.replace(/[\u2212\u2012\u2013\u2014\uFE63\uFF0D]/g, '-');
|
|
98
|
+
const normalizeNumericInput = value => value
|
|
99
|
+
// Remove bidi / direction control characters (Arabic keyboards)
|
|
100
|
+
.replace(/[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, '')
|
|
101
|
+
// Normalize Unicode minus variants to ASCII "-"
|
|
102
|
+
.replace(/[\u2212\u2012\u2013\u2014\uFE63\uFF0D]/g, '-');
|
|
103
|
+
/**
|
|
104
|
+
* Converts a string with any Unicode numeral system to a JavaScript number.
|
|
105
|
+
* Handles all numeral systems supported by Intl.NumberFormat.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} input - The input string with numerals in any Unicode system
|
|
108
|
+
* @param {string} locale - The locale for parsing separators
|
|
109
|
+
* @returns {number} The parsed number, or NaN if invalid
|
|
110
|
+
*/
|
|
111
|
+
const parseNumberWithLocale = (input, locale) => {
|
|
112
|
+
// Handle empty, null, or undefined inputs
|
|
113
|
+
if (input === '' || input === undefined || input === null) {
|
|
114
|
+
return NaN;
|
|
115
|
+
}
|
|
116
|
+
input = normalizeNumericInput(input);
|
|
117
|
+
const {
|
|
118
|
+
groupSeparator,
|
|
119
|
+
decimalSeparator
|
|
120
|
+
} = getSeparators(locale);
|
|
121
|
+
|
|
122
|
+
// Kanji digit map
|
|
123
|
+
const kanjiMap = {
|
|
124
|
+
零: '0',
|
|
125
|
+
〇: '0',
|
|
126
|
+
一: '1',
|
|
127
|
+
二: '2',
|
|
128
|
+
三: '3',
|
|
129
|
+
四: '4',
|
|
130
|
+
五: '5',
|
|
131
|
+
六: '6',
|
|
132
|
+
七: '7',
|
|
133
|
+
八: '8',
|
|
134
|
+
九: '9'
|
|
135
|
+
};
|
|
136
|
+
const digitRanges = [{
|
|
137
|
+
start: 0x0030,
|
|
138
|
+
end: 0x0039,
|
|
139
|
+
base: 0x0030
|
|
140
|
+
}, {
|
|
141
|
+
start: 0x0660,
|
|
142
|
+
end: 0x0669,
|
|
143
|
+
base: 0x0660
|
|
144
|
+
}, {
|
|
145
|
+
start: 0x0966,
|
|
146
|
+
end: 0x096f,
|
|
147
|
+
base: 0x0966
|
|
148
|
+
}, {
|
|
149
|
+
start: 0x09e6,
|
|
150
|
+
end: 0x09ef,
|
|
151
|
+
base: 0x09e6
|
|
152
|
+
}, {
|
|
153
|
+
start: 0xff10,
|
|
154
|
+
end: 0xff19,
|
|
155
|
+
base: 0xff10
|
|
156
|
+
}];
|
|
157
|
+
let normalized = Array.from(input).map(char => {
|
|
158
|
+
// Preserve scientific notation characters
|
|
159
|
+
if (char === 'e' || char === 'E' || char === '+' || char === '-') {
|
|
160
|
+
return char;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check Kanji first
|
|
164
|
+
if (kanjiMap[char] !== undefined) {
|
|
165
|
+
return kanjiMap[char];
|
|
166
|
+
}
|
|
167
|
+
const code = char.charCodeAt(0);
|
|
168
|
+
for (const range of digitRanges) {
|
|
169
|
+
if (code >= range.start && code <= range.end) {
|
|
170
|
+
return String(code - range.start);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return char;
|
|
174
|
+
}).join('');
|
|
175
|
+
|
|
176
|
+
// Remove grouping separators
|
|
177
|
+
if (groupSeparator) {
|
|
178
|
+
if (groupSeparator?.trim() === '') {
|
|
179
|
+
normalized = normalized?.replace(/[\u00A0\u202F\s]/g, '');
|
|
180
|
+
} else {
|
|
181
|
+
if (decimalSeparator !== ',' && decimalSeparator !== '٬') {
|
|
182
|
+
normalized = normalized?.replace(/[,٬]/g, '');
|
|
183
|
+
}
|
|
184
|
+
if (groupSeparator !== ',' && groupSeparator !== '٬') {
|
|
185
|
+
const escaped = groupSeparator?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
186
|
+
normalized = normalized?.replace(new RegExp(escaped, 'g'), '');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
normalized = normalized.replace(/٫/g, '.');
|
|
191
|
+
if (decimalSeparator && decimalSeparator !== '.' && decimalSeparator !== '٫') {
|
|
192
|
+
const escaped = decimalSeparator.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
193
|
+
normalized = normalized.replace(new RegExp(escaped, 'g'), '.');
|
|
194
|
+
}
|
|
195
|
+
normalized = normalizeMinus(normalized);
|
|
196
|
+
return Number(normalized);
|
|
197
|
+
};
|
|
62
198
|
const validateNumberSeparators = (input, locale) => {
|
|
63
|
-
|
|
64
|
-
if (input === '' || Number.isNaN(input)) {
|
|
199
|
+
if (input === '') {
|
|
65
200
|
return true;
|
|
66
201
|
}
|
|
202
|
+
|
|
203
|
+
// Normalize bidi marks + minus signs FIRST
|
|
204
|
+
input = normalizeNumericInput(input);
|
|
67
205
|
const {
|
|
68
206
|
groupSeparator,
|
|
69
207
|
decimalSeparator
|
|
@@ -72,35 +210,54 @@ const validateNumberSeparators = (input, locale) => {
|
|
|
72
210
|
return !isNaN(Number(input));
|
|
73
211
|
}
|
|
74
212
|
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
213
|
+
const digit = '[' + '\\u0030-\\u0039' + '\\u0660-\\u0669' + '\\u0966-\\u096F' + '\\u09E6-\\u09EF' + '\\uFF10-\\uFF19' + '一二三四五六七八九〇零' + ']';
|
|
214
|
+
|
|
215
|
+
// Group separator regex
|
|
75
216
|
let group = '';
|
|
76
217
|
if (groupSeparator) {
|
|
77
|
-
if (groupSeparator
|
|
78
|
-
group = '[\\u00A0\\u202F\\s]';
|
|
218
|
+
if (groupSeparator?.trim() === '') {
|
|
219
|
+
group = '[\\u00A0\\u202F\\s]';
|
|
220
|
+
} else if (groupSeparator === ',' || groupSeparator === '٬') {
|
|
221
|
+
group = '[,٬]';
|
|
79
222
|
} else {
|
|
80
223
|
group = esc(groupSeparator);
|
|
81
224
|
}
|
|
82
225
|
}
|
|
83
|
-
const decimal = esc(decimalSeparator);
|
|
84
226
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (!regex.test(input)) {
|
|
90
|
-
return false;
|
|
227
|
+
// Decimal separator regex
|
|
228
|
+
let decimal = esc(decimalSeparator);
|
|
229
|
+
if (decimalSeparator === '.' || decimalSeparator === '٫') {
|
|
230
|
+
decimal = '[.٫]';
|
|
91
231
|
}
|
|
232
|
+
const sign = '[\\-\\u2212]?';
|
|
233
|
+
const scientific = `([eE][+-]?${digit}+)?`;
|
|
92
234
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
235
|
+
// Detect if grouping is used AT ALL
|
|
236
|
+
const usesGrouping = group && (groupSeparator?.trim() === '' ? /[\u00A0\u202F\s]/.test(input) : groupSeparator === ',' || groupSeparator === '٬' ? /[,٬]/.test(input) : groupSeparator ? input.includes(groupSeparator) : false);
|
|
237
|
+
const scientificMatch = input?.match(/^([^eE]+)([eE][+-]?.*)?$/);
|
|
238
|
+
const baseNumber = scientificMatch ? scientificMatch[1] : input;
|
|
239
|
+
|
|
240
|
+
// Split integer part from the base number - handle both decimal separator variants
|
|
241
|
+
let integerPart;
|
|
242
|
+
if (decimalSeparator === '.' || decimalSeparator === '٫') {
|
|
243
|
+
// Split by either . or ٫
|
|
244
|
+
integerPart = baseNumber?.split(/[.,]/)[0];
|
|
245
|
+
} else {
|
|
246
|
+
integerPart = baseNumber?.split(decimalSeparator)[0];
|
|
101
247
|
}
|
|
102
|
-
|
|
103
|
-
|
|
248
|
+
|
|
249
|
+
// STEP 1: strict integer validation
|
|
250
|
+
// When grouping is used, we need to handle two cases:
|
|
251
|
+
// 1. Numbers with 1-3 digits (no separator required): 1, 12, 123
|
|
252
|
+
// 2. Numbers with 4+ digits (separator required): 1,234 or 12,345 or 123,456
|
|
253
|
+
const integerRegex = usesGrouping ? new RegExp(`^${sign}(${digit}{1,3}|${digit}{1,3}(${group}${digit}{3})+)$`) : new RegExp(`^${sign}${digit}+$`);
|
|
254
|
+
if (!integerRegex.test(integerPart)) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// STEP 2: full number validation
|
|
259
|
+
const fullRegex = new RegExp(`^${sign}${digit}+` + (usesGrouping ? `(${group}${digit}{3})*` : '') + `(${decimal}${digit}+)?${scientific}$`);
|
|
260
|
+
return fullRegex.test(input);
|
|
104
261
|
};
|
|
105
262
|
|
|
106
263
|
// eslint-disable-next-line react/display-name -- https://github.com/carbon-design-system/carbon/issues/20452
|
|
@@ -292,8 +449,9 @@ const NumberInput = /*#__PURE__*/React.forwardRef((props, forwardRef) => {
|
|
|
292
449
|
}
|
|
293
450
|
};
|
|
294
451
|
const outerElementClasses = cx(`${prefix}--form-item`, {
|
|
295
|
-
|
|
296
|
-
|
|
452
|
+
...(customClassName ? {
|
|
453
|
+
[customClassName]: true
|
|
454
|
+
} : {}),
|
|
297
455
|
[`${prefix}--number-input--fluid--invalid`]: isFluid && normalizedProps.invalid,
|
|
298
456
|
[`${prefix}--number-input--fluid--focus`]: isFluid && isFocused,
|
|
299
457
|
[`${prefix}--number-input--fluid--disabled`]: isFluid && disabled
|
|
@@ -764,23 +922,33 @@ function getInputValidity({
|
|
|
764
922
|
validate,
|
|
765
923
|
locale
|
|
766
924
|
}) {
|
|
767
|
-
if (typeof validate === 'function') {
|
|
768
|
-
const result = validate(value, locale);
|
|
769
|
-
if (result === false) {
|
|
770
|
-
return false; // immediate invalid
|
|
771
|
-
}
|
|
772
|
-
// If true or undefined, continue to further validations
|
|
773
|
-
}
|
|
774
925
|
if (invalid) {
|
|
775
926
|
return false;
|
|
776
927
|
}
|
|
777
|
-
|
|
778
|
-
|
|
928
|
+
|
|
929
|
+
// Skip validation if value is empty and allowEmpty
|
|
930
|
+
if (value === '') return allowEmpty;
|
|
931
|
+
|
|
932
|
+
// Normalize the value
|
|
933
|
+
let numericValue;
|
|
934
|
+
if (typeof value === 'string') {
|
|
935
|
+
numericValue = parseNumberWithLocale(value, locale); // safe: handles Arabic, Kanji, etc.
|
|
936
|
+
} else {
|
|
937
|
+
numericValue = value;
|
|
779
938
|
}
|
|
780
|
-
|
|
781
|
-
|
|
939
|
+
|
|
940
|
+
// Use custom validate ONLY for formatting, not numeric comparison
|
|
941
|
+
if (validate && typeof value === 'string') {
|
|
942
|
+
const isFormatValid = validate(value, locale);
|
|
943
|
+
if (isFormatValid === false) {
|
|
944
|
+
return false; // invalid format
|
|
945
|
+
}
|
|
782
946
|
}
|
|
783
|
-
|
|
947
|
+
|
|
948
|
+
// Check min/max bounds
|
|
949
|
+
if (max !== undefined && numericValue > max) return false;
|
|
950
|
+
if (min !== undefined && numericValue < min) return false;
|
|
951
|
+
return true; // valid
|
|
784
952
|
}
|
|
785
953
|
|
|
786
954
|
/**
|
|
@@ -801,4 +969,4 @@ function disableWheel(e) {
|
|
|
801
969
|
e.preventDefault();
|
|
802
970
|
}
|
|
803
971
|
|
|
804
|
-
export { NumberInput, validateNumberSeparators };
|
|
972
|
+
export { NumberInput, parseNumberWithLocale, validateNumberSeparators };
|
|
@@ -207,6 +207,15 @@ export interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInp
|
|
|
207
207
|
*/
|
|
208
208
|
warnText?: ReactNode;
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Converts a string with any Unicode numeral system to a JavaScript number.
|
|
212
|
+
* Handles all numeral systems supported by Intl.NumberFormat.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} input - The input string with numerals in any Unicode system
|
|
215
|
+
* @param {string} locale - The locale for parsing separators
|
|
216
|
+
* @returns {number} The parsed number, or NaN if invalid
|
|
217
|
+
*/
|
|
218
|
+
export declare const parseNumberWithLocale: (input: string, locale: string) => number;
|
|
210
219
|
export declare const validateNumberSeparators: (input: string, locale: string) => boolean;
|
|
211
220
|
declare const NumberInput: React.ForwardRefExoticComponent<NumberInputProps & React.RefAttributes<HTMLInputElement>>;
|
|
212
221
|
export { NumberInput };
|
|
@@ -45,8 +45,41 @@ const getSeparators = locale => {
|
|
|
45
45
|
const numberWithGroupAndDecimal = 1234567.89;
|
|
46
46
|
const formatted = new Intl.NumberFormat(locale).format(numberWithGroupAndDecimal);
|
|
47
47
|
|
|
48
|
-
//
|
|
49
|
-
|
|
48
|
+
// Comprehensive Unicode digit pattern that includes all common numeral systems
|
|
49
|
+
// supported by Intl.NumberFormat across different locales
|
|
50
|
+
const digitPattern = '[' + '\\u0030-\\u0039' +
|
|
51
|
+
// Western
|
|
52
|
+
'\\u0660-\\u0669' +
|
|
53
|
+
// Eastern Arabic
|
|
54
|
+
'\\u0966-\\u096F' +
|
|
55
|
+
// Devanagari
|
|
56
|
+
'\\u09E6-\\u09EF' +
|
|
57
|
+
// Bengali
|
|
58
|
+
'\\uFF10-\\uFF19' +
|
|
59
|
+
// Fullwidth Japanese 0-9
|
|
60
|
+
'一二三四五六七八九〇零' +
|
|
61
|
+
// Kanji digits
|
|
62
|
+
']';
|
|
63
|
+
|
|
64
|
+
// Non-digit pattern that excludes ALL digit types (not just ASCII 0-9)
|
|
65
|
+
const nonDigitPattern = '[^' + '\\u0030-\\u0039' +
|
|
66
|
+
// Western
|
|
67
|
+
'\\u0660-\\u0669' +
|
|
68
|
+
// Eastern Arabic
|
|
69
|
+
'\\u0966-\\u096F' +
|
|
70
|
+
// Devanagari
|
|
71
|
+
'\\u09E6-\\u09EF' +
|
|
72
|
+
// Bengali
|
|
73
|
+
'\\uFF10-\\uFF19' +
|
|
74
|
+
// Fullwidth Japanese 0-9
|
|
75
|
+
'一二三四五六七八九〇零' +
|
|
76
|
+
// Kanji digits
|
|
77
|
+
']+';
|
|
78
|
+
|
|
79
|
+
// Extract separators using regex that handles all numeral systems
|
|
80
|
+
// Use nonDigitPattern instead of \D+ to correctly identify separators
|
|
81
|
+
const regex = new RegExp(`(${nonDigitPattern})${digitPattern}{3}(${nonDigitPattern})${digitPattern}{2}$`);
|
|
82
|
+
const match = formatted.match(regex);
|
|
50
83
|
if (match) {
|
|
51
84
|
const groupSeparator = match[1];
|
|
52
85
|
const decimalSeparator = match[2];
|
|
@@ -61,11 +94,116 @@ const getSeparators = locale => {
|
|
|
61
94
|
};
|
|
62
95
|
}
|
|
63
96
|
};
|
|
97
|
+
|
|
98
|
+
// Normalizes all Unicode minus variants to ASCII hyphen-minus (-)
|
|
99
|
+
const normalizeMinus = value => value.replace(/[\u2212\u2012\u2013\u2014\uFE63\uFF0D]/g, '-');
|
|
100
|
+
const normalizeNumericInput = value => value
|
|
101
|
+
// Remove bidi / direction control characters (Arabic keyboards)
|
|
102
|
+
.replace(/[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, '')
|
|
103
|
+
// Normalize Unicode minus variants to ASCII "-"
|
|
104
|
+
.replace(/[\u2212\u2012\u2013\u2014\uFE63\uFF0D]/g, '-');
|
|
105
|
+
/**
|
|
106
|
+
* Converts a string with any Unicode numeral system to a JavaScript number.
|
|
107
|
+
* Handles all numeral systems supported by Intl.NumberFormat.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} input - The input string with numerals in any Unicode system
|
|
110
|
+
* @param {string} locale - The locale for parsing separators
|
|
111
|
+
* @returns {number} The parsed number, or NaN if invalid
|
|
112
|
+
*/
|
|
113
|
+
const parseNumberWithLocale = (input, locale) => {
|
|
114
|
+
// Handle empty, null, or undefined inputs
|
|
115
|
+
if (input === '' || input === undefined || input === null) {
|
|
116
|
+
return NaN;
|
|
117
|
+
}
|
|
118
|
+
input = normalizeNumericInput(input);
|
|
119
|
+
const {
|
|
120
|
+
groupSeparator,
|
|
121
|
+
decimalSeparator
|
|
122
|
+
} = getSeparators(locale);
|
|
123
|
+
|
|
124
|
+
// Kanji digit map
|
|
125
|
+
const kanjiMap = {
|
|
126
|
+
零: '0',
|
|
127
|
+
〇: '0',
|
|
128
|
+
一: '1',
|
|
129
|
+
二: '2',
|
|
130
|
+
三: '3',
|
|
131
|
+
四: '4',
|
|
132
|
+
五: '5',
|
|
133
|
+
六: '6',
|
|
134
|
+
七: '7',
|
|
135
|
+
八: '8',
|
|
136
|
+
九: '9'
|
|
137
|
+
};
|
|
138
|
+
const digitRanges = [{
|
|
139
|
+
start: 0x0030,
|
|
140
|
+
end: 0x0039,
|
|
141
|
+
base: 0x0030
|
|
142
|
+
}, {
|
|
143
|
+
start: 0x0660,
|
|
144
|
+
end: 0x0669,
|
|
145
|
+
base: 0x0660
|
|
146
|
+
}, {
|
|
147
|
+
start: 0x0966,
|
|
148
|
+
end: 0x096f,
|
|
149
|
+
base: 0x0966
|
|
150
|
+
}, {
|
|
151
|
+
start: 0x09e6,
|
|
152
|
+
end: 0x09ef,
|
|
153
|
+
base: 0x09e6
|
|
154
|
+
}, {
|
|
155
|
+
start: 0xff10,
|
|
156
|
+
end: 0xff19,
|
|
157
|
+
base: 0xff10
|
|
158
|
+
}];
|
|
159
|
+
let normalized = Array.from(input).map(char => {
|
|
160
|
+
// Preserve scientific notation characters
|
|
161
|
+
if (char === 'e' || char === 'E' || char === '+' || char === '-') {
|
|
162
|
+
return char;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check Kanji first
|
|
166
|
+
if (kanjiMap[char] !== undefined) {
|
|
167
|
+
return kanjiMap[char];
|
|
168
|
+
}
|
|
169
|
+
const code = char.charCodeAt(0);
|
|
170
|
+
for (const range of digitRanges) {
|
|
171
|
+
if (code >= range.start && code <= range.end) {
|
|
172
|
+
return String(code - range.start);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return char;
|
|
176
|
+
}).join('');
|
|
177
|
+
|
|
178
|
+
// Remove grouping separators
|
|
179
|
+
if (groupSeparator) {
|
|
180
|
+
if (groupSeparator?.trim() === '') {
|
|
181
|
+
normalized = normalized?.replace(/[\u00A0\u202F\s]/g, '');
|
|
182
|
+
} else {
|
|
183
|
+
if (decimalSeparator !== ',' && decimalSeparator !== '٬') {
|
|
184
|
+
normalized = normalized?.replace(/[,٬]/g, '');
|
|
185
|
+
}
|
|
186
|
+
if (groupSeparator !== ',' && groupSeparator !== '٬') {
|
|
187
|
+
const escaped = groupSeparator?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
188
|
+
normalized = normalized?.replace(new RegExp(escaped, 'g'), '');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
normalized = normalized.replace(/٫/g, '.');
|
|
193
|
+
if (decimalSeparator && decimalSeparator !== '.' && decimalSeparator !== '٫') {
|
|
194
|
+
const escaped = decimalSeparator.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
195
|
+
normalized = normalized.replace(new RegExp(escaped, 'g'), '.');
|
|
196
|
+
}
|
|
197
|
+
normalized = normalizeMinus(normalized);
|
|
198
|
+
return Number(normalized);
|
|
199
|
+
};
|
|
64
200
|
const validateNumberSeparators = (input, locale) => {
|
|
65
|
-
|
|
66
|
-
if (input === '' || Number.isNaN(input)) {
|
|
201
|
+
if (input === '') {
|
|
67
202
|
return true;
|
|
68
203
|
}
|
|
204
|
+
|
|
205
|
+
// Normalize bidi marks + minus signs FIRST
|
|
206
|
+
input = normalizeNumericInput(input);
|
|
69
207
|
const {
|
|
70
208
|
groupSeparator,
|
|
71
209
|
decimalSeparator
|
|
@@ -74,35 +212,54 @@ const validateNumberSeparators = (input, locale) => {
|
|
|
74
212
|
return !isNaN(Number(input));
|
|
75
213
|
}
|
|
76
214
|
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
215
|
+
const digit = '[' + '\\u0030-\\u0039' + '\\u0660-\\u0669' + '\\u0966-\\u096F' + '\\u09E6-\\u09EF' + '\\uFF10-\\uFF19' + '一二三四五六七八九〇零' + ']';
|
|
216
|
+
|
|
217
|
+
// Group separator regex
|
|
77
218
|
let group = '';
|
|
78
219
|
if (groupSeparator) {
|
|
79
|
-
if (groupSeparator
|
|
80
|
-
group = '[\\u00A0\\u202F\\s]';
|
|
220
|
+
if (groupSeparator?.trim() === '') {
|
|
221
|
+
group = '[\\u00A0\\u202F\\s]';
|
|
222
|
+
} else if (groupSeparator === ',' || groupSeparator === '٬') {
|
|
223
|
+
group = '[,٬]';
|
|
81
224
|
} else {
|
|
82
225
|
group = esc(groupSeparator);
|
|
83
226
|
}
|
|
84
227
|
}
|
|
85
|
-
const decimal = esc(decimalSeparator);
|
|
86
228
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (!regex.test(input)) {
|
|
92
|
-
return false;
|
|
229
|
+
// Decimal separator regex
|
|
230
|
+
let decimal = esc(decimalSeparator);
|
|
231
|
+
if (decimalSeparator === '.' || decimalSeparator === '٫') {
|
|
232
|
+
decimal = '[.٫]';
|
|
93
233
|
}
|
|
234
|
+
const sign = '[\\-\\u2212]?';
|
|
235
|
+
const scientific = `([eE][+-]?${digit}+)?`;
|
|
94
236
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
237
|
+
// Detect if grouping is used AT ALL
|
|
238
|
+
const usesGrouping = group && (groupSeparator?.trim() === '' ? /[\u00A0\u202F\s]/.test(input) : groupSeparator === ',' || groupSeparator === '٬' ? /[,٬]/.test(input) : groupSeparator ? input.includes(groupSeparator) : false);
|
|
239
|
+
const scientificMatch = input?.match(/^([^eE]+)([eE][+-]?.*)?$/);
|
|
240
|
+
const baseNumber = scientificMatch ? scientificMatch[1] : input;
|
|
241
|
+
|
|
242
|
+
// Split integer part from the base number - handle both decimal separator variants
|
|
243
|
+
let integerPart;
|
|
244
|
+
if (decimalSeparator === '.' || decimalSeparator === '٫') {
|
|
245
|
+
// Split by either . or ٫
|
|
246
|
+
integerPart = baseNumber?.split(/[.,]/)[0];
|
|
247
|
+
} else {
|
|
248
|
+
integerPart = baseNumber?.split(decimalSeparator)[0];
|
|
103
249
|
}
|
|
104
|
-
|
|
105
|
-
|
|
250
|
+
|
|
251
|
+
// STEP 1: strict integer validation
|
|
252
|
+
// When grouping is used, we need to handle two cases:
|
|
253
|
+
// 1. Numbers with 1-3 digits (no separator required): 1, 12, 123
|
|
254
|
+
// 2. Numbers with 4+ digits (separator required): 1,234 or 12,345 or 123,456
|
|
255
|
+
const integerRegex = usesGrouping ? new RegExp(`^${sign}(${digit}{1,3}|${digit}{1,3}(${group}${digit}{3})+)$`) : new RegExp(`^${sign}${digit}+$`);
|
|
256
|
+
if (!integerRegex.test(integerPart)) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// STEP 2: full number validation
|
|
261
|
+
const fullRegex = new RegExp(`^${sign}${digit}+` + (usesGrouping ? `(${group}${digit}{3})*` : '') + `(${decimal}${digit}+)?${scientific}$`);
|
|
262
|
+
return fullRegex.test(input);
|
|
106
263
|
};
|
|
107
264
|
|
|
108
265
|
// eslint-disable-next-line react/display-name -- https://github.com/carbon-design-system/carbon/issues/20452
|
|
@@ -294,8 +451,9 @@ const NumberInput = /*#__PURE__*/React.forwardRef((props, forwardRef) => {
|
|
|
294
451
|
}
|
|
295
452
|
};
|
|
296
453
|
const outerElementClasses = cx(`${prefix}--form-item`, {
|
|
297
|
-
|
|
298
|
-
|
|
454
|
+
...(customClassName ? {
|
|
455
|
+
[customClassName]: true
|
|
456
|
+
} : {}),
|
|
299
457
|
[`${prefix}--number-input--fluid--invalid`]: isFluid && normalizedProps.invalid,
|
|
300
458
|
[`${prefix}--number-input--fluid--focus`]: isFluid && isFocused,
|
|
301
459
|
[`${prefix}--number-input--fluid--disabled`]: isFluid && disabled
|
|
@@ -766,23 +924,33 @@ function getInputValidity({
|
|
|
766
924
|
validate,
|
|
767
925
|
locale
|
|
768
926
|
}) {
|
|
769
|
-
if (typeof validate === 'function') {
|
|
770
|
-
const result = validate(value, locale);
|
|
771
|
-
if (result === false) {
|
|
772
|
-
return false; // immediate invalid
|
|
773
|
-
}
|
|
774
|
-
// If true or undefined, continue to further validations
|
|
775
|
-
}
|
|
776
927
|
if (invalid) {
|
|
777
928
|
return false;
|
|
778
929
|
}
|
|
779
|
-
|
|
780
|
-
|
|
930
|
+
|
|
931
|
+
// Skip validation if value is empty and allowEmpty
|
|
932
|
+
if (value === '') return allowEmpty;
|
|
933
|
+
|
|
934
|
+
// Normalize the value
|
|
935
|
+
let numericValue;
|
|
936
|
+
if (typeof value === 'string') {
|
|
937
|
+
numericValue = parseNumberWithLocale(value, locale); // safe: handles Arabic, Kanji, etc.
|
|
938
|
+
} else {
|
|
939
|
+
numericValue = value;
|
|
781
940
|
}
|
|
782
|
-
|
|
783
|
-
|
|
941
|
+
|
|
942
|
+
// Use custom validate ONLY for formatting, not numeric comparison
|
|
943
|
+
if (validate && typeof value === 'string') {
|
|
944
|
+
const isFormatValid = validate(value, locale);
|
|
945
|
+
if (isFormatValid === false) {
|
|
946
|
+
return false; // invalid format
|
|
947
|
+
}
|
|
784
948
|
}
|
|
785
|
-
|
|
949
|
+
|
|
950
|
+
// Check min/max bounds
|
|
951
|
+
if (max !== undefined && numericValue > max) return false;
|
|
952
|
+
if (min !== undefined && numericValue < min) return false;
|
|
953
|
+
return true; // valid
|
|
786
954
|
}
|
|
787
955
|
|
|
788
956
|
/**
|
|
@@ -804,4 +972,5 @@ function disableWheel(e) {
|
|
|
804
972
|
}
|
|
805
973
|
|
|
806
974
|
exports.NumberInput = NumberInput;
|
|
975
|
+
exports.parseNumberWithLocale = parseNumberWithLocale;
|
|
807
976
|
exports.validateNumberSeparators = validateNumberSeparators;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carbon/react",
|
|
3
3
|
"description": "React components for the Carbon Design System",
|
|
4
|
-
"version": "1.99.0
|
|
4
|
+
"version": "1.99.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@babel/runtime": "^7.27.3",
|
|
55
55
|
"@carbon/feature-flags": ">=0.32.0",
|
|
56
|
-
"@carbon/icons-react": "^11.73.0
|
|
57
|
-
"@carbon/layout": "^11.46.0
|
|
58
|
-
"@carbon/styles": "^1.98.0
|
|
56
|
+
"@carbon/icons-react": "^11.73.0",
|
|
57
|
+
"@carbon/layout": "^11.46.0",
|
|
58
|
+
"@carbon/styles": "^1.98.0",
|
|
59
59
|
"@carbon/utilities": "^0.14.0",
|
|
60
60
|
"@floating-ui/react": "^0.27.4",
|
|
61
61
|
"@ibm/telemetry-js": "^1.5.0",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
"@babel/preset-env": "^7.27.2",
|
|
79
79
|
"@babel/preset-react": "^7.27.1",
|
|
80
80
|
"@babel/preset-typescript": "^7.27.1",
|
|
81
|
-
"@carbon/test-utils": "^10.39.0
|
|
82
|
-
"@carbon/themes": "^11.66.0
|
|
81
|
+
"@carbon/test-utils": "^10.39.0",
|
|
82
|
+
"@carbon/themes": "^11.66.0",
|
|
83
83
|
"@figma/code-connect": "^1.3.5",
|
|
84
84
|
"@rollup/plugin-babel": "^6.0.0",
|
|
85
85
|
"@rollup/plugin-commonjs": "^28.0.3",
|
|
@@ -131,5 +131,5 @@
|
|
|
131
131
|
"**/*.scss",
|
|
132
132
|
"**/*.css"
|
|
133
133
|
],
|
|
134
|
-
"gitHead": "
|
|
134
|
+
"gitHead": "ce7846aab8a3a1afe9b03d2d07d267af6cdb6ac2"
|
|
135
135
|
}
|