@carbon/react 1.98.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 +956 -956
- package/es/components/Dropdown/Dropdown.js +4 -0
- package/es/components/Link/Link.js +1 -1
- package/es/components/MultiSelect/FilterableMultiSelect.js +1 -0
- package/es/components/MultiSelect/MultiSelect.js +1 -0
- package/es/components/NumberInput/NumberInput.d.ts +15 -2
- package/es/components/NumberInput/NumberInput.js +223 -43
- package/es/components/Popover/index.d.ts +1 -1
- package/es/components/Popover/index.js +27 -13
- package/es/components/Tabs/Tabs.js +1 -1
- package/es/components/TextArea/TextArea.js +14 -4
- package/es/components/TextInput/PasswordInput.js +3 -2
- package/lib/components/Dropdown/Dropdown.js +4 -0
- package/lib/components/Link/Link.js +1 -1
- package/lib/components/MultiSelect/FilterableMultiSelect.js +1 -0
- package/lib/components/MultiSelect/MultiSelect.js +1 -0
- package/lib/components/NumberInput/NumberInput.d.ts +15 -2
- package/lib/components/NumberInput/NumberInput.js +223 -42
- package/lib/components/Popover/index.d.ts +1 -1
- package/lib/components/Popover/index.js +27 -13
- package/lib/components/Tabs/Tabs.js +1 -1
- package/lib/components/TextArea/TextArea.js +14 -4
- package/lib/components/TextInput/PasswordInput.js +3 -2
- package/package.json +9 -10
- package/telemetry.yml +1 -0
|
@@ -342,7 +342,11 @@ const Dropdown = /*#__PURE__*/React.forwardRef(({
|
|
|
342
342
|
size: size$1,
|
|
343
343
|
className: className,
|
|
344
344
|
invalid: normalizedProps.invalid,
|
|
345
|
+
invalidText: isFluid ? invalidText : undefined,
|
|
346
|
+
invalidTextId: normalizedProps.invalidId,
|
|
345
347
|
warn: normalizedProps.warn,
|
|
348
|
+
warnText: isFluid ? warnText : undefined,
|
|
349
|
+
warnTextId: normalizedProps.warnId,
|
|
346
350
|
light: light,
|
|
347
351
|
isOpen: isOpen,
|
|
348
352
|
ref: enableFloatingStyles || autoAlign ? refs.setReference : null,
|
|
@@ -67,7 +67,7 @@ const LinkBase = /*#__PURE__*/React.forwardRef(({
|
|
|
67
67
|
ref: ref
|
|
68
68
|
}, linkProps, rest, {
|
|
69
69
|
onClick: handleOnClick
|
|
70
|
-
}), children, !inline && Icon && /*#__PURE__*/React.createElement("
|
|
70
|
+
}), children, !inline && Icon && /*#__PURE__*/React.createElement("span", {
|
|
71
71
|
className: `${prefix}--link__icon`
|
|
72
72
|
}, /*#__PURE__*/React.createElement(Icon, null)));
|
|
73
73
|
});
|
|
@@ -724,6 +724,7 @@ const FilterableMultiSelect = /*#__PURE__*/forwardRef(function FilterableMultiSe
|
|
|
724
724
|
return /*#__PURE__*/React.createElement(ListBox.MenuItem, _extends({
|
|
725
725
|
key: itemProps.id,
|
|
726
726
|
"aria-label": itemText,
|
|
727
|
+
"aria-checked": isIndeterminate ? 'mixed' : isChecked,
|
|
727
728
|
isActive: isChecked && !item['isSelectAll'],
|
|
728
729
|
isHighlighted: highlightedIndex === index,
|
|
729
730
|
title: itemText,
|
|
@@ -509,6 +509,7 @@ const MultiSelect = /*#__PURE__*/React.forwardRef(({
|
|
|
509
509
|
key: itemProps.id,
|
|
510
510
|
isActive: isChecked && !item['isSelectAll'],
|
|
511
511
|
"aria-label": itemText,
|
|
512
|
+
"aria-checked": isIndeterminate ? 'mixed' : isChecked,
|
|
512
513
|
isHighlighted: highlightedIndex === index,
|
|
513
514
|
title: itemText,
|
|
514
515
|
disabled: itemProps['aria-disabled']
|
|
@@ -135,10 +135,14 @@ export interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInp
|
|
|
135
135
|
*/
|
|
136
136
|
min?: number;
|
|
137
137
|
/**
|
|
138
|
-
* Provide an optional handler that is called when the input
|
|
138
|
+
* Provide an optional handler that is called when the input is blurred.
|
|
139
|
+
*/
|
|
140
|
+
onBlur?: (event: React.FocusEvent<HTMLInputElement>, value?: string | number) => void;
|
|
141
|
+
/**
|
|
142
|
+
* Provide an optional handler that is called when the stepper
|
|
139
143
|
* buttons are blurred.
|
|
140
144
|
*/
|
|
141
|
-
|
|
145
|
+
onStepperBlur?: (event: React.FocusEvent<HTMLButtonElement>) => void;
|
|
142
146
|
/**
|
|
143
147
|
* Provide an optional handler that is called when the internal state of
|
|
144
148
|
* NumberInput changes. This handler is called with event and state info.
|
|
@@ -203,6 +207,15 @@ export interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInp
|
|
|
203
207
|
*/
|
|
204
208
|
warnText?: ReactNode;
|
|
205
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;
|
|
206
219
|
export declare const validateNumberSeparators: (input: string, locale: string) => boolean;
|
|
207
220
|
declare const NumberInput: React.ForwardRefExoticComponent<NumberInputProps & React.RefAttributes<HTMLInputElement>>;
|
|
208
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
|
|
@@ -126,6 +283,7 @@ const NumberInput = /*#__PURE__*/React.forwardRef((props, forwardRef) => {
|
|
|
126
283
|
max,
|
|
127
284
|
min,
|
|
128
285
|
onBlur,
|
|
286
|
+
onStepperBlur,
|
|
129
287
|
onChange,
|
|
130
288
|
onClick,
|
|
131
289
|
onKeyUp,
|
|
@@ -291,8 +449,9 @@ const NumberInput = /*#__PURE__*/React.forwardRef((props, forwardRef) => {
|
|
|
291
449
|
}
|
|
292
450
|
};
|
|
293
451
|
const outerElementClasses = cx(`${prefix}--form-item`, {
|
|
294
|
-
|
|
295
|
-
|
|
452
|
+
...(customClassName ? {
|
|
453
|
+
[customClassName]: true
|
|
454
|
+
} : {}),
|
|
296
455
|
[`${prefix}--number-input--fluid--invalid`]: isFluid && normalizedProps.invalid,
|
|
297
456
|
[`${prefix}--number-input--fluid--focus`]: isFluid && isFocused,
|
|
298
457
|
[`${prefix}--number-input--fluid--disabled`]: isFluid && disabled
|
|
@@ -426,6 +585,7 @@ const NumberInput = /*#__PURE__*/React.forwardRef((props, forwardRef) => {
|
|
|
426
585
|
if (disableWheelProp) {
|
|
427
586
|
e.target.removeEventListener('wheel', disableWheel);
|
|
428
587
|
}
|
|
588
|
+
let parsedValueForBlur;
|
|
429
589
|
if (type === 'text') {
|
|
430
590
|
// When isControlled, the current inputValue needs re-parsed
|
|
431
591
|
// because the consumer's onChange hasn't been called yet and
|
|
@@ -441,6 +601,7 @@ const NumberInput = /*#__PURE__*/React.forwardRef((props, forwardRef) => {
|
|
|
441
601
|
// causing the _numberValue to mismatch the formatted value in
|
|
442
602
|
// the input. To avoid this, formattedValue is re-parsed.
|
|
443
603
|
const parsedFormattedNewValue = numberParser.parse(formattedValue);
|
|
604
|
+
parsedValueForBlur = parsedFormattedNewValue;
|
|
444
605
|
if (onChange && isValid) {
|
|
445
606
|
const state = {
|
|
446
607
|
value: parsedFormattedNewValue,
|
|
@@ -466,7 +627,12 @@ const NumberInput = /*#__PURE__*/React.forwardRef((props, forwardRef) => {
|
|
|
466
627
|
}
|
|
467
628
|
}
|
|
468
629
|
if (onBlur) {
|
|
469
|
-
|
|
630
|
+
if (type === 'number') {
|
|
631
|
+
onBlur(e, value);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const parsedTextValue = parsedValueForBlur ?? (isControlled ? numberParser.parse(inputValue) : numberValue);
|
|
635
|
+
onBlur(e, parsedTextValue);
|
|
470
636
|
}
|
|
471
637
|
},
|
|
472
638
|
pattern: pattern,
|
|
@@ -486,7 +652,7 @@ const NumberInput = /*#__PURE__*/React.forwardRef((props, forwardRef) => {
|
|
|
486
652
|
className: `${prefix}--number__control-btn down-icon`,
|
|
487
653
|
disabled: disabled || readOnly,
|
|
488
654
|
onClick: event => handleStepperClick(event, 'down'),
|
|
489
|
-
onBlur:
|
|
655
|
+
onBlur: onStepperBlur,
|
|
490
656
|
tabIndex: -1,
|
|
491
657
|
title: decrementNumLabel || iconDescription,
|
|
492
658
|
type: "button"
|
|
@@ -499,7 +665,7 @@ const NumberInput = /*#__PURE__*/React.forwardRef((props, forwardRef) => {
|
|
|
499
665
|
className: `${prefix}--number__control-btn up-icon`,
|
|
500
666
|
disabled: disabled || readOnly,
|
|
501
667
|
onClick: event => handleStepperClick(event, 'up'),
|
|
502
|
-
onBlur:
|
|
668
|
+
onBlur: onStepperBlur,
|
|
503
669
|
tabIndex: -1,
|
|
504
670
|
title: incrementNumLabel || iconDescription,
|
|
505
671
|
type: "button"
|
|
@@ -610,10 +776,14 @@ NumberInput.propTypes = {
|
|
|
610
776
|
*/
|
|
611
777
|
stepStartValue: PropTypes.number,
|
|
612
778
|
/**
|
|
613
|
-
* Provide an optional handler that is called when the input
|
|
614
|
-
* buttons are blurred.
|
|
779
|
+
* Provide an optional handler that is called when the input is blurred.
|
|
615
780
|
*/
|
|
616
781
|
onBlur: PropTypes.func,
|
|
782
|
+
/**
|
|
783
|
+
* Provide an optional handler that is called when the stepper
|
|
784
|
+
* buttons are blurred.
|
|
785
|
+
*/
|
|
786
|
+
onStepperBlur: PropTypes.func,
|
|
617
787
|
/**
|
|
618
788
|
* Provide an optional handler that is called when the internal state of
|
|
619
789
|
* NumberInput changes. This handler is called with event and state info.
|
|
@@ -752,23 +922,33 @@ function getInputValidity({
|
|
|
752
922
|
validate,
|
|
753
923
|
locale
|
|
754
924
|
}) {
|
|
755
|
-
if (typeof validate === 'function') {
|
|
756
|
-
const result = validate(value, locale);
|
|
757
|
-
if (result === false) {
|
|
758
|
-
return false; // immediate invalid
|
|
759
|
-
}
|
|
760
|
-
// If true or undefined, continue to further validations
|
|
761
|
-
}
|
|
762
925
|
if (invalid) {
|
|
763
926
|
return false;
|
|
764
927
|
}
|
|
765
|
-
|
|
766
|
-
|
|
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;
|
|
767
938
|
}
|
|
768
|
-
|
|
769
|
-
|
|
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
|
+
}
|
|
770
946
|
}
|
|
771
|
-
|
|
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
|
|
772
952
|
}
|
|
773
953
|
|
|
774
954
|
/**
|
|
@@ -789,4 +969,4 @@ function disableWheel(e) {
|
|
|
789
969
|
e.preventDefault();
|
|
790
970
|
}
|
|
791
971
|
|
|
792
|
-
export { NumberInput, validateNumberSeparators };
|
|
972
|
+
export { NumberInput, parseNumberWithLocale, validateNumberSeparators };
|
|
@@ -60,27 +60,41 @@ forwardRef) {
|
|
|
60
60
|
const caretRef = useRef(null);
|
|
61
61
|
const popover = useRef(null);
|
|
62
62
|
const enableFloatingStyles = useFeatureFlag('enable-v12-dynamic-floating-styles') || autoAlign;
|
|
63
|
+
const lastClickWasInsidePopoverContent = useRef(false);
|
|
63
64
|
let align = mapPopoverAlign(initialAlign);
|
|
64
65
|
|
|
66
|
+
// Tracks clicks inside PopoverContent to prevent it from closing when clicked, this handles an edge
|
|
67
|
+
// case where the popover will close when clicking non-focusable elements (e.g. text)
|
|
68
|
+
useEvent(popover, 'mousedown', event => {
|
|
69
|
+
const target = event.target;
|
|
70
|
+
lastClickWasInsidePopoverContent.current = refs.floating.current?.contains(target) || false;
|
|
71
|
+
|
|
72
|
+
// reset flag
|
|
73
|
+
if (lastClickWasInsidePopoverContent.current) {
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
lastClickWasInsidePopoverContent.current = false;
|
|
76
|
+
}, 0);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
65
80
|
// The `Popover` should close whenever it and its children loses focus
|
|
66
81
|
useEvent(popover, 'focusout', event => {
|
|
67
82
|
const relatedTarget = event.relatedTarget;
|
|
68
|
-
if (isTabTip) {
|
|
69
|
-
if (relatedTarget && !popover.current?.contains(relatedTarget)) {
|
|
70
|
-
onRequestClose?.();
|
|
71
|
-
}
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
83
|
if (!relatedTarget) {
|
|
84
|
+
// do not close if PopoverContent was clicked
|
|
85
|
+
if (lastClickWasInsidePopoverContent.current) {
|
|
86
|
+
lastClickWasInsidePopoverContent.current = false;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
75
89
|
onRequestClose?.();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const isOutsideFloating = enableFloatingStyles && refs.floating.current ? !refs.floating.current.contains(relatedTarget) : true;
|
|
90
|
+
} else if (relatedTarget && !popover.current?.contains(relatedTarget)) {
|
|
91
|
+
const isOutsideFloating = enableFloatingStyles && refs.floating.current ? !refs.floating.current.contains(relatedTarget) : true;
|
|
92
|
+
const isFocusableWrapper = relatedTarget && popover.current && relatedTarget.contains(popover.current);
|
|
80
93
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
// Only close if focus moved outside both containers and not to an interactive parent wrapper
|
|
95
|
+
if (isOutsideFloating && !isFocusableWrapper) {
|
|
96
|
+
onRequestClose?.();
|
|
97
|
+
}
|
|
84
98
|
}
|
|
85
99
|
});
|
|
86
100
|
useWindowEvent('click', ({
|
|
@@ -886,7 +886,7 @@ const Tab = /*#__PURE__*/forwardRef(({
|
|
|
886
886
|
})
|
|
887
887
|
}, /*#__PURE__*/React.createElement("button", {
|
|
888
888
|
type: "button",
|
|
889
|
-
tabIndex:
|
|
889
|
+
tabIndex: -1,
|
|
890
890
|
"aria-disabled": disabled,
|
|
891
891
|
"aria-hidden": selectedIndex === index && dismissable ? 'false' : 'true',
|
|
892
892
|
disabled: disabled,
|
|
@@ -202,8 +202,10 @@ const TextArea = frFn((props, forwardRef) => {
|
|
|
202
202
|
}, labelText) : null;
|
|
203
203
|
const counter = enableCounter && maxCount && (counterMode === 'character' || counterMode === 'word') ? /*#__PURE__*/React.createElement(Text, {
|
|
204
204
|
as: "div",
|
|
205
|
-
className: counterClasses
|
|
205
|
+
className: counterClasses,
|
|
206
|
+
"aria-hidden": "true"
|
|
206
207
|
}, `${textCount}/${maxCount}`) : null;
|
|
208
|
+
const counterDescriptionId = enableCounter && maxCount ? `${id}-counter-desc` : undefined;
|
|
207
209
|
const helperId = !helperText ? undefined : `text-area-helper-text-${textAreaInstanceId}`;
|
|
208
210
|
const helper = helperText ? /*#__PURE__*/React.createElement(Text, {
|
|
209
211
|
as: "div",
|
|
@@ -234,8 +236,13 @@ const TextArea = frFn((props, forwardRef) => {
|
|
|
234
236
|
let ariaDescribedBy;
|
|
235
237
|
if (invalid) {
|
|
236
238
|
ariaDescribedBy = errorId;
|
|
237
|
-
} else if (
|
|
238
|
-
ariaDescribedBy =
|
|
239
|
+
} else if (warn && !isFluid) {
|
|
240
|
+
ariaDescribedBy = warnId;
|
|
241
|
+
} else {
|
|
242
|
+
const ids = [];
|
|
243
|
+
if (!isFluid && helperText && helperId) ids.push(helperId);
|
|
244
|
+
if (counterDescriptionId) ids.push(counterDescriptionId);
|
|
245
|
+
ariaDescribedBy = ids.length > 0 ? ids.join(' ') : undefined;
|
|
239
246
|
}
|
|
240
247
|
if (enableCounter) {
|
|
241
248
|
// handle different counter mode
|
|
@@ -292,7 +299,10 @@ const TextArea = frFn((props, forwardRef) => {
|
|
|
292
299
|
className: formItemClasses
|
|
293
300
|
}, /*#__PURE__*/React.createElement("div", {
|
|
294
301
|
className: `${prefix}--text-area__label-wrapper`
|
|
295
|
-
}, label, counter), /*#__PURE__*/React.createElement("
|
|
302
|
+
}, label, counter), enableCounter && maxCount && /*#__PURE__*/React.createElement("span", {
|
|
303
|
+
id: counterDescriptionId,
|
|
304
|
+
className: `${prefix}--visually-hidden`
|
|
305
|
+
}, counterMode === 'word' ? `Word limit ${maxCount}` : `Character limit ${maxCount}`), /*#__PURE__*/React.createElement("div", {
|
|
296
306
|
ref: wrapperRef,
|
|
297
307
|
className: textAreaWrapperClasses,
|
|
298
308
|
"data-invalid": invalid || null
|
|
@@ -135,6 +135,7 @@ const PasswordInput = /*#__PURE__*/React.forwardRef(({
|
|
|
135
135
|
[`${prefix}--tooltip--${tooltipPosition}`]: tooltipPosition,
|
|
136
136
|
[`${prefix}--tooltip--align-${tooltipAlignment}`]: tooltipAlignment
|
|
137
137
|
});
|
|
138
|
+
const tooltipClasses = cx(`${prefix}--toggle-password-tooltip`, `${prefix}--icon-tooltip`);
|
|
138
139
|
let align = undefined;
|
|
139
140
|
if (tooltipPosition === 'top' || tooltipPosition === 'bottom') {
|
|
140
141
|
if (tooltipAlignment === 'center') {
|
|
@@ -172,12 +173,12 @@ const PasswordInput = /*#__PURE__*/React.forwardRef(({
|
|
|
172
173
|
className: `${prefix}--text-input__divider`
|
|
173
174
|
}), /*#__PURE__*/React.createElement(Tooltip, {
|
|
174
175
|
align: align,
|
|
175
|
-
className:
|
|
176
|
+
className: tooltipClasses,
|
|
176
177
|
label: passwordIsVisible ? hidePasswordLabel : showPasswordLabel
|
|
177
178
|
}, /*#__PURE__*/React.createElement("button", {
|
|
178
179
|
type: "button",
|
|
179
180
|
className: passwordVisibilityToggleClasses,
|
|
180
|
-
disabled: disabled
|
|
181
|
+
disabled: disabled,
|
|
181
182
|
onClick: handleTogglePasswordVisibility
|
|
182
183
|
}, passwordVisibilityIcon)));
|
|
183
184
|
useEffect(() => {
|
|
@@ -346,7 +346,11 @@ const Dropdown = /*#__PURE__*/React.forwardRef(({
|
|
|
346
346
|
size: size,
|
|
347
347
|
className: className,
|
|
348
348
|
invalid: normalizedProps.invalid,
|
|
349
|
+
invalidText: isFluid ? invalidText : undefined,
|
|
350
|
+
invalidTextId: normalizedProps.invalidId,
|
|
349
351
|
warn: normalizedProps.warn,
|
|
352
|
+
warnText: isFluid ? warnText : undefined,
|
|
353
|
+
warnTextId: normalizedProps.warnId,
|
|
350
354
|
light: light,
|
|
351
355
|
isOpen: isOpen,
|
|
352
356
|
ref: enableFloatingStyles || autoAlign ? refs.setReference : null,
|
|
@@ -71,7 +71,7 @@ const LinkBase = /*#__PURE__*/React.forwardRef(({
|
|
|
71
71
|
ref: ref
|
|
72
72
|
}, linkProps, rest, {
|
|
73
73
|
onClick: handleOnClick
|
|
74
|
-
}), children, !inline && Icon && /*#__PURE__*/React.createElement("
|
|
74
|
+
}), children, !inline && Icon && /*#__PURE__*/React.createElement("span", {
|
|
75
75
|
className: `${prefix}--link__icon`
|
|
76
76
|
}, /*#__PURE__*/React.createElement(Icon, null)));
|
|
77
77
|
});
|
|
@@ -726,6 +726,7 @@ const FilterableMultiSelect = /*#__PURE__*/React.forwardRef(function FilterableM
|
|
|
726
726
|
return /*#__PURE__*/React.createElement(index$1.default.MenuItem, _rollupPluginBabelHelpers.extends({
|
|
727
727
|
key: itemProps.id,
|
|
728
728
|
"aria-label": itemText,
|
|
729
|
+
"aria-checked": isIndeterminate ? 'mixed' : isChecked,
|
|
729
730
|
isActive: isChecked && !item['isSelectAll'],
|
|
730
731
|
isHighlighted: highlightedIndex === index,
|
|
731
732
|
title: itemText,
|
|
@@ -511,6 +511,7 @@ const MultiSelect = /*#__PURE__*/React.forwardRef(({
|
|
|
511
511
|
key: itemProps.id,
|
|
512
512
|
isActive: isChecked && !item['isSelectAll'],
|
|
513
513
|
"aria-label": itemText,
|
|
514
|
+
"aria-checked": isIndeterminate ? 'mixed' : isChecked,
|
|
514
515
|
isHighlighted: highlightedIndex === index,
|
|
515
516
|
title: itemText,
|
|
516
517
|
disabled: itemProps['aria-disabled']
|
|
@@ -135,10 +135,14 @@ export interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInp
|
|
|
135
135
|
*/
|
|
136
136
|
min?: number;
|
|
137
137
|
/**
|
|
138
|
-
* Provide an optional handler that is called when the input
|
|
138
|
+
* Provide an optional handler that is called when the input is blurred.
|
|
139
|
+
*/
|
|
140
|
+
onBlur?: (event: React.FocusEvent<HTMLInputElement>, value?: string | number) => void;
|
|
141
|
+
/**
|
|
142
|
+
* Provide an optional handler that is called when the stepper
|
|
139
143
|
* buttons are blurred.
|
|
140
144
|
*/
|
|
141
|
-
|
|
145
|
+
onStepperBlur?: (event: React.FocusEvent<HTMLButtonElement>) => void;
|
|
142
146
|
/**
|
|
143
147
|
* Provide an optional handler that is called when the internal state of
|
|
144
148
|
* NumberInput changes. This handler is called with event and state info.
|
|
@@ -203,6 +207,15 @@ export interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInp
|
|
|
203
207
|
*/
|
|
204
208
|
warnText?: ReactNode;
|
|
205
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;
|
|
206
219
|
export declare const validateNumberSeparators: (input: string, locale: string) => boolean;
|
|
207
220
|
declare const NumberInput: React.ForwardRefExoticComponent<NumberInputProps & React.RefAttributes<HTMLInputElement>>;
|
|
208
221
|
export { NumberInput };
|