@jobber/components 7.10.0 → 7.11.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.
Files changed (106) hide show
  1. package/dist/Autocomplete/index.cjs +1 -0
  2. package/dist/Autocomplete/index.mjs +1 -0
  3. package/dist/Card/index.cjs +3 -0
  4. package/dist/Card/index.mjs +3 -0
  5. package/dist/Chip/index.cjs +1 -0
  6. package/dist/Chip/index.mjs +1 -0
  7. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +1 -0
  8. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +1 -0
  9. package/dist/Chips/InternalChipDismissible/index.cjs +1 -0
  10. package/dist/Chips/InternalChipDismissible/index.mjs +1 -0
  11. package/dist/Chips/index.cjs +1 -0
  12. package/dist/Chips/index.mjs +1 -0
  13. package/dist/Combobox/components/ComboboxActivator/index.cjs +1 -0
  14. package/dist/Combobox/components/ComboboxActivator/index.mjs +1 -0
  15. package/dist/Combobox/components/ComboboxContent/index.cjs +1 -0
  16. package/dist/Combobox/components/ComboboxContent/index.mjs +1 -0
  17. package/dist/Combobox/components/ComboboxTrigger/index.cjs +1 -0
  18. package/dist/Combobox/components/ComboboxTrigger/index.mjs +1 -0
  19. package/dist/Combobox/index.cjs +1 -0
  20. package/dist/Combobox/index.mjs +1 -0
  21. package/dist/ConfirmationModal/index.cjs +1 -0
  22. package/dist/ConfirmationModal/index.mjs +1 -0
  23. package/dist/DataDump/index.cjs +3 -0
  24. package/dist/DataDump/index.mjs +3 -0
  25. package/dist/DataList/components/DataListActions/index.cjs +1 -0
  26. package/dist/DataList/components/DataListActions/index.mjs +1 -0
  27. package/dist/DataList/components/DataListBulkActions/index.cjs +1 -0
  28. package/dist/DataList/components/DataListBulkActions/index.mjs +1 -0
  29. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +1 -0
  30. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +1 -0
  31. package/dist/DataList/components/DataListFilters/index.cjs +1 -0
  32. package/dist/DataList/components/DataListFilters/index.mjs +1 -0
  33. package/dist/DataList/components/DataListHeader/index.cjs +1 -0
  34. package/dist/DataList/components/DataListHeader/index.mjs +1 -0
  35. package/dist/DataList/components/DataListItem/index.cjs +1 -0
  36. package/dist/DataList/components/DataListItem/index.mjs +1 -0
  37. package/dist/DataList/components/DataListItemActions/index.cjs +1 -0
  38. package/dist/DataList/components/DataListItemActions/index.mjs +1 -0
  39. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +1 -0
  40. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +1 -0
  41. package/dist/DataList/components/DataListItems/index.cjs +1 -0
  42. package/dist/DataList/components/DataListItems/index.mjs +1 -0
  43. package/dist/DataList/components/DataListLayout/index.cjs +1 -0
  44. package/dist/DataList/components/DataListLayout/index.mjs +1 -0
  45. package/dist/DataList/components/DataListLayoutActions/index.cjs +1 -0
  46. package/dist/DataList/components/DataListLayoutActions/index.mjs +1 -0
  47. package/dist/DataList/index.cjs +1 -0
  48. package/dist/DataList/index.mjs +1 -0
  49. package/dist/DatePicker/index.cjs +1 -0
  50. package/dist/DatePicker/index.mjs +1 -0
  51. package/dist/DrawerRoot-cjs.js +181 -968
  52. package/dist/DrawerRoot-es.js +5 -734
  53. package/dist/FormatFile/index.cjs +1 -0
  54. package/dist/FormatFile/index.mjs +1 -0
  55. package/dist/Gallery/index.cjs +1 -0
  56. package/dist/Gallery/index.mjs +1 -0
  57. package/dist/InputDate/index.cjs +1 -0
  58. package/dist/InputDate/index.mjs +1 -0
  59. package/dist/InputNumberExperimental-cjs.js +783 -0
  60. package/dist/InputNumberExperimental-es.js +763 -0
  61. package/dist/LightBox/index.cjs +1 -0
  62. package/dist/LightBox/index.mjs +1 -0
  63. package/dist/Menu/index.cjs +3 -0
  64. package/dist/Menu/index.mjs +3 -0
  65. package/dist/MenuSubmenuTrigger-cjs.js +202 -447
  66. package/dist/MenuSubmenuTrigger-es.js +7 -249
  67. package/dist/Modal/index.cjs +1 -0
  68. package/dist/Modal/index.mjs +1 -0
  69. package/dist/NumberFieldInput-cjs.js +1828 -0
  70. package/dist/NumberFieldInput-es.js +1788 -0
  71. package/dist/Page/index.cjs +3 -0
  72. package/dist/Page/index.mjs +3 -0
  73. package/dist/Popover/index.cjs +1 -0
  74. package/dist/Popover/index.mjs +1 -0
  75. package/dist/Tooltip/index.cjs +1 -0
  76. package/dist/Tooltip/index.mjs +1 -0
  77. package/dist/docs/Menu/Menu.md +197 -37
  78. package/dist/floating-ui.react-cjs.js +35 -34
  79. package/dist/floating-ui.react-dom-cjs.js +65 -64
  80. package/dist/floating-ui.react-dom-es.js +2 -1
  81. package/dist/floating-ui.react-es.js +2 -1
  82. package/dist/floating-ui.utils.dom-cjs.js +185 -0
  83. package/dist/floating-ui.utils.dom-es.js +165 -0
  84. package/dist/index.cjs +3 -0
  85. package/dist/index.esm-cjs.js +0 -183
  86. package/dist/index.esm-es.js +1 -165
  87. package/dist/index.mjs +3 -0
  88. package/dist/primitives/BottomSheet/index.cjs +3 -1
  89. package/dist/primitives/BottomSheet/index.mjs +3 -1
  90. package/dist/primitives/InputNumberExperimental/InputNumberExperimental.d.ts +20 -0
  91. package/dist/primitives/InputNumberExperimental/index.cjs +22 -0
  92. package/dist/primitives/InputNumberExperimental/index.d.ts +2 -0
  93. package/dist/primitives/InputNumberExperimental/index.mjs +16 -0
  94. package/dist/primitives/InputNumberExperimental/types.d.ts +147 -0
  95. package/dist/primitives/index.cjs +9 -1
  96. package/dist/primitives/index.d.ts +2 -0
  97. package/dist/primitives/index.mjs +8 -1
  98. package/dist/styles.css +499 -0
  99. package/dist/unstyledPrimitives/index.cjs +264 -2039
  100. package/dist/unstyledPrimitives/index.mjs +72 -1847
  101. package/dist/useBaseUiId-cjs.js +275 -0
  102. package/dist/useBaseUiId-es.js +251 -0
  103. package/dist/useValueChanged-cjs.js +820 -0
  104. package/dist/useValueChanged-es.js +736 -0
  105. package/package.json +2 -2
  106. package/rollup.config.mjs +13 -2
@@ -0,0 +1,1828 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var useValueChanged = require('./useValueChanged-cjs.js');
5
+ var useRenderElement = require('./useRenderElement-cjs.js');
6
+ var floatingUi_utils_dom = require('./floating-ui.utils.dom-cjs.js');
7
+ var useBaseUiId = require('./useBaseUiId-cjs.js');
8
+ var jsxRuntime = require('react/jsx-runtime');
9
+ var ReactDOM = require('react-dom');
10
+
11
+ function _interopNamespaceDefault(e) {
12
+ var n = Object.create(null);
13
+ if (e) {
14
+ Object.keys(e).forEach(function (k) {
15
+ if (k !== 'default') {
16
+ var d = Object.getOwnPropertyDescriptor(e, k);
17
+ Object.defineProperty(n, k, d.get ? d : {
18
+ enumerable: true,
19
+ get: function () { return e[k]; }
20
+ });
21
+ }
22
+ });
23
+ }
24
+ n.default = e;
25
+ return Object.freeze(n);
26
+ }
27
+
28
+ var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
29
+ var ReactDOM__namespace = /*#__PURE__*/_interopNamespaceDefault(ReactDOM);
30
+
31
+ /**
32
+ * Returns a function that forces a rerender.
33
+ */
34
+ function useForcedRerendering() {
35
+ const [, setState] = React__namespace.useState({});
36
+ return React__namespace.useCallback(() => {
37
+ setState({});
38
+ }, []);
39
+ }
40
+
41
+ let FieldControlDataAttributes = /*#__PURE__*/function (FieldControlDataAttributes) {
42
+ /**
43
+ * Present when the field is disabled.
44
+ */
45
+ FieldControlDataAttributes["disabled"] = "data-disabled";
46
+ /**
47
+ * Present when the field is in valid state.
48
+ */
49
+ FieldControlDataAttributes["valid"] = "data-valid";
50
+ /**
51
+ * Present when the field is in invalid state.
52
+ */
53
+ FieldControlDataAttributes["invalid"] = "data-invalid";
54
+ /**
55
+ * Present when the field has been touched.
56
+ */
57
+ FieldControlDataAttributes["touched"] = "data-touched";
58
+ /**
59
+ * Present when the field's value has changed.
60
+ */
61
+ FieldControlDataAttributes["dirty"] = "data-dirty";
62
+ /**
63
+ * Present when the field is filled.
64
+ */
65
+ FieldControlDataAttributes["filled"] = "data-filled";
66
+ /**
67
+ * Present when the field control is focused.
68
+ */
69
+ FieldControlDataAttributes["focused"] = "data-focused";
70
+ return FieldControlDataAttributes;
71
+ }({});
72
+
73
+ const DEFAULT_VALIDITY_STATE = {
74
+ badInput: false,
75
+ customError: false,
76
+ patternMismatch: false,
77
+ rangeOverflow: false,
78
+ rangeUnderflow: false,
79
+ stepMismatch: false,
80
+ tooLong: false,
81
+ tooShort: false,
82
+ typeMismatch: false,
83
+ valid: null,
84
+ valueMissing: false
85
+ };
86
+ const DEFAULT_FIELD_STATE_ATTRIBUTES = {
87
+ valid: null,
88
+ touched: false,
89
+ dirty: false,
90
+ filled: false,
91
+ focused: false
92
+ };
93
+ const DEFAULT_FIELD_ROOT_STATE = {
94
+ disabled: false,
95
+ ...DEFAULT_FIELD_STATE_ATTRIBUTES
96
+ };
97
+ const fieldValidityMapping = {
98
+ valid(value) {
99
+ if (value === null) {
100
+ return null;
101
+ }
102
+ if (value) {
103
+ return {
104
+ [FieldControlDataAttributes.valid]: ''
105
+ };
106
+ }
107
+ return {
108
+ [FieldControlDataAttributes.invalid]: ''
109
+ };
110
+ }
111
+ };
112
+
113
+ const FieldRootContext = /*#__PURE__*/React__namespace.createContext({
114
+ invalid: undefined,
115
+ name: undefined,
116
+ validityData: {
117
+ state: DEFAULT_VALIDITY_STATE,
118
+ errors: [],
119
+ error: '',
120
+ value: '',
121
+ initialValue: null
122
+ },
123
+ setValidityData: useRenderElement.NOOP,
124
+ disabled: undefined,
125
+ touched: DEFAULT_FIELD_STATE_ATTRIBUTES.touched,
126
+ setTouched: useRenderElement.NOOP,
127
+ dirty: DEFAULT_FIELD_STATE_ATTRIBUTES.dirty,
128
+ setDirty: useRenderElement.NOOP,
129
+ filled: DEFAULT_FIELD_STATE_ATTRIBUTES.filled,
130
+ setFilled: useRenderElement.NOOP,
131
+ focused: DEFAULT_FIELD_STATE_ATTRIBUTES.focused,
132
+ setFocused: useRenderElement.NOOP,
133
+ validate: () => null,
134
+ validationMode: 'onSubmit',
135
+ validationDebounceTime: 0,
136
+ shouldValidateOnChange: () => false,
137
+ state: DEFAULT_FIELD_ROOT_STATE,
138
+ markedDirtyRef: {
139
+ current: false
140
+ },
141
+ validation: {
142
+ getValidationProps: (props = useRenderElement.EMPTY_OBJECT) => props,
143
+ getInputValidationProps: (props = useRenderElement.EMPTY_OBJECT) => props,
144
+ inputRef: {
145
+ current: null
146
+ },
147
+ commit: async () => {}
148
+ }
149
+ });
150
+ if (process.env.NODE_ENV !== "production") FieldRootContext.displayName = "FieldRootContext";
151
+ function useFieldRootContext(optional = true) {
152
+ const context = React__namespace.useContext(FieldRootContext);
153
+ if (context.setValidityData === useRenderElement.NOOP && !optional) {
154
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: FieldRootContext is missing. Field parts must be placed within <Field.Root>.' : useRenderElement.formatErrorMessage(28));
155
+ }
156
+ return context;
157
+ }
158
+
159
+ /**
160
+ * Combines the field's client-side, stateful validity data with the external invalid state to
161
+ * determine the field's true validity.
162
+ */
163
+ function getCombinedFieldValidityData(validityData, invalid) {
164
+ return {
165
+ ...validityData,
166
+ state: {
167
+ ...validityData.state,
168
+ valid: !invalid && validityData.state.valid
169
+ }
170
+ };
171
+ }
172
+
173
+ const FormContext = /*#__PURE__*/React__namespace.createContext({
174
+ formRef: {
175
+ current: {
176
+ fields: new Map()
177
+ }
178
+ },
179
+ errors: {},
180
+ clearErrors: useRenderElement.NOOP,
181
+ validationMode: 'onSubmit',
182
+ submitAttemptedRef: {
183
+ current: false
184
+ }
185
+ });
186
+ if (process.env.NODE_ENV !== "production") FormContext.displayName = "FormContext";
187
+ function useFormContext() {
188
+ return React__namespace.useContext(FormContext);
189
+ }
190
+
191
+ function useField(params) {
192
+ const {
193
+ enabled = true,
194
+ value,
195
+ id,
196
+ name,
197
+ controlRef,
198
+ commit
199
+ } = params;
200
+ const {
201
+ formRef
202
+ } = useFormContext();
203
+ const {
204
+ invalid,
205
+ markedDirtyRef,
206
+ validityData,
207
+ setValidityData
208
+ } = useFieldRootContext();
209
+ const getValue = useValueChanged.useStableCallback(params.getValue);
210
+ useValueChanged.useIsoLayoutEffect(() => {
211
+ if (!enabled) {
212
+ return;
213
+ }
214
+ let initialValue = value;
215
+ if (initialValue === undefined) {
216
+ initialValue = getValue();
217
+ }
218
+ if (validityData.initialValue === null && initialValue !== null) {
219
+ setValidityData(prev => ({
220
+ ...prev,
221
+ initialValue
222
+ }));
223
+ }
224
+ }, [enabled, setValidityData, value, validityData.initialValue, getValue]);
225
+ useValueChanged.useIsoLayoutEffect(() => {
226
+ if (!enabled || !id) {
227
+ return;
228
+ }
229
+ formRef.current.fields.set(id, {
230
+ getValue,
231
+ name,
232
+ controlRef,
233
+ validityData: getCombinedFieldValidityData(validityData, invalid),
234
+ validate(flushSync = true) {
235
+ let nextValue = value;
236
+ if (nextValue === undefined) {
237
+ nextValue = getValue();
238
+ }
239
+ markedDirtyRef.current = true;
240
+ if (!flushSync) {
241
+ commit(nextValue);
242
+ } else {
243
+ // Synchronously update the validity state so the submit event can be prevented.
244
+ ReactDOM__namespace.flushSync(() => commit(nextValue));
245
+ }
246
+ }
247
+ });
248
+ }, [commit, controlRef, enabled, formRef, getValue, id, invalid, markedDirtyRef, name, validityData, value]);
249
+ useValueChanged.useIsoLayoutEffect(() => {
250
+ const fields = formRef.current.fields;
251
+ return () => {
252
+ if (id) {
253
+ fields.delete(id);
254
+ }
255
+ };
256
+ }, [formRef, id]);
257
+ }
258
+
259
+ /**
260
+ * A context for providing [labelable elements](https://html.spec.whatwg.org/multipage/forms.html#category-label)\
261
+ * with an accessible name (label) and description.
262
+ */
263
+ const LabelableContext = /*#__PURE__*/React__namespace.createContext({
264
+ controlId: undefined,
265
+ registerControlId: useRenderElement.NOOP,
266
+ labelId: undefined,
267
+ setLabelId: useRenderElement.NOOP,
268
+ messageIds: [],
269
+ setMessageIds: useRenderElement.NOOP,
270
+ getDescriptionProps: externalProps => externalProps
271
+ });
272
+ if (process.env.NODE_ENV !== "production") LabelableContext.displayName = "LabelableContext";
273
+ function useLabelableContext() {
274
+ return React__namespace.useContext(LabelableContext);
275
+ }
276
+
277
+ function useLabelableId(params = {}) {
278
+ const {
279
+ id,
280
+ implicit = false,
281
+ controlRef
282
+ } = params;
283
+ const {
284
+ controlId,
285
+ registerControlId
286
+ } = useLabelableContext();
287
+ const defaultId = useBaseUiId.useBaseUiId(id);
288
+ const controlIdForEffect = implicit ? controlId : undefined;
289
+ const controlSourceRef = useRenderElement.useRefWithInit(() => Symbol('labelable-control'));
290
+ const hasRegisteredRef = React__namespace.useRef(false);
291
+ const hadExplicitIdRef = React__namespace.useRef(id != null);
292
+ const unregisterControlId = useValueChanged.useStableCallback(() => {
293
+ if (!hasRegisteredRef.current || registerControlId === useRenderElement.NOOP) {
294
+ return;
295
+ }
296
+ hasRegisteredRef.current = false;
297
+ registerControlId(controlSourceRef.current, undefined);
298
+ });
299
+ useValueChanged.useIsoLayoutEffect(() => {
300
+ if (registerControlId === useRenderElement.NOOP) {
301
+ return undefined;
302
+ }
303
+ let nextId;
304
+ if (implicit) {
305
+ const elem = controlRef?.current;
306
+ if (floatingUi_utils_dom.isElement(elem) && elem.closest('label') != null) {
307
+ nextId = id ?? null;
308
+ } else {
309
+ nextId = controlIdForEffect ?? defaultId;
310
+ }
311
+ } else if (id != null) {
312
+ hadExplicitIdRef.current = true;
313
+ nextId = id;
314
+ } else if (hadExplicitIdRef.current) {
315
+ nextId = defaultId;
316
+ } else {
317
+ unregisterControlId();
318
+ return undefined;
319
+ }
320
+ if (nextId === undefined) {
321
+ unregisterControlId();
322
+ return undefined;
323
+ }
324
+ hasRegisteredRef.current = true;
325
+ registerControlId(controlSourceRef.current, nextId);
326
+ return undefined;
327
+ }, [id, controlRef, controlIdForEffect, registerControlId, implicit, defaultId, controlSourceRef, unregisterControlId]);
328
+ React__namespace.useEffect(() => {
329
+ return unregisterControlId;
330
+ }, [unregisterControlId]);
331
+ return controlId ?? defaultId;
332
+ }
333
+
334
+ const cache = new Map();
335
+ function getFormatter(locale, options) {
336
+ const optionsString = JSON.stringify({
337
+ locale,
338
+ options
339
+ });
340
+ const cachedFormatter = cache.get(optionsString);
341
+ if (cachedFormatter) {
342
+ return cachedFormatter;
343
+ }
344
+ const formatter = new Intl.NumberFormat(locale, options);
345
+ cache.set(optionsString, formatter);
346
+ return formatter;
347
+ }
348
+ function formatNumber(value, locale, options) {
349
+ if (value == null) {
350
+ return '';
351
+ }
352
+ return getFormatter(locale, options).format(value);
353
+ }
354
+ function formatNumberMaxPrecision(value, locale, options) {
355
+ return formatNumber(value, locale, {
356
+ ...options,
357
+ maximumFractionDigits: 20
358
+ });
359
+ }
360
+
361
+ const EMPTY = 0;
362
+ class Interval extends useValueChanged.Timeout {
363
+ static create() {
364
+ return new Interval();
365
+ }
366
+
367
+ /**
368
+ * Executes `fn` at `delay` interval, clearing any previously scheduled call.
369
+ */
370
+ start(delay, fn) {
371
+ this.clear();
372
+ this.currentId = setInterval(() => {
373
+ fn();
374
+ }, delay);
375
+ }
376
+ clear = () => {
377
+ if (this.currentId !== EMPTY) {
378
+ clearInterval(this.currentId);
379
+ this.currentId = EMPTY;
380
+ }
381
+ };
382
+ }
383
+
384
+ /**
385
+ * A `setInterval` with automatic cleanup and guard.
386
+ */
387
+ function useInterval() {
388
+ const timeout = useRenderElement.useRefWithInit(Interval.create).current;
389
+ useValueChanged.useOnMount(timeout.disposeEffect);
390
+ return timeout;
391
+ }
392
+
393
+ const NumberFieldRootContext = /*#__PURE__*/React__namespace.createContext(undefined);
394
+ if (process.env.NODE_ENV !== "production") NumberFieldRootContext.displayName = "NumberFieldRootContext";
395
+ function useNumberFieldRootContext() {
396
+ const context = React__namespace.useContext(NumberFieldRootContext);
397
+ if (context === undefined) {
398
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: NumberFieldRootContext is missing. NumberField parts must be placed within <NumberField.Root>.' : useRenderElement.formatErrorMessage(43));
399
+ }
400
+ return context;
401
+ }
402
+
403
+ const stateAttributesMapping$1 = {
404
+ inputValue: () => null,
405
+ value: () => null,
406
+ ...fieldValidityMapping
407
+ };
408
+
409
+ const HAN_NUMERALS = ['零', '〇', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
410
+ // Map Han numeral characters to ASCII digits. Includes both forms of zero.
411
+ const HAN_NUMERAL_TO_DIGIT = {
412
+ 零: '0',
413
+ 〇: '0',
414
+ 一: '1',
415
+ 二: '2',
416
+ 三: '3',
417
+ 四: '4',
418
+ 五: '5',
419
+ 六: '6',
420
+ 七: '7',
421
+ 八: '8',
422
+ 九: '9'
423
+ };
424
+ const ARABIC_NUMERALS = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
425
+ const PERSIAN_NUMERALS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
426
+ const FULLWIDTH_NUMERALS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
427
+ const PERCENTAGES = ['%', '٪', '%', '﹪'];
428
+ const PERMILLE = ['‰', '؉'];
429
+ const UNICODE_MINUS_SIGNS = ['−', '-', '‒', '–', '—', '﹣'];
430
+ const UNICODE_PLUS_SIGNS = ['+', '﹢'];
431
+
432
+ // Fullwidth punctuation common in CJK inputs
433
+ const FULLWIDTH_DECIMAL = '.'; // U+FF0E
434
+ const FULLWIDTH_GROUP = ','; // U+FF0C
435
+
436
+ const ARABIC_RE = new RegExp(`[${ARABIC_NUMERALS.join('')}]`, 'g');
437
+ const PERSIAN_RE = new RegExp(`[${PERSIAN_NUMERALS.join('')}]`, 'g');
438
+ const FULLWIDTH_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`, 'g');
439
+ const HAN_RE = new RegExp(`[${HAN_NUMERALS.join('')}]`, 'g');
440
+ const PERCENT_RE = new RegExp(`[${PERCENTAGES.join('')}]`);
441
+ const PERMILLE_RE = new RegExp(`[${PERMILLE.join('')}]`);
442
+
443
+ // Detection regexes (non-global to avoid lastIndex side effects)
444
+ const ARABIC_DETECT_RE = /[٠١٢٣٤٥٦٧٨٩]/;
445
+ const PERSIAN_DETECT_RE = /[۰۱۲۳۴۵۶۷۸۹]/;
446
+ const HAN_DETECT_RE = /[零〇一二三四五六七八九]/;
447
+ const FULLWIDTH_DETECT_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`);
448
+ const BASE_NON_NUMERIC_SYMBOLS = ['.', ',', FULLWIDTH_DECIMAL, FULLWIDTH_GROUP, '٫', '٬'];
449
+ const SPACE_SEPARATOR_RE = /\p{Zs}/u;
450
+ const PLUS_SIGNS_WITH_ASCII = ['+', ...UNICODE_PLUS_SIGNS];
451
+ const MINUS_SIGNS_WITH_ASCII = ['-', ...UNICODE_MINUS_SIGNS];
452
+ const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
453
+ const escapeClassChar = s => s.replace(/[-\\\]^]/g, m => `\\${m}`); // escape for use inside [...]
454
+
455
+ const charClassFrom = chars => `[${chars.map(escapeClassChar).join('')}]`;
456
+ const ANY_MINUS_CLASS = charClassFrom(['-'].concat(UNICODE_MINUS_SIGNS));
457
+ const ANY_PLUS_CLASS = charClassFrom(['+'].concat(UNICODE_PLUS_SIGNS));
458
+ const ANY_MINUS_RE = new RegExp(ANY_MINUS_CLASS, 'gu');
459
+ const ANY_PLUS_RE = new RegExp(ANY_PLUS_CLASS, 'gu');
460
+ const ANY_MINUS_DETECT_RE = new RegExp(ANY_MINUS_CLASS);
461
+ const ANY_PLUS_DETECT_RE = new RegExp(ANY_PLUS_CLASS);
462
+ function getNumberLocaleDetails(locale, options) {
463
+ const parts = getFormatter(locale, options).formatToParts(11111.1);
464
+ const result = {};
465
+ parts.forEach(part => {
466
+ result[part.type] = part.value;
467
+ });
468
+
469
+ // The formatting options may result in not returning a decimal.
470
+ getFormatter(locale).formatToParts(0.1).forEach(part => {
471
+ if (part.type === 'decimal') {
472
+ result[part.type] = part.value;
473
+ }
474
+ });
475
+ return result;
476
+ }
477
+ function parseNumber(formattedNumber, locale, options) {
478
+ if (formattedNumber == null) {
479
+ return null;
480
+ }
481
+
482
+ // Normalize control characters and whitespace; remove bidi/format controls
483
+ let input = String(formattedNumber).replace(/\p{Cf}/gu, '').trim();
484
+
485
+ // Normalize unicode minus/plus to ASCII, handle leading/trailing signs
486
+ input = input.replace(ANY_MINUS_RE, '-').replace(ANY_PLUS_RE, '+');
487
+ let isNegative = false;
488
+
489
+ // Trailing sign, e.g. "1234-" / "1234+"
490
+ const trailing = input.match(/([+-])\s*$/);
491
+ if (trailing) {
492
+ if (trailing[1] === '-') {
493
+ isNegative = true;
494
+ }
495
+ input = input.replace(/([+-])\s*$/, '');
496
+ }
497
+ // Leading sign
498
+ const leading = input.match(/^\s*([+-])/);
499
+ if (leading) {
500
+ if (leading[1] === '-') {
501
+ isNegative = true;
502
+ }
503
+ input = input.replace(/^\s*[+-]/, '');
504
+ }
505
+
506
+ // Heuristic locale detection
507
+ let computedLocale = locale;
508
+ if (computedLocale === undefined) {
509
+ if (ARABIC_DETECT_RE.test(input) || PERSIAN_DETECT_RE.test(input)) {
510
+ computedLocale = 'ar';
511
+ } else if (HAN_DETECT_RE.test(input)) {
512
+ computedLocale = 'zh';
513
+ }
514
+ }
515
+ const {
516
+ group,
517
+ decimal,
518
+ currency
519
+ } = getNumberLocaleDetails(computedLocale, options);
520
+
521
+ // Build robust unit regex from all unit parts (such as "km/h")
522
+ const unitParts = getFormatter(computedLocale, options).formatToParts(1).filter(p => p.type === 'unit').map(p => escapeRegExp(p.value));
523
+ const unitRegex = unitParts.length ? new RegExp(unitParts.join('|'), 'g') : null;
524
+ let groupRegex = null;
525
+ if (group) {
526
+ const isSpaceGroup = /\p{Zs}/u.test(group);
527
+ const isApostropheGroup = group === "'" || group === '’';
528
+
529
+ // Check if the group separator is a space-like character.
530
+ // If so, we'll replace all such characters with an empty string.
531
+ if (isSpaceGroup) {
532
+ groupRegex = /\p{Zs}/gu;
533
+ } else if (isApostropheGroup) {
534
+ // Some environments format numbers with ASCII apostrophe and others with a curly apostrophe.
535
+ groupRegex = /['’]/g;
536
+ } else {
537
+ groupRegex = new RegExp(escapeRegExp(group), 'g');
538
+ }
539
+ }
540
+ const replacements = [{
541
+ regex: group ? groupRegex : null,
542
+ replacement: ''
543
+ }, {
544
+ regex: decimal ? new RegExp(escapeRegExp(decimal), 'g') : null,
545
+ replacement: '.'
546
+ },
547
+ // Fullwidth punctuation
548
+ {
549
+ regex: /./g,
550
+ replacement: '.'
551
+ },
552
+ // FULLWIDTH_DECIMAL
553
+ {
554
+ regex: /,/g,
555
+ replacement: ''
556
+ },
557
+ // FULLWIDTH_GROUP
558
+ // Arabic punctuation
559
+ {
560
+ regex: /٫/g,
561
+ replacement: '.'
562
+ },
563
+ // ARABIC DECIMAL SEPARATOR (U+066B)
564
+ {
565
+ regex: /٬/g,
566
+ replacement: ''
567
+ },
568
+ // ARABIC THOUSANDS SEPARATOR (U+066C)
569
+ // Currency & unit labels
570
+ {
571
+ regex: currency ? new RegExp(escapeRegExp(currency), 'g') : null,
572
+ replacement: ''
573
+ }, {
574
+ regex: unitRegex,
575
+ replacement: ''
576
+ },
577
+ // Numeral systems to ASCII digits
578
+ {
579
+ regex: ARABIC_RE,
580
+ replacement: ch => String(ARABIC_NUMERALS.indexOf(ch))
581
+ }, {
582
+ regex: PERSIAN_RE,
583
+ replacement: ch => String(PERSIAN_NUMERALS.indexOf(ch))
584
+ }, {
585
+ regex: FULLWIDTH_RE,
586
+ replacement: ch => String(FULLWIDTH_NUMERALS.indexOf(ch))
587
+ }, {
588
+ regex: HAN_RE,
589
+ replacement: ch => HAN_NUMERAL_TO_DIGIT[ch]
590
+ }];
591
+ let unformatted = replacements.reduce((acc, {
592
+ regex,
593
+ replacement
594
+ }) => {
595
+ return regex ? acc.replace(regex, replacement) : acc;
596
+ }, input);
597
+
598
+ // Mixed-locale safety: keep only the last '.' as decimal
599
+ const lastDot = unformatted.lastIndexOf('.');
600
+ if (lastDot !== -1) {
601
+ unformatted = `${unformatted.slice(0, lastDot).replace(/\./g, '')}.${unformatted.slice(lastDot + 1).replace(/\./g, '')}`;
602
+ }
603
+
604
+ // Guard against Infinity inputs (ASCII and symbol)
605
+ if (/^[-+]?Infinity$/i.test(input) || /[∞]/.test(input)) {
606
+ return null;
607
+ }
608
+ const parseTarget = (isNegative ? '-' : '') + unformatted;
609
+ let num = parseFloat(parseTarget);
610
+ const style = options?.style;
611
+ const isUnitPercent = style === 'unit' && options?.unit === 'percent';
612
+ const hasPercentSymbol = PERCENT_RE.test(formattedNumber) || style === 'percent';
613
+ const hasPermilleSymbol = PERMILLE_RE.test(formattedNumber);
614
+ if (hasPermilleSymbol) {
615
+ num /= 1000;
616
+ } else if (!isUnitPercent && hasPercentSymbol) {
617
+ num /= 100;
618
+ }
619
+ if (Number.isNaN(num)) {
620
+ return null;
621
+ }
622
+ return num;
623
+ }
624
+
625
+ const CHANGE_VALUE_TICK_DELAY = 60;
626
+ const START_AUTO_CHANGE_DELAY = 400;
627
+ const TOUCH_TIMEOUT = 50;
628
+ const MAX_POINTER_MOVES_AFTER_TOUCH = 3;
629
+ const SCROLLING_POINTER_MOVE_DISTANCE = 8;
630
+ const DEFAULT_STEP = 1;
631
+
632
+ const STEP_EPSILON_FACTOR = 1e-10;
633
+ function getFractionDigits(format) {
634
+ const defaultOptions = getFormatter('en-US').resolvedOptions();
635
+ const minimumFractionDigits = format?.minimumFractionDigits ?? defaultOptions.minimumFractionDigits ?? 0;
636
+ const maximumFractionDigits = Math.max(format?.maximumFractionDigits ?? defaultOptions.maximumFractionDigits ?? 20, minimumFractionDigits);
637
+ return {
638
+ maximumFractionDigits,
639
+ minimumFractionDigits
640
+ };
641
+ }
642
+ function roundToFractionDigits(value, maximumFractionDigits) {
643
+ if (!Number.isFinite(value)) {
644
+ return value;
645
+ }
646
+ const digits = Math.min(Math.max(maximumFractionDigits, 0), 20);
647
+ return Number(value.toFixed(digits));
648
+ }
649
+ function removeFloatingPointErrors(value, format) {
650
+ const {
651
+ maximumFractionDigits
652
+ } = getFractionDigits(format);
653
+ return roundToFractionDigits(value, maximumFractionDigits);
654
+ }
655
+ function snapToStep(clampedValue, base, step, mode = 'directional') {
656
+ if (step === 0) {
657
+ return clampedValue;
658
+ }
659
+ const stepSize = Math.abs(step);
660
+ const direction = Math.sign(step);
661
+ const tolerance = stepSize * STEP_EPSILON_FACTOR * direction;
662
+ const divisor = mode === 'nearest' ? step : stepSize;
663
+ const rawSteps = (clampedValue - base + tolerance) / divisor;
664
+ let snappedSteps;
665
+ if (mode === 'nearest') {
666
+ snappedSteps = Math.round(rawSteps);
667
+ } else if (direction > 0) {
668
+ snappedSteps = Math.floor(rawSteps);
669
+ } else {
670
+ snappedSteps = Math.ceil(rawSteps);
671
+ }
672
+ const stepForResult = mode === 'nearest' ? step : stepSize;
673
+ return base + snappedSteps * stepForResult;
674
+ }
675
+ function toValidatedNumber(value, {
676
+ step,
677
+ minWithDefault,
678
+ maxWithDefault,
679
+ minWithZeroDefault,
680
+ format,
681
+ snapOnStep,
682
+ small,
683
+ clamp: shouldClamp
684
+ }) {
685
+ if (value === null) {
686
+ return value;
687
+ }
688
+ const clampedValue = shouldClamp ? useValueChanged.clamp(value, minWithDefault, maxWithDefault) : value;
689
+ if (step != null && snapOnStep) {
690
+ if (step === 0) {
691
+ return removeFloatingPointErrors(clampedValue, format);
692
+ }
693
+
694
+ // If a real minimum is provided, use it
695
+ let base = minWithZeroDefault;
696
+ if (!small && minWithDefault !== Number.MIN_SAFE_INTEGER) {
697
+ base = minWithDefault;
698
+ }
699
+ const snappedValue = snapToStep(clampedValue, base, step, small ? 'nearest' : 'directional');
700
+ return removeFloatingPointErrors(snappedValue, format);
701
+ }
702
+ return removeFloatingPointErrors(clampedValue, format);
703
+ }
704
+
705
+ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function NumberFieldRoot(componentProps, forwardedRef) {
706
+ const {
707
+ id: idProp,
708
+ min,
709
+ max,
710
+ smallStep = 0.1,
711
+ step: stepProp = 1,
712
+ largeStep = 10,
713
+ required = false,
714
+ disabled: disabledProp = false,
715
+ readOnly = false,
716
+ name: nameProp,
717
+ defaultValue,
718
+ value: valueProp,
719
+ onValueChange: onValueChangeProp,
720
+ onValueCommitted: onValueCommittedProp,
721
+ allowWheelScrub = false,
722
+ snapOnStep = false,
723
+ allowOutOfRange = false,
724
+ format,
725
+ locale,
726
+ render,
727
+ className,
728
+ inputRef: inputRefProp,
729
+ ...elementProps
730
+ } = componentProps;
731
+ const {
732
+ setDirty,
733
+ validityData,
734
+ disabled: fieldDisabled,
735
+ setFilled,
736
+ invalid,
737
+ name: fieldName,
738
+ state: fieldState,
739
+ validation,
740
+ shouldValidateOnChange
741
+ } = useFieldRootContext();
742
+ const disabled = fieldDisabled || disabledProp;
743
+ const name = fieldName ?? nameProp;
744
+ const step = stepProp === 'any' ? 1 : stepProp;
745
+ const [isScrubbing, setIsScrubbing] = React__namespace.useState(false);
746
+ const minWithDefault = min ?? Number.MIN_SAFE_INTEGER;
747
+ const maxWithDefault = max ?? Number.MAX_SAFE_INTEGER;
748
+ const minWithZeroDefault = min ?? 0;
749
+ const formatStyle = format?.style;
750
+ const inputRef = React__namespace.useRef(null);
751
+ const hiddenInputRef = useRenderElement.useMergedRefs(inputRefProp, validation.inputRef);
752
+ const id = useLabelableId({
753
+ id: idProp
754
+ });
755
+ const [valueUnwrapped, setValueUnwrapped] = useValueChanged.useControlled({
756
+ controlled: valueProp,
757
+ default: defaultValue,
758
+ name: 'NumberField',
759
+ state: 'value'
760
+ });
761
+ const value = valueUnwrapped ?? null;
762
+ const valueRef = useValueChanged.useValueAsRef(value);
763
+ useValueChanged.useIsoLayoutEffect(() => {
764
+ setFilled(value !== null);
765
+ }, [setFilled, value]);
766
+ const forceRender = useForcedRerendering();
767
+ const formatOptionsRef = useValueChanged.useValueAsRef(format);
768
+ const hasPendingCommitRef = React__namespace.useRef(false);
769
+ const onValueCommitted = useValueChanged.useStableCallback((nextValue, eventDetails) => {
770
+ hasPendingCommitRef.current = false;
771
+ onValueCommittedProp?.(nextValue, eventDetails);
772
+ });
773
+ const startTickTimeout = useValueChanged.useTimeout();
774
+ const tickInterval = useInterval();
775
+ const intentionalTouchCheckTimeout = useValueChanged.useTimeout();
776
+ const isPressedRef = React__namespace.useRef(false);
777
+ const movesAfterTouchRef = React__namespace.useRef(0);
778
+ const allowInputSyncRef = React__namespace.useRef(true);
779
+ const lastChangedValueRef = React__namespace.useRef(null);
780
+ const unsubscribeFromGlobalContextMenuRef = React__namespace.useRef(() => {});
781
+
782
+ // During SSR, the value is formatted on the server, whose locale may differ from the client's
783
+ // locale. This causes a hydration mismatch, which we manually suppress. This is preferable to
784
+ // rendering an empty input field and then updating it with the formatted value, as the user
785
+ // can still see the value prior to hydration, even if it's not formatted correctly.
786
+ const [inputValue, setInputValue] = React__namespace.useState(() => {
787
+ if (valueProp !== undefined) {
788
+ return getControlledInputValue(value, locale, format);
789
+ }
790
+ return formatNumber(value, locale, format);
791
+ });
792
+ const [inputMode, setInputMode] = React__namespace.useState('numeric');
793
+ const getAllowedNonNumericKeys = useValueChanged.useStableCallback(() => {
794
+ const {
795
+ decimal,
796
+ group,
797
+ currency,
798
+ literal
799
+ } = getNumberLocaleDetails(locale, format);
800
+ const keys = new Set();
801
+ BASE_NON_NUMERIC_SYMBOLS.forEach(symbol => keys.add(symbol));
802
+ if (decimal) {
803
+ keys.add(decimal);
804
+ }
805
+ if (group) {
806
+ keys.add(group);
807
+ if (SPACE_SEPARATOR_RE.test(group)) {
808
+ keys.add(' ');
809
+ }
810
+ }
811
+ const allowPercentSymbols = formatStyle === 'percent' || formatStyle === 'unit' && format?.unit === 'percent';
812
+ const allowPermilleSymbols = formatStyle === 'percent' || formatStyle === 'unit' && format?.unit === 'permille';
813
+ if (allowPercentSymbols) {
814
+ PERCENTAGES.forEach(key => keys.add(key));
815
+ }
816
+ if (allowPermilleSymbols) {
817
+ PERMILLE.forEach(key => keys.add(key));
818
+ }
819
+ if (formatStyle === 'currency' && currency) {
820
+ keys.add(currency);
821
+ }
822
+ if (literal) {
823
+ // Some locales (e.g. de-DE) insert a literal space character between the number
824
+ // and the symbol, so allow those characters to be typed/removed.
825
+ Array.from(literal).forEach(char => keys.add(char));
826
+ if (SPACE_SEPARATOR_RE.test(literal)) {
827
+ keys.add(' ');
828
+ }
829
+ }
830
+
831
+ // Allow plus sign in all cases; minus sign only when negatives are valid
832
+ PLUS_SIGNS_WITH_ASCII.forEach(key => keys.add(key));
833
+ if (minWithDefault < 0) {
834
+ MINUS_SIGNS_WITH_ASCII.forEach(key => keys.add(key));
835
+ }
836
+ return keys;
837
+ });
838
+ const getStepAmount = useValueChanged.useStableCallback(event => {
839
+ if (event?.altKey) {
840
+ return smallStep;
841
+ }
842
+ if (event?.shiftKey) {
843
+ return largeStep;
844
+ }
845
+ return step;
846
+ });
847
+ const setValue = useValueChanged.useStableCallback((unvalidatedValue, details) => {
848
+ const eventWithOptionalKeyState = details.event;
849
+ const dir = details.direction;
850
+ const reason = details.reason;
851
+ // Only allow out-of-range values for direct text entry (native-like behavior).
852
+ // Step-based interactions (keyboard arrows, buttons, wheel, scrub) still clamp to min/max.
853
+ const shouldClampValue = !allowOutOfRange || !(reason === useValueChanged.inputChange || reason === useValueChanged.inputBlur || reason === useValueChanged.inputPaste || reason === useValueChanged.inputClear || reason === useValueChanged.none);
854
+ const validatedValue = toValidatedNumber(unvalidatedValue, {
855
+ step: dir ? getStepAmount(eventWithOptionalKeyState) * dir : undefined,
856
+ format: formatOptionsRef.current,
857
+ minWithDefault,
858
+ maxWithDefault,
859
+ minWithZeroDefault,
860
+ snapOnStep,
861
+ small: eventWithOptionalKeyState?.altKey ?? false,
862
+ clamp: shouldClampValue
863
+ });
864
+
865
+ // Determine whether we should notify about a change even if the numeric value is unchanged.
866
+ // This is needed when the user input is clamped/snapped to the same current value, or when
867
+ // the source value differs but validation normalizes to the existing value.
868
+ const isInputReason = details.reason === useValueChanged.inputChange || details.reason === useValueChanged.inputClear || details.reason === useValueChanged.inputBlur || details.reason === useValueChanged.inputPaste || details.reason === useValueChanged.none;
869
+ const shouldFireChange = validatedValue !== value || isInputReason && (unvalidatedValue !== value || allowInputSyncRef.current === false);
870
+ if (shouldFireChange) {
871
+ lastChangedValueRef.current = validatedValue;
872
+ onValueChangeProp?.(validatedValue, details);
873
+ if (details.isCanceled) {
874
+ return shouldFireChange;
875
+ }
876
+ setValueUnwrapped(validatedValue);
877
+ setDirty(validatedValue !== validityData.initialValue);
878
+ hasPendingCommitRef.current = true;
879
+ }
880
+
881
+ // Keep the visible input in sync immediately when programmatic changes occur
882
+ // (increment/decrement, wheel, etc). During direct typing we don't want
883
+ // to overwrite the user-provided text until blur, so we gate on
884
+ // `allowInputSyncRef`.
885
+ if (allowInputSyncRef.current) {
886
+ setInputValue(formatNumber(validatedValue, locale, format));
887
+ }
888
+
889
+ // Formatting can change even if the numeric value hasn't, so ensure a re-render when needed.
890
+ forceRender();
891
+ return shouldFireChange;
892
+ });
893
+ const incrementValue = useValueChanged.useStableCallback((amount, {
894
+ direction,
895
+ currentValue,
896
+ event,
897
+ reason
898
+ }) => {
899
+ const prevValue = currentValue == null ? valueRef.current : currentValue;
900
+ const nextValue = typeof prevValue === 'number' ? prevValue + amount * direction : Math.max(0, min ?? 0);
901
+ const nativeEvent = event;
902
+ return setValue(nextValue, useValueChanged.createChangeEventDetails(reason, nativeEvent, undefined, {
903
+ direction
904
+ }));
905
+ });
906
+ const stopAutoChange = useValueChanged.useStableCallback(() => {
907
+ intentionalTouchCheckTimeout.clear();
908
+ startTickTimeout.clear();
909
+ tickInterval.clear();
910
+ unsubscribeFromGlobalContextMenuRef.current();
911
+ movesAfterTouchRef.current = 0;
912
+ });
913
+ const startAutoChange = useValueChanged.useStableCallback((isIncrement, triggerEvent) => {
914
+ stopAutoChange();
915
+ if (!inputRef.current) {
916
+ return;
917
+ }
918
+ const win = floatingUi_utils_dom.getWindow(inputRef.current);
919
+ function handleContextMenu(event) {
920
+ event.preventDefault();
921
+ }
922
+
923
+ // A global context menu is necessary to prevent the context menu from appearing when the touch
924
+ // is slightly outside of the element's hit area.
925
+ win.addEventListener('contextmenu', handleContextMenu);
926
+ unsubscribeFromGlobalContextMenuRef.current = () => {
927
+ win.removeEventListener('contextmenu', handleContextMenu);
928
+ };
929
+ win.addEventListener('pointerup', event => {
930
+ isPressedRef.current = false;
931
+ stopAutoChange();
932
+ const committed = lastChangedValueRef.current ?? valueRef.current;
933
+ const commitReason = isIncrement ? useValueChanged.incrementPress : useValueChanged.decrementPress;
934
+ onValueCommitted(committed, useValueChanged.createGenericEventDetails(commitReason, event));
935
+ }, {
936
+ once: true
937
+ });
938
+ function tick() {
939
+ const amount = getStepAmount(triggerEvent) ?? DEFAULT_STEP;
940
+ return incrementValue(amount, {
941
+ direction: isIncrement ? 1 : -1,
942
+ event: triggerEvent,
943
+ reason: isIncrement ? 'increment-press' : 'decrement-press'
944
+ });
945
+ }
946
+ if (!tick()) {
947
+ stopAutoChange();
948
+ return;
949
+ }
950
+ startTickTimeout.start(START_AUTO_CHANGE_DELAY, () => {
951
+ tickInterval.start(CHANGE_VALUE_TICK_DELAY, () => {
952
+ if (!tick()) {
953
+ stopAutoChange();
954
+ }
955
+ });
956
+ });
957
+ });
958
+
959
+ // We need to update the input value when the external `value` prop changes. This ends up acting
960
+ // as a single source of truth to update the input value, bypassing the need to manually set it in
961
+ // each event handler internally in this hook.
962
+ // This is done inside a layout effect as an alternative to the technique to set state during
963
+ // render as we're accessing a ref, which must be inside an effect.
964
+ // https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
965
+ //
966
+ // ESLint is disabled because it needs to run even if the parsed value hasn't changed, since the
967
+ // value still can be formatted differently.
968
+ // eslint-disable-next-line react-hooks/exhaustive-deps
969
+ useValueChanged.useIsoLayoutEffect(function syncFormattedInputValueOnValueChange() {
970
+ // This ensures the value is only updated on blur rather than every keystroke, but still
971
+ // allows the input value to be updated when the value is changed externally.
972
+ if (!allowInputSyncRef.current) {
973
+ return;
974
+ }
975
+ const nextInputValue = valueProp !== undefined ? getControlledInputValue(value, locale, format) : formatNumber(value, locale, format);
976
+ if (nextInputValue !== inputValue) {
977
+ setInputValue(nextInputValue);
978
+ }
979
+ });
980
+ useValueChanged.useIsoLayoutEffect(function setDynamicInputModeForIOS() {
981
+ if (!useValueChanged.isIOS) {
982
+ return;
983
+ }
984
+
985
+ // iOS numeric software keyboard doesn't have a minus key, so we need to use the default
986
+ // keyboard to let the user input a negative number.
987
+ let computedInputMode = 'text';
988
+ if (minWithDefault >= 0) {
989
+ // iOS numeric software keyboard doesn't have a decimal key for "numeric" input mode, but
990
+ // this is better than the "text" input if possible to use.
991
+ computedInputMode = 'decimal';
992
+ }
993
+ setInputMode(computedInputMode);
994
+ }, [minWithDefault, formatStyle]);
995
+ React__namespace.useEffect(() => {
996
+ return () => stopAutoChange();
997
+ }, [stopAutoChange]);
998
+
999
+ // The `onWheel` prop can't be prevented, so we need to use a global event listener.
1000
+ React__namespace.useEffect(function registerElementWheelListener() {
1001
+ const element = inputRef.current;
1002
+ if (disabled || readOnly || !allowWheelScrub || !element) {
1003
+ return undefined;
1004
+ }
1005
+ function handleWheel(event) {
1006
+ if (
1007
+ // Allow pinch-zooming.
1008
+ event.ctrlKey || useValueChanged.ownerDocument(inputRef.current).activeElement !== inputRef.current) {
1009
+ return;
1010
+ }
1011
+
1012
+ // Prevent the default behavior to avoid scrolling the page.
1013
+ event.preventDefault();
1014
+ const amount = getStepAmount(event) ?? DEFAULT_STEP;
1015
+ incrementValue(amount, {
1016
+ direction: event.deltaY > 0 ? -1 : 1,
1017
+ event,
1018
+ reason: 'wheel'
1019
+ });
1020
+ }
1021
+ element.addEventListener('wheel', handleWheel);
1022
+ return () => {
1023
+ element.removeEventListener('wheel', handleWheel);
1024
+ };
1025
+ }, [allowWheelScrub, incrementValue, disabled, readOnly, largeStep, step, getStepAmount]);
1026
+ const state = React__namespace.useMemo(() => ({
1027
+ ...fieldState,
1028
+ disabled,
1029
+ readOnly,
1030
+ required,
1031
+ value,
1032
+ inputValue,
1033
+ scrubbing: isScrubbing
1034
+ }), [fieldState, disabled, readOnly, required, value, inputValue, isScrubbing]);
1035
+ const contextValue = React__namespace.useMemo(() => ({
1036
+ inputRef,
1037
+ inputValue,
1038
+ value,
1039
+ startAutoChange,
1040
+ stopAutoChange,
1041
+ minWithDefault,
1042
+ maxWithDefault,
1043
+ disabled,
1044
+ readOnly,
1045
+ id,
1046
+ setValue,
1047
+ incrementValue,
1048
+ getStepAmount,
1049
+ allowInputSyncRef,
1050
+ formatOptionsRef,
1051
+ valueRef,
1052
+ lastChangedValueRef,
1053
+ hasPendingCommitRef,
1054
+ isPressedRef,
1055
+ intentionalTouchCheckTimeout,
1056
+ movesAfterTouchRef,
1057
+ name,
1058
+ required,
1059
+ invalid,
1060
+ inputMode,
1061
+ getAllowedNonNumericKeys,
1062
+ min,
1063
+ max,
1064
+ setInputValue,
1065
+ locale,
1066
+ isScrubbing,
1067
+ setIsScrubbing,
1068
+ state,
1069
+ onValueCommitted
1070
+ }), [inputRef, inputValue, value, startAutoChange, stopAutoChange, minWithDefault, maxWithDefault, disabled, readOnly, id, setValue, incrementValue, getStepAmount, formatOptionsRef, valueRef, intentionalTouchCheckTimeout, name, required, invalid, inputMode, getAllowedNonNumericKeys, min, max, setInputValue, locale, isScrubbing, state, onValueCommitted]);
1071
+ const element = useRenderElement.useRenderElement('div', componentProps, {
1072
+ ref: forwardedRef,
1073
+ state,
1074
+ props: elementProps,
1075
+ stateAttributesMapping: stateAttributesMapping$1
1076
+ });
1077
+ return /*#__PURE__*/jsxRuntime.jsxs(NumberFieldRootContext.Provider, {
1078
+ value: contextValue,
1079
+ children: [element, /*#__PURE__*/jsxRuntime.jsx("input", {
1080
+ ...validation.getInputValidationProps({
1081
+ onFocus() {
1082
+ inputRef.current?.focus();
1083
+ },
1084
+ onChange(event) {
1085
+ // Workaround for https://github.com/facebook/react/issues/9023
1086
+ if (event.nativeEvent.defaultPrevented) {
1087
+ return;
1088
+ }
1089
+
1090
+ // Handle browser autofill.
1091
+ const nextValue = event.currentTarget.valueAsNumber;
1092
+ const parsedValue = Number.isNaN(nextValue) ? null : nextValue;
1093
+ const details = useValueChanged.createChangeEventDetails(useValueChanged.none, event.nativeEvent);
1094
+ setDirty(parsedValue !== validityData.initialValue);
1095
+ setValue(parsedValue, details);
1096
+ if (shouldValidateOnChange()) {
1097
+ validation.commit(parsedValue);
1098
+ }
1099
+ }
1100
+ }),
1101
+ ref: hiddenInputRef,
1102
+ type: "number",
1103
+ name: name,
1104
+ value: value ?? '',
1105
+ min: min,
1106
+ max: max
1107
+ // stepMismatch validation is broken unless an explicit `min` is added.
1108
+ // See https://github.com/facebook/react/issues/12334.
1109
+ ,
1110
+ step: stepProp,
1111
+ disabled: disabled,
1112
+ required: required,
1113
+ "aria-hidden": true,
1114
+ tabIndex: -1,
1115
+ style: name ? useValueChanged.visuallyHiddenInput : useValueChanged.visuallyHidden
1116
+ })]
1117
+ });
1118
+ });
1119
+ if (process.env.NODE_ENV !== "production") NumberFieldRoot.displayName = "NumberFieldRoot";
1120
+ function getControlledInputValue(value, locale, format) {
1121
+ const explicitPrecision = format?.maximumFractionDigits != null || format?.minimumFractionDigits != null;
1122
+ return explicitPrecision ? formatNumber(value, locale, format) : formatNumberMaxPrecision(value, locale, format);
1123
+ }
1124
+
1125
+ /**
1126
+ * Groups the input with the increment and decrement buttons.
1127
+ * Renders a `<div>` element.
1128
+ *
1129
+ * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
1130
+ */
1131
+ const NumberFieldGroup = /*#__PURE__*/React__namespace.forwardRef(function NumberFieldGroup(componentProps, forwardedRef) {
1132
+ const {
1133
+ render,
1134
+ className,
1135
+ ...elementProps
1136
+ } = componentProps;
1137
+ const {
1138
+ state
1139
+ } = useNumberFieldRootContext();
1140
+ const element = useRenderElement.useRenderElement('div', componentProps, {
1141
+ ref: forwardedRef,
1142
+ state,
1143
+ props: [{
1144
+ role: 'group'
1145
+ }, elementProps],
1146
+ stateAttributesMapping: stateAttributesMapping$1
1147
+ });
1148
+ return element;
1149
+ });
1150
+ if (process.env.NODE_ENV !== "production") NumberFieldGroup.displayName = "NumberFieldGroup";
1151
+
1152
+ // Treat pen as touch-like to avoid forcing the software keyboard on stylus taps.
1153
+ // Linux Chrome may emit "pen" historically for mouse usage due to a bug, but the touch path
1154
+ // still works with minor behavioral differences.
1155
+ function isTouchLikePointerType(pointerType) {
1156
+ return pointerType === 'touch' || pointerType === 'pen';
1157
+ }
1158
+ function useNumberFieldButton(params) {
1159
+ const {
1160
+ allowInputSyncRef,
1161
+ disabled,
1162
+ formatOptionsRef,
1163
+ getStepAmount,
1164
+ id,
1165
+ incrementValue,
1166
+ inputRef,
1167
+ inputValue,
1168
+ intentionalTouchCheckTimeout,
1169
+ isIncrement,
1170
+ isPressedRef,
1171
+ locale,
1172
+ movesAfterTouchRef,
1173
+ readOnly,
1174
+ setValue,
1175
+ startAutoChange,
1176
+ stopAutoChange,
1177
+ valueRef,
1178
+ lastChangedValueRef,
1179
+ onValueCommitted
1180
+ } = params;
1181
+ const incrementDownCoordsRef = React__namespace.useRef({
1182
+ x: 0,
1183
+ y: 0
1184
+ });
1185
+ const isTouchingButtonRef = React__namespace.useRef(false);
1186
+ const ignoreClickRef = React__namespace.useRef(false);
1187
+ const pointerTypeRef = React__namespace.useRef('');
1188
+ const pressReason = isIncrement ? 'increment-press' : 'decrement-press';
1189
+ function commitValue(nativeEvent) {
1190
+ allowInputSyncRef.current = true;
1191
+
1192
+ // The input may be dirty but not yet blurred, so the value won't have been committed.
1193
+ const parsedValue = parseNumber(inputValue, locale, formatOptionsRef.current);
1194
+ if (parsedValue !== null) {
1195
+ // The increment value function needs to know the current input value to increment it
1196
+ // correctly.
1197
+ valueRef.current = parsedValue;
1198
+ setValue(parsedValue, useValueChanged.createChangeEventDetails(pressReason, nativeEvent, undefined, {
1199
+ direction: isIncrement ? 1 : -1
1200
+ }));
1201
+ }
1202
+ }
1203
+ const props = {
1204
+ disabled,
1205
+ 'aria-readonly': readOnly || undefined,
1206
+ 'aria-label': isIncrement ? 'Increase' : 'Decrease',
1207
+ 'aria-controls': id,
1208
+ // Keyboard users shouldn't have access to the buttons, since they can use the input element
1209
+ // to change the value. On the other hand, `aria-hidden` is not applied because touch screen
1210
+ // readers should be able to use the buttons.
1211
+ tabIndex: -1,
1212
+ style: {
1213
+ WebkitUserSelect: 'none',
1214
+ userSelect: 'none'
1215
+ },
1216
+ onTouchStart() {
1217
+ isTouchingButtonRef.current = true;
1218
+ },
1219
+ onTouchEnd() {
1220
+ isTouchingButtonRef.current = false;
1221
+ },
1222
+ onClick(event) {
1223
+ const isDisabled = disabled || readOnly;
1224
+ if (event.defaultPrevented || isDisabled || (
1225
+ // If it's not a keyboard/virtual click, ignore.
1226
+ isTouchLikePointerType(pointerTypeRef.current) ? ignoreClickRef.current : event.detail !== 0)) {
1227
+ return;
1228
+ }
1229
+ commitValue(event.nativeEvent);
1230
+ const amount = getStepAmount(event) ?? DEFAULT_STEP;
1231
+ const prev = valueRef.current;
1232
+ incrementValue(amount, {
1233
+ direction: isIncrement ? 1 : -1,
1234
+ event: event.nativeEvent,
1235
+ reason: pressReason
1236
+ });
1237
+ const committed = lastChangedValueRef.current ?? valueRef.current;
1238
+ if (committed !== prev) {
1239
+ onValueCommitted(committed, useValueChanged.createGenericEventDetails(pressReason, event.nativeEvent));
1240
+ }
1241
+ },
1242
+ onPointerDown(event) {
1243
+ const isMainButton = !event.button || event.button === 0;
1244
+ if (event.defaultPrevented || readOnly || !isMainButton || disabled) {
1245
+ return;
1246
+ }
1247
+ pointerTypeRef.current = event.pointerType;
1248
+ ignoreClickRef.current = false;
1249
+ isPressedRef.current = true;
1250
+ incrementDownCoordsRef.current = {
1251
+ x: event.clientX,
1252
+ y: event.clientY
1253
+ };
1254
+ commitValue(event.nativeEvent);
1255
+ const isTouchPointer = isTouchLikePointerType(event.pointerType);
1256
+ if (!isTouchPointer) {
1257
+ event.preventDefault();
1258
+ inputRef.current?.focus();
1259
+ startAutoChange(isIncrement, event);
1260
+ } else {
1261
+ // We need to check if the pointerdown was intentional, and not the result of a scroll
1262
+ // or pinch-zoom. In that case, we don't want to change the value.
1263
+ intentionalTouchCheckTimeout.start(TOUCH_TIMEOUT, () => {
1264
+ const moves = movesAfterTouchRef.current;
1265
+ movesAfterTouchRef.current = 0;
1266
+ // Only start auto-change if the touch is still pressed (prevents races
1267
+ // with pointerup occurring before the timeout fires on quick taps).
1268
+ const stillPressed = isPressedRef.current;
1269
+ if (stillPressed && moves != null && moves < MAX_POINTER_MOVES_AFTER_TOUCH) {
1270
+ startAutoChange(isIncrement, event);
1271
+ ignoreClickRef.current = true; // synthesized click should be ignored
1272
+ } else {
1273
+ // No auto-change (simple tap or scroll gesture), allow the click handler
1274
+ // to perform a single increment and commit.
1275
+ ignoreClickRef.current = false;
1276
+ stopAutoChange();
1277
+ }
1278
+ });
1279
+ }
1280
+ },
1281
+ onPointerUp(event) {
1282
+ // Ensure we mark the press as released for touch flows even if auto-change never started,
1283
+ // so the delayed auto-change check won’t start after a quick tap.
1284
+ if (isTouchLikePointerType(event.pointerType)) {
1285
+ isPressedRef.current = false;
1286
+ }
1287
+ },
1288
+ onPointerMove(event) {
1289
+ const isDisabled = disabled || readOnly;
1290
+ if (isDisabled || !isTouchLikePointerType(event.pointerType) || !isPressedRef.current) {
1291
+ return;
1292
+ }
1293
+ if (movesAfterTouchRef.current != null) {
1294
+ movesAfterTouchRef.current += 1;
1295
+ }
1296
+ const {
1297
+ x,
1298
+ y
1299
+ } = incrementDownCoordsRef.current;
1300
+ const dx = x - event.clientX;
1301
+ const dy = y - event.clientY;
1302
+
1303
+ // An alternative to this technique is to detect when the NumberField's parent container
1304
+ // has been scrolled
1305
+ if (dx ** 2 + dy ** 2 > SCROLLING_POINTER_MOVE_DISTANCE ** 2) {
1306
+ stopAutoChange();
1307
+ }
1308
+ },
1309
+ onMouseEnter(event) {
1310
+ const isDisabled = disabled || readOnly;
1311
+ if (event.defaultPrevented || isDisabled || !isPressedRef.current || isTouchingButtonRef.current || isTouchLikePointerType(pointerTypeRef.current)) {
1312
+ return;
1313
+ }
1314
+ startAutoChange(isIncrement, event);
1315
+ },
1316
+ onMouseLeave() {
1317
+ if (isTouchingButtonRef.current) {
1318
+ return;
1319
+ }
1320
+ stopAutoChange();
1321
+ },
1322
+ onMouseUp() {
1323
+ if (isTouchingButtonRef.current) {
1324
+ return;
1325
+ }
1326
+ stopAutoChange();
1327
+ }
1328
+ };
1329
+ return props;
1330
+ }
1331
+
1332
+ /**
1333
+ * A stepper button that increases the field value when clicked.
1334
+ * Renders an `<button>` element.
1335
+ *
1336
+ * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
1337
+ */
1338
+ const NumberFieldIncrement = /*#__PURE__*/React__namespace.forwardRef(function NumberFieldIncrement(componentProps, forwardedRef) {
1339
+ const {
1340
+ render,
1341
+ className,
1342
+ disabled: disabledProp = false,
1343
+ nativeButton = true,
1344
+ ...elementProps
1345
+ } = componentProps;
1346
+ const {
1347
+ allowInputSyncRef,
1348
+ disabled: contextDisabled,
1349
+ formatOptionsRef,
1350
+ getStepAmount,
1351
+ id,
1352
+ incrementValue,
1353
+ inputRef,
1354
+ inputValue,
1355
+ intentionalTouchCheckTimeout,
1356
+ isPressedRef,
1357
+ locale,
1358
+ maxWithDefault,
1359
+ movesAfterTouchRef,
1360
+ readOnly,
1361
+ setValue,
1362
+ startAutoChange,
1363
+ state,
1364
+ stopAutoChange,
1365
+ value,
1366
+ valueRef,
1367
+ lastChangedValueRef,
1368
+ onValueCommitted
1369
+ } = useNumberFieldRootContext();
1370
+ const isMax = value != null && value >= maxWithDefault;
1371
+ const disabled = disabledProp || contextDisabled || isMax;
1372
+ const props = useNumberFieldButton({
1373
+ isIncrement: true,
1374
+ inputRef,
1375
+ startAutoChange,
1376
+ stopAutoChange,
1377
+ inputValue,
1378
+ disabled,
1379
+ readOnly,
1380
+ id,
1381
+ setValue,
1382
+ getStepAmount,
1383
+ incrementValue,
1384
+ allowInputSyncRef,
1385
+ formatOptionsRef,
1386
+ valueRef,
1387
+ isPressedRef,
1388
+ intentionalTouchCheckTimeout,
1389
+ movesAfterTouchRef,
1390
+ locale,
1391
+ lastChangedValueRef,
1392
+ onValueCommitted
1393
+ });
1394
+ const {
1395
+ getButtonProps,
1396
+ buttonRef
1397
+ } = useBaseUiId.useButton({
1398
+ disabled,
1399
+ native: nativeButton,
1400
+ focusableWhenDisabled: true
1401
+ });
1402
+ const buttonState = React__namespace.useMemo(() => ({
1403
+ ...state,
1404
+ disabled
1405
+ }), [state, disabled]);
1406
+ const element = useRenderElement.useRenderElement('button', componentProps, {
1407
+ ref: [forwardedRef, buttonRef],
1408
+ state: buttonState,
1409
+ props: [props, elementProps, getButtonProps],
1410
+ stateAttributesMapping: stateAttributesMapping$1
1411
+ });
1412
+ return element;
1413
+ });
1414
+ if (process.env.NODE_ENV !== "production") NumberFieldIncrement.displayName = "NumberFieldIncrement";
1415
+
1416
+ /**
1417
+ * A stepper button that decreases the field value when clicked.
1418
+ * Renders an `<button>` element.
1419
+ *
1420
+ * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
1421
+ */
1422
+ const NumberFieldDecrement = /*#__PURE__*/React__namespace.forwardRef(function NumberFieldDecrement(componentProps, forwardedRef) {
1423
+ const {
1424
+ render,
1425
+ className,
1426
+ disabled: disabledProp = false,
1427
+ nativeButton = true,
1428
+ ...elementProps
1429
+ } = componentProps;
1430
+ const {
1431
+ allowInputSyncRef,
1432
+ disabled: contextDisabled,
1433
+ formatOptionsRef,
1434
+ getStepAmount,
1435
+ id,
1436
+ incrementValue,
1437
+ inputRef,
1438
+ inputValue,
1439
+ intentionalTouchCheckTimeout,
1440
+ isPressedRef,
1441
+ minWithDefault,
1442
+ movesAfterTouchRef,
1443
+ readOnly,
1444
+ setValue,
1445
+ startAutoChange,
1446
+ state,
1447
+ stopAutoChange,
1448
+ value,
1449
+ valueRef,
1450
+ locale,
1451
+ lastChangedValueRef,
1452
+ onValueCommitted
1453
+ } = useNumberFieldRootContext();
1454
+ const isMin = value != null && value <= minWithDefault;
1455
+ const disabled = disabledProp || contextDisabled || isMin;
1456
+ const props = useNumberFieldButton({
1457
+ isIncrement: false,
1458
+ inputRef,
1459
+ startAutoChange,
1460
+ stopAutoChange,
1461
+ inputValue,
1462
+ disabled,
1463
+ readOnly,
1464
+ id,
1465
+ setValue,
1466
+ getStepAmount,
1467
+ incrementValue,
1468
+ allowInputSyncRef,
1469
+ formatOptionsRef,
1470
+ valueRef,
1471
+ isPressedRef,
1472
+ intentionalTouchCheckTimeout,
1473
+ movesAfterTouchRef,
1474
+ locale,
1475
+ lastChangedValueRef,
1476
+ onValueCommitted
1477
+ });
1478
+ const {
1479
+ getButtonProps,
1480
+ buttonRef
1481
+ } = useBaseUiId.useButton({
1482
+ disabled,
1483
+ native: nativeButton,
1484
+ focusableWhenDisabled: true
1485
+ });
1486
+ const buttonState = React__namespace.useMemo(() => ({
1487
+ ...state,
1488
+ disabled
1489
+ }), [state, disabled]);
1490
+ const element = useRenderElement.useRenderElement('button', componentProps, {
1491
+ ref: [forwardedRef, buttonRef],
1492
+ state: buttonState,
1493
+ props: [props, elementProps, getButtonProps],
1494
+ stateAttributesMapping: stateAttributesMapping$1
1495
+ });
1496
+ return element;
1497
+ });
1498
+ if (process.env.NODE_ENV !== "production") NumberFieldDecrement.displayName = "NumberFieldDecrement";
1499
+
1500
+ const stateAttributesMapping = {
1501
+ ...fieldValidityMapping,
1502
+ ...stateAttributesMapping$1
1503
+ };
1504
+ const NAVIGATE_KEYS = new Set(['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Escape']);
1505
+
1506
+ /**
1507
+ * The native input control in the number field.
1508
+ * Renders an `<input>` element.
1509
+ *
1510
+ * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
1511
+ */
1512
+ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function NumberFieldInput(componentProps, forwardedRef) {
1513
+ const {
1514
+ render,
1515
+ className,
1516
+ ...elementProps
1517
+ } = componentProps;
1518
+ const {
1519
+ allowInputSyncRef,
1520
+ disabled,
1521
+ formatOptionsRef,
1522
+ getAllowedNonNumericKeys,
1523
+ getStepAmount,
1524
+ id,
1525
+ incrementValue,
1526
+ inputMode,
1527
+ inputValue,
1528
+ max,
1529
+ min,
1530
+ name,
1531
+ readOnly,
1532
+ required,
1533
+ setValue,
1534
+ state,
1535
+ setInputValue,
1536
+ locale,
1537
+ inputRef,
1538
+ value,
1539
+ onValueCommitted,
1540
+ lastChangedValueRef,
1541
+ hasPendingCommitRef,
1542
+ valueRef
1543
+ } = useNumberFieldRootContext();
1544
+ const {
1545
+ clearErrors
1546
+ } = useFormContext();
1547
+ const {
1548
+ validationMode,
1549
+ setTouched,
1550
+ setFocused,
1551
+ invalid,
1552
+ shouldValidateOnChange,
1553
+ validation
1554
+ } = useFieldRootContext();
1555
+ const {
1556
+ labelId
1557
+ } = useLabelableContext();
1558
+ const hasTouchedInputRef = React__namespace.useRef(false);
1559
+ const blockRevalidationRef = React__namespace.useRef(false);
1560
+ useField({
1561
+ id,
1562
+ commit: validation.commit,
1563
+ value,
1564
+ controlRef: inputRef,
1565
+ name,
1566
+ getValue: () => value ?? null
1567
+ });
1568
+ useValueChanged.useValueChanged(value, previousValue => {
1569
+ const validateOnChange = shouldValidateOnChange();
1570
+ clearErrors(name);
1571
+ if (validateOnChange) {
1572
+ validation.commit(value);
1573
+ }
1574
+ if (previousValue === value || validateOnChange) {
1575
+ return;
1576
+ }
1577
+ if (blockRevalidationRef.current) {
1578
+ blockRevalidationRef.current = false;
1579
+ return;
1580
+ }
1581
+ validation.commit(value, true);
1582
+ });
1583
+ const inputProps = {
1584
+ id,
1585
+ required,
1586
+ disabled,
1587
+ readOnly,
1588
+ inputMode,
1589
+ value: inputValue,
1590
+ type: 'text',
1591
+ autoComplete: 'off',
1592
+ autoCorrect: 'off',
1593
+ spellCheck: 'false',
1594
+ 'aria-roledescription': 'Number field',
1595
+ 'aria-invalid': invalid || undefined,
1596
+ 'aria-labelledby': labelId,
1597
+ // If the server's locale does not match the client's locale, the formatting may not match,
1598
+ // causing a hydration mismatch.
1599
+ suppressHydrationWarning: true,
1600
+ onFocus(event) {
1601
+ if (event.defaultPrevented || readOnly || disabled) {
1602
+ return;
1603
+ }
1604
+ setFocused(true);
1605
+ if (hasTouchedInputRef.current) {
1606
+ return;
1607
+ }
1608
+ hasTouchedInputRef.current = true;
1609
+
1610
+ // Browsers set selection at the start of the input field by default. We want to set it at
1611
+ // the end for the first focus.
1612
+ const target = event.currentTarget;
1613
+ const length = target.value.length;
1614
+ target.setSelectionRange(length, length);
1615
+ },
1616
+ onBlur(event) {
1617
+ if (event.defaultPrevented || readOnly || disabled) {
1618
+ return;
1619
+ }
1620
+ setTouched(true);
1621
+ setFocused(false);
1622
+ const hadManualInput = !allowInputSyncRef.current;
1623
+ const hadPendingProgrammaticChange = hasPendingCommitRef.current;
1624
+ allowInputSyncRef.current = true;
1625
+ if (inputValue.trim() === '') {
1626
+ setValue(null, useValueChanged.createChangeEventDetails(useValueChanged.inputClear, event.nativeEvent));
1627
+ if (validationMode === 'onBlur') {
1628
+ validation.commit(null);
1629
+ }
1630
+ onValueCommitted(null, useValueChanged.createGenericEventDetails(useValueChanged.inputClear, event.nativeEvent));
1631
+ return;
1632
+ }
1633
+ const formatOptions = formatOptionsRef.current;
1634
+ const parsedValue = parseNumber(inputValue, locale, formatOptions);
1635
+ if (parsedValue === null) {
1636
+ return;
1637
+ }
1638
+
1639
+ // If an explicit precision is requested, round the committed numeric value.
1640
+ const hasExplicitPrecision = formatOptions?.maximumFractionDigits != null || formatOptions?.minimumFractionDigits != null;
1641
+ const maxFrac = formatOptions?.maximumFractionDigits;
1642
+ const committed = hasExplicitPrecision && typeof maxFrac === 'number' ? Number(parsedValue.toFixed(maxFrac)) : parsedValue;
1643
+ const nextEventDetails = useValueChanged.createGenericEventDetails(useValueChanged.inputBlur, event.nativeEvent);
1644
+ const shouldUpdateValue = value !== committed;
1645
+ const shouldCommit = hadManualInput || shouldUpdateValue || hadPendingProgrammaticChange;
1646
+ if (validationMode === 'onBlur') {
1647
+ validation.commit(committed);
1648
+ }
1649
+ if (shouldUpdateValue) {
1650
+ blockRevalidationRef.current = true;
1651
+ setValue(committed, useValueChanged.createChangeEventDetails(useValueChanged.inputBlur, event.nativeEvent));
1652
+ }
1653
+ if (shouldCommit) {
1654
+ onValueCommitted(committed, nextEventDetails);
1655
+ }
1656
+
1657
+ // Normalize only the displayed text
1658
+ const canonicalText = formatNumber(committed, locale, formatOptions);
1659
+ const maxPrecisionText = formatNumberMaxPrecision(parsedValue, locale, formatOptions);
1660
+ const shouldPreserveFullPrecision = !hasExplicitPrecision && parsedValue === value && inputValue === maxPrecisionText;
1661
+ if (!shouldPreserveFullPrecision && inputValue !== canonicalText) {
1662
+ setInputValue(canonicalText);
1663
+ }
1664
+ },
1665
+ onChange(event) {
1666
+ // Workaround for https://github.com/facebook/react/issues/9023
1667
+ if (event.nativeEvent.defaultPrevented) {
1668
+ return;
1669
+ }
1670
+ allowInputSyncRef.current = false;
1671
+ const targetValue = event.target.value;
1672
+ if (targetValue.trim() === '') {
1673
+ setInputValue(targetValue);
1674
+ setValue(null, useValueChanged.createChangeEventDetails(useValueChanged.inputClear, event.nativeEvent));
1675
+ return;
1676
+ }
1677
+
1678
+ // Update the input text immediately and only fire onValueChange if the typed value is
1679
+ // currently parseable into a number. This preserves good UX for IME
1680
+ // composition/partial input while still providing live numeric updates when possible.
1681
+ const allowedNonNumericKeys = getAllowedNonNumericKeys();
1682
+ const isValidCharacterString = Array.from(targetValue).every(ch => {
1683
+ const isAsciiDigit = ch >= '0' && ch <= '9';
1684
+ const isArabicNumeral = ARABIC_DETECT_RE.test(ch);
1685
+ const isHanNumeral = HAN_DETECT_RE.test(ch);
1686
+ const isPersianNumeral = PERSIAN_DETECT_RE.test(ch);
1687
+ const isFullwidthNumeral = FULLWIDTH_DETECT_RE.test(ch);
1688
+ const isMinus = ANY_MINUS_DETECT_RE.test(ch);
1689
+ return isAsciiDigit || isArabicNumeral || isHanNumeral || isPersianNumeral || isFullwidthNumeral || isMinus || allowedNonNumericKeys.has(ch);
1690
+ });
1691
+ if (!isValidCharacterString) {
1692
+ return;
1693
+ }
1694
+ const parsedValue = parseNumber(targetValue, locale, formatOptionsRef.current);
1695
+ setInputValue(targetValue);
1696
+ if (parsedValue !== null) {
1697
+ setValue(parsedValue, useValueChanged.createChangeEventDetails(useValueChanged.inputChange, event.nativeEvent));
1698
+ }
1699
+ },
1700
+ onKeyDown(event) {
1701
+ if (event.defaultPrevented || readOnly || disabled) {
1702
+ return;
1703
+ }
1704
+ const nativeEvent = event.nativeEvent;
1705
+ allowInputSyncRef.current = true;
1706
+ const allowedNonNumericKeys = getAllowedNonNumericKeys();
1707
+ let isAllowedNonNumericKey = allowedNonNumericKeys.has(event.key);
1708
+ const {
1709
+ decimal,
1710
+ currency,
1711
+ percentSign
1712
+ } = getNumberLocaleDetails(locale, formatOptionsRef.current);
1713
+ const selectionStart = event.currentTarget.selectionStart;
1714
+ const selectionEnd = event.currentTarget.selectionEnd;
1715
+ const isAllSelected = selectionStart === 0 && selectionEnd === inputValue.length;
1716
+
1717
+ // Normalize handling of plus/minus signs via precomputed regexes
1718
+ const selectionContainsIndex = index => selectionStart != null && selectionEnd != null && index >= selectionStart && index < selectionEnd;
1719
+ if (ANY_MINUS_DETECT_RE.test(event.key) && Array.from(allowedNonNumericKeys).some(k => ANY_MINUS_DETECT_RE.test(k || ''))) {
1720
+ // Only allow one sign unless replacing the existing one or all text is selected
1721
+ const existingIndex = inputValue.search(ANY_MINUS_RE);
1722
+ const isReplacingExisting = existingIndex != null && existingIndex !== -1 && selectionContainsIndex(existingIndex);
1723
+ isAllowedNonNumericKey = !(ANY_MINUS_DETECT_RE.test(inputValue) || ANY_PLUS_DETECT_RE.test(inputValue)) || isAllSelected || isReplacingExisting;
1724
+ }
1725
+ if (ANY_PLUS_DETECT_RE.test(event.key) && Array.from(allowedNonNumericKeys).some(k => ANY_PLUS_DETECT_RE.test(k || ''))) {
1726
+ const existingIndex = inputValue.search(ANY_PLUS_RE);
1727
+ const isReplacingExisting = existingIndex != null && existingIndex !== -1 && selectionContainsIndex(existingIndex);
1728
+ isAllowedNonNumericKey = !(ANY_MINUS_DETECT_RE.test(inputValue) || ANY_PLUS_DETECT_RE.test(inputValue)) || isAllSelected || isReplacingExisting;
1729
+ }
1730
+
1731
+ // Only allow one of each symbol.
1732
+ [decimal, currency, percentSign].forEach(symbol => {
1733
+ if (event.key === symbol) {
1734
+ const symbolIndex = inputValue.indexOf(symbol);
1735
+ const isSymbolHighlighted = selectionContainsIndex(symbolIndex);
1736
+ isAllowedNonNumericKey = !inputValue.includes(symbol) || isAllSelected || isSymbolHighlighted;
1737
+ }
1738
+ });
1739
+ const isAsciiDigit = event.key >= '0' && event.key <= '9';
1740
+ const isArabicNumeral = ARABIC_DETECT_RE.test(event.key);
1741
+ const isHanNumeral = HAN_DETECT_RE.test(event.key);
1742
+ const isFullwidthNumeral = FULLWIDTH_DETECT_RE.test(event.key);
1743
+ const isNavigateKey = NAVIGATE_KEYS.has(event.key);
1744
+ if (
1745
+ // Allow composition events (e.g., pinyin)
1746
+ // event.nativeEvent.isComposing does not work in Safari:
1747
+ // https://bugs.webkit.org/show_bug.cgi?id=165004
1748
+ event.which === 229 || event.altKey || event.ctrlKey || event.metaKey || isAllowedNonNumericKey || isAsciiDigit || isArabicNumeral || isFullwidthNumeral || isHanNumeral || isNavigateKey) {
1749
+ return;
1750
+ }
1751
+
1752
+ // We need to commit the number at this point if the input hasn't been blurred.
1753
+ const parsedValue = parseNumber(inputValue, locale, formatOptionsRef.current);
1754
+ const amount = getStepAmount(event) ?? DEFAULT_STEP;
1755
+
1756
+ // Prevent insertion of text or caret from moving.
1757
+ useValueChanged.stopEvent(event);
1758
+ const commitDetails = useValueChanged.createGenericEventDetails(useValueChanged.keyboard, nativeEvent);
1759
+ if (event.key === 'ArrowUp') {
1760
+ incrementValue(amount, {
1761
+ direction: 1,
1762
+ currentValue: parsedValue,
1763
+ event: nativeEvent,
1764
+ reason: useValueChanged.keyboard
1765
+ });
1766
+ onValueCommitted(lastChangedValueRef.current ?? valueRef.current, commitDetails);
1767
+ } else if (event.key === 'ArrowDown') {
1768
+ incrementValue(amount, {
1769
+ direction: -1,
1770
+ currentValue: parsedValue,
1771
+ event: nativeEvent,
1772
+ reason: useValueChanged.keyboard
1773
+ });
1774
+ onValueCommitted(lastChangedValueRef.current ?? valueRef.current, commitDetails);
1775
+ } else if (event.key === 'Home' && min != null) {
1776
+ setValue(min, useValueChanged.createChangeEventDetails(useValueChanged.keyboard, nativeEvent));
1777
+ onValueCommitted(lastChangedValueRef.current ?? valueRef.current, commitDetails);
1778
+ } else if (event.key === 'End' && max != null) {
1779
+ setValue(max, useValueChanged.createChangeEventDetails(useValueChanged.keyboard, nativeEvent));
1780
+ onValueCommitted(lastChangedValueRef.current ?? valueRef.current, commitDetails);
1781
+ }
1782
+ },
1783
+ onPaste(event) {
1784
+ if (event.defaultPrevented || readOnly || disabled) {
1785
+ return;
1786
+ }
1787
+
1788
+ // Prevent `onChange` from being called.
1789
+ event.preventDefault();
1790
+ const clipboardData = event.clipboardData || window.Clipboard;
1791
+ const pastedData = clipboardData.getData('text/plain');
1792
+ const parsedValue = parseNumber(pastedData, locale, formatOptionsRef.current);
1793
+ if (parsedValue !== null) {
1794
+ allowInputSyncRef.current = false;
1795
+ setValue(parsedValue, useValueChanged.createChangeEventDetails(useValueChanged.inputPaste, event.nativeEvent));
1796
+ setInputValue(pastedData);
1797
+ }
1798
+ }
1799
+ };
1800
+ const element = useRenderElement.useRenderElement('input', componentProps, {
1801
+ ref: [forwardedRef, inputRef],
1802
+ state,
1803
+ props: [inputProps, validation.getValidationProps(), elementProps],
1804
+ stateAttributesMapping
1805
+ });
1806
+ return element;
1807
+ });
1808
+ if (process.env.NODE_ENV !== "production") NumberFieldInput.displayName = "NumberFieldInput";
1809
+
1810
+ exports.DEFAULT_FIELD_STATE_ATTRIBUTES = DEFAULT_FIELD_STATE_ATTRIBUTES;
1811
+ exports.DEFAULT_STEP = DEFAULT_STEP;
1812
+ exports.DEFAULT_VALIDITY_STATE = DEFAULT_VALIDITY_STATE;
1813
+ exports.FieldRootContext = FieldRootContext;
1814
+ exports.LabelableContext = LabelableContext;
1815
+ exports.NumberFieldDecrement = NumberFieldDecrement;
1816
+ exports.NumberFieldGroup = NumberFieldGroup;
1817
+ exports.NumberFieldIncrement = NumberFieldIncrement;
1818
+ exports.NumberFieldInput = NumberFieldInput;
1819
+ exports.NumberFieldRoot = NumberFieldRoot;
1820
+ exports.fieldValidityMapping = fieldValidityMapping;
1821
+ exports.getCombinedFieldValidityData = getCombinedFieldValidityData;
1822
+ exports.stateAttributesMapping = stateAttributesMapping$1;
1823
+ exports.useField = useField;
1824
+ exports.useFieldRootContext = useFieldRootContext;
1825
+ exports.useFormContext = useFormContext;
1826
+ exports.useLabelableContext = useLabelableContext;
1827
+ exports.useLabelableId = useLabelableId;
1828
+ exports.useNumberFieldRootContext = useNumberFieldRootContext;