@jobber/components 9.6.1 → 9.6.2

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 (141) hide show
  1. package/dist/Autocomplete/index.cjs +2 -0
  2. package/dist/Autocomplete/index.mjs +2 -0
  3. package/dist/Autocomplete-cjs.js +5 -4
  4. package/dist/Autocomplete-es.js +2 -1
  5. package/dist/Card/index.cjs +2 -1
  6. package/dist/Card/index.mjs +2 -1
  7. package/dist/Chip/index.cjs +2 -0
  8. package/dist/Chip/index.mjs +2 -0
  9. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +2 -0
  10. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +2 -0
  11. package/dist/Chips/InternalChipDismissible/index.cjs +2 -0
  12. package/dist/Chips/InternalChipDismissible/index.mjs +2 -0
  13. package/dist/Chips/index.cjs +2 -0
  14. package/dist/Chips/index.mjs +2 -0
  15. package/dist/Combobox/index.cjs +4 -2
  16. package/dist/Combobox/index.mjs +4 -2
  17. package/dist/{ComboboxSeparator-cjs.js → ComboboxChipRemove-cjs.js} +455 -501
  18. package/dist/{ComboboxSeparator-es.js → ComboboxChipRemove-es.js} +456 -500
  19. package/dist/ComboboxPrimitive-cjs.js +30 -29
  20. package/dist/ComboboxPrimitive-es.js +3 -2
  21. package/dist/ConfirmationModal/index.cjs +2 -0
  22. package/dist/ConfirmationModal/index.mjs +2 -0
  23. package/dist/DataDump/index.cjs +2 -1
  24. package/dist/DataDump/index.mjs +2 -1
  25. package/dist/DataList/components/DataListActions/index.cjs +2 -0
  26. package/dist/DataList/components/DataListActions/index.mjs +2 -0
  27. package/dist/DataList/components/DataListBulkActions/index.cjs +2 -0
  28. package/dist/DataList/components/DataListBulkActions/index.mjs +2 -0
  29. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +2 -0
  30. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +2 -0
  31. package/dist/DataList/components/DataListFilters/index.cjs +2 -0
  32. package/dist/DataList/components/DataListFilters/index.mjs +2 -0
  33. package/dist/DataList/components/DataListHeader/index.cjs +2 -0
  34. package/dist/DataList/components/DataListHeader/index.mjs +2 -0
  35. package/dist/DataList/components/DataListItem/index.cjs +2 -0
  36. package/dist/DataList/components/DataListItem/index.mjs +2 -0
  37. package/dist/DataList/components/DataListItemActions/index.cjs +2 -0
  38. package/dist/DataList/components/DataListItemActions/index.mjs +2 -0
  39. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +2 -0
  40. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +2 -0
  41. package/dist/DataList/components/DataListItems/index.cjs +2 -0
  42. package/dist/DataList/components/DataListItems/index.mjs +2 -0
  43. package/dist/DataList/components/DataListLayout/index.cjs +2 -0
  44. package/dist/DataList/components/DataListLayout/index.mjs +2 -0
  45. package/dist/DataList/components/DataListLayoutActions/index.cjs +2 -0
  46. package/dist/DataList/components/DataListLayoutActions/index.mjs +2 -0
  47. package/dist/DataList/index.cjs +2 -0
  48. package/dist/DataList/index.mjs +2 -0
  49. package/dist/DatePicker/index.cjs +2 -0
  50. package/dist/DatePicker/index.mjs +2 -0
  51. package/dist/DatePicker-cjs.js +5 -4
  52. package/dist/DatePicker-es.js +2 -1
  53. package/dist/Dialog/index.cjs +2 -1
  54. package/dist/Dialog/index.mjs +2 -1
  55. package/dist/DrawerDescription-cjs.js +99 -268
  56. package/dist/DrawerDescription-es.js +103 -270
  57. package/dist/DrawerTitle-cjs.js +6 -2
  58. package/dist/DrawerTitle-es.js +7 -3
  59. package/dist/FieldDescription-cjs.js +181 -184
  60. package/dist/FieldDescription-es.js +184 -187
  61. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +2 -0
  62. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +2 -0
  63. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +2 -0
  64. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +2 -0
  65. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +2 -0
  66. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +2 -0
  67. package/dist/FilterPicker/index.cjs +2 -0
  68. package/dist/FilterPicker/index.mjs +2 -0
  69. package/dist/FilterPickerContent-cjs.js +5 -4
  70. package/dist/FilterPickerContent-es.js +2 -1
  71. package/dist/FormatFile/index.cjs +2 -0
  72. package/dist/FormatFile/index.mjs +2 -0
  73. package/dist/Gallery/index.cjs +2 -0
  74. package/dist/Gallery/index.mjs +2 -0
  75. package/dist/InputDate/index.cjs +2 -0
  76. package/dist/InputDate/index.mjs +2 -0
  77. package/dist/InputNumber/index.cjs +2 -1
  78. package/dist/InputNumber/index.mjs +2 -1
  79. package/dist/LightBox/index.cjs +2 -0
  80. package/dist/LightBox/index.mjs +2 -0
  81. package/dist/Menu/index.cjs +2 -1
  82. package/dist/Menu/index.mjs +2 -1
  83. package/dist/Menu-cjs.js +5 -4
  84. package/dist/Menu-es.js +2 -1
  85. package/dist/MenuSubmenuTrigger-cjs.js +164 -206
  86. package/dist/MenuSubmenuTrigger-es.js +152 -194
  87. package/dist/Modal/index.cjs +2 -0
  88. package/dist/Modal/index.mjs +3 -1
  89. package/dist/NumberFieldInput-cjs.js +308 -187
  90. package/dist/NumberFieldInput-es.js +308 -187
  91. package/dist/Page/index.cjs +2 -1
  92. package/dist/Page/index.mjs +2 -1
  93. package/dist/Popover/index.cjs +2 -0
  94. package/dist/Popover/index.mjs +2 -0
  95. package/dist/Popover-cjs.js +8 -7
  96. package/dist/Popover-es.js +2 -1
  97. package/dist/ScrollAreaViewport-cjs.js +1093 -747
  98. package/dist/ScrollAreaViewport-es.js +1081 -734
  99. package/dist/Select/index.cjs +2 -1
  100. package/dist/Select/index.mjs +2 -1
  101. package/dist/SelectGroupLabel-cjs.js +306 -207
  102. package/dist/SelectGroupLabel-es.js +302 -203
  103. package/dist/Tooltip/index.cjs +2 -0
  104. package/dist/Tooltip/index.mjs +2 -0
  105. package/dist/Tooltip-cjs.js +7 -6
  106. package/dist/Tooltip-es.js +2 -1
  107. package/dist/dialogReturnFocus-cjs.js +1 -1
  108. package/dist/dialogReturnFocus-es.js +2 -2
  109. package/dist/floating-ui.react-cjs.js +192 -2564
  110. package/dist/floating-ui.react-dom-cjs.js +2103 -0
  111. package/dist/floating-ui.react-dom-es.js +2072 -0
  112. package/dist/floating-ui.react-es.js +156 -2519
  113. package/dist/floating-ui.utils-cjs.js +342 -0
  114. package/dist/floating-ui.utils-es.js +300 -0
  115. package/dist/index.cjs +4 -3
  116. package/dist/index.mjs +4 -3
  117. package/dist/primitives/BottomSheet/index.cjs +1 -1
  118. package/dist/primitives/BottomSheet/index.mjs +1 -1
  119. package/dist/primitives/ComboboxPrimitive/index.cjs +4 -2
  120. package/dist/primitives/ComboboxPrimitive/index.mjs +4 -2
  121. package/dist/primitives/SelectPrimitive/index.cjs +2 -0
  122. package/dist/primitives/SelectPrimitive/index.mjs +2 -0
  123. package/dist/primitives/index.cjs +3 -2
  124. package/dist/primitives/index.mjs +3 -2
  125. package/dist/scrollEdges-cjs.js +11 -1
  126. package/dist/scrollEdges-es.js +11 -1
  127. package/dist/unstyledPrimitives/index.cjs +478 -720
  128. package/dist/unstyledPrimitives/index.mjs +421 -663
  129. package/dist/useCompositeListItem-cjs.js +361 -2108
  130. package/dist/useCompositeListItem-es.js +331 -2076
  131. package/dist/useLabel-cjs.js +3 -2
  132. package/dist/useLabel-es.js +3 -2
  133. package/dist/useRenderElement-cjs.js +7 -9
  134. package/dist/useRenderElement-es.js +7 -9
  135. package/dist/useScrollLock-cjs.js +344 -819
  136. package/dist/useScrollLock-es.js +296 -732
  137. package/dist/useScrollToActive-cjs.js +5 -4
  138. package/dist/useScrollToActive-es.js +2 -1
  139. package/package.json +3 -3
  140. package/dist/scrollable-cjs.js +0 -58
  141. package/dist/scrollable-es.js +0 -54
@@ -1,8 +1,9 @@
1
1
  import * as React from 'react';
2
- import { v as useControlled, w as useValueAsRef, c as useIsoLayoutEffect, d as useStableCallback, x as ios, y as addEventListener, h as createChangeEventDetails, z as none, B as visuallyHiddenInput, C as visuallyHidden, D as activeElement, o as ownerDocument, E as wheel, G as createGenericEventDetails, H as useOnMount, T as Timeout, q as useTimeout, I as getWindow, f as useButton, J as incrementPress, K as decrementPress, L as useValueChanged, M as inputClear, N as inputBlur, O as inputChange, P as keyboard, S as SafeReact, Q as inputPaste } from './useScrollLock-es.js';
2
+ import { n as useControlled, q as useValueAsRef, a as useIsoLayoutEffect, b as useStableCallback, r as ios, s as addEventListener, d as createChangeEventDetails, v as none, w as visuallyHiddenInput, x as visuallyHidden, y as inputChange, z as inputClear, A as inputBlur, B as inputPaste, D as activeElement, o as ownerDocument, E as createGenericEventDetails, G as wheel, H as useOnMount, T as Timeout, k as useTimeout, I as incrementPress, J as decrementPress, c as useButton, K as useValueChanged, L as stopEvent, M as keyboard, S as SafeReact } from './useScrollLock-es.js';
3
3
  import { f as formatErrorMessage, b as useMergedRefs, u as useRenderElement, a as useRefWithInit, N as NOOP, w as warn } from './useRenderElement-es.js';
4
4
  import { f as fieldValidityMapping, b as useFieldRootContext, a as useFormContext, c as useLabelableId, u as useLabelableContext, d as useRegisterFieldControl } from './useCompositeListItem-es.js';
5
5
  import { jsxs, jsx } from 'react/jsx-runtime';
6
+ import { K as getWindow } from './floating-ui.utils-es.js';
6
7
  import { s as stringifyLocale } from './stringifyLocale-es.js';
7
8
  import { c as clamp } from './scrollEdges-es.js';
8
9
 
@@ -53,112 +54,121 @@ const stateAttributesMapping = {
53
54
  ...fieldValidityMapping
54
55
  };
55
56
 
56
- // Han numerals in digit order, with both zero forms first ('' at 0, '' at 1), so a
57
- // character's digit value is `max(indexOf - 1, 0)`.
58
- const HAN_NUMERALS = '零〇一二三四五六七八九';
59
-
60
- // Arabic-Indic (U+0660–0669), Persian (U+06F0–06F9), and fullwidth (U+FF10–FF19) digits are
61
- // contiguous ranges whose bases are divisible by 16, so `charCode % 16` is the digit value for
62
- // all three systems.
63
- const NON_ASCII_DIGIT_RE = /[٠-٩۰-۹0-9]/g;
64
- const HAN_RE = /[零〇一二三四五六七八九]/g;
57
+ const HAN_NUMERALS = ['零', '〇', '一', '二', '三', '四', '五', '六', '', '', '九'];
58
+ // Map Han numeral characters to ASCII digits. Includes both forms of zero.
59
+ const HAN_NUMERAL_TO_DIGIT = {
60
+ 零: '0',
61
+ 〇: '0',
62
+ 一: '1',
63
+ 二: '2',
64
+ 三: '3',
65
+ 四: '4',
66
+ 五: '5',
67
+ 六: '6',
68
+ 七: '7',
69
+ 八: '8',
70
+ 九: '9'
71
+ };
72
+ const ARABIC_NUMERALS = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
73
+ const PERSIAN_NUMERALS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
74
+ const FULLWIDTH_NUMERALS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
65
75
  const PERCENTAGES = ['%', '٪', '%', '﹪'];
66
76
  const PERMILLE = ['‰', '؉'];
77
+ const UNICODE_MINUS_SIGNS = ['−', '-', '‒', '–', '—', '﹣'];
78
+ const UNICODE_PLUS_SIGNS = ['+', '﹢'];
67
79
 
68
80
  // Fullwidth punctuation common in CJK inputs
69
81
  const FULLWIDTH_DECIMAL = '.'; // U+FF0E
70
82
  const FULLWIDTH_GROUP = ','; // U+FF0C
71
83
 
72
- const PERCENT_RE = /[%٪%﹪]/;
73
- const PERMILLE_RE = /[‰؉]/;
74
- const PERCENT_GLOBAL_RE = /[%٪%﹪]/g;
75
- const PERMILLE_GLOBAL_RE = /[‰؉]/g;
76
-
77
- // Detection regexes (non-global to avoid lastIndex side effects). Arabic-Indic and Persian
78
- // digits share one regex because both resolve to the `ar` locale heuristic.
79
- const ARABIC_PERSIAN_DETECT_RE = /[٠-٩۰-۹]/;
80
- const HAN_DETECT_RE = /[零〇一二三四五六七八九]/;
81
- const ANY_NUMERAL_DETECT_RE = /[0-9٠-٩۰-۹0-9零〇一二三四五六七八九]/;
84
+ const ARABIC_RE = new RegExp(`[${ARABIC_NUMERALS.join('')}]`, 'g');
85
+ const PERSIAN_RE = new RegExp(`[${PERSIAN_NUMERALS.join('')}]`, 'g');
86
+ const FULLWIDTH_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`, 'g');
87
+ const HAN_RE = new RegExp(`[${HAN_NUMERALS.join('')}]`, 'g');
88
+ const PERCENT_RE = new RegExp(`[${PERCENTAGES.join('')}]`);
89
+ const PERMILLE_RE = new RegExp(`[${PERMILLE.join('')}]`);
90
+ const PERCENT_GLOBAL_RE = new RegExp(PERCENT_RE.source, 'g');
91
+ const PERMILLE_GLOBAL_RE = new RegExp(PERMILLE_RE.source, 'g');
92
+
93
+ // Detection regexes (non-global to avoid lastIndex side effects), derived from the numeral arrays
94
+ // so they can't drift out of sync.
95
+ const ARABIC_DETECT_RE = new RegExp(`[${ARABIC_NUMERALS.join('')}]`);
96
+ const PERSIAN_DETECT_RE = new RegExp(`[${PERSIAN_NUMERALS.join('')}]`);
97
+ const HAN_DETECT_RE = new RegExp(`[${HAN_NUMERALS.join('')}]`);
98
+ const FULLWIDTH_DETECT_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`);
82
99
 
83
100
  // Whether the character is a digit in any numeral system the field accepts.
84
101
  function isNumeralChar(char) {
85
- return ANY_NUMERAL_DETECT_RE.test(char);
102
+ return char >= '0' && char <= '9' || ARABIC_DETECT_RE.test(char) || PERSIAN_DETECT_RE.test(char) || HAN_DETECT_RE.test(char) || FULLWIDTH_DETECT_RE.test(char);
86
103
  }
87
104
  const BASE_NON_NUMERIC_SYMBOLS = ['.', ',', FULLWIDTH_DECIMAL, FULLWIDTH_GROUP, '٫', '٬'];
88
105
  const SPACE_SEPARATOR_RE = /\p{Zs}/u;
89
- // Format/bidi control characters (e.g. the LRM/ALM marks RTL locales insert around exponent and
90
- // currency signs). `parseNumber` strips these, so input validation must treat them as ignorable
91
- // rather than rejecting the typed string. Non-global so it's safe for repeated `.test(char)`.
92
- const FORMAT_CONTROL_DETECT_RE = /\p{Cf}/u;
93
- const FORMAT_CONTROL_GLOBAL_RE = /\p{Cf}/gu;
94
- const PLUS_SIGNS_WITH_ASCII = ['+', '+', '﹢'];
95
- const MINUS_SIGNS_WITH_ASCII = ['-', '−', '-', '‒', '–', '—', '﹣'];
106
+ const PLUS_SIGNS_WITH_ASCII = ['+', ...UNICODE_PLUS_SIGNS];
107
+ const MINUS_SIGNS_WITH_ASCII = ['-', ...UNICODE_MINUS_SIGNS];
96
108
  const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
109
+ const escapeClassChar = s => s.replace(/[-\\\]^]/g, m => `\\${m}`); // escape for use inside [...]
110
+
97
111
  function shiftDecimal(value, exponentDelta) {
98
112
  const [coefficient, exponent = '0'] = String(value).split('e');
99
113
  return Number(`${coefficient}e${Number(exponent) + exponentDelta}`);
100
114
  }
101
- const ANY_MINUS_RE = /[-−-‒–—﹣]/gu;
102
- const ANY_PLUS_RE = /[++﹢]/gu;
103
- const ANY_MINUS_DETECT_RE = /[-−-‒–—﹣]/;
104
- const ANY_PLUS_DETECT_RE = /[++﹢]/;
105
-
106
- // A representative value with a grouping separator and a fractional part, so that the formatter
107
- // emits every locale-specific part (group, decimal, currency, unit, literal, exponent, …). Shared
108
- // so the locale-detail and allowed-character derivations enumerate the same parts.
109
- const SAMPLE_FORMAT_NUMBER = 11111.1;
110
-
111
- /**
112
- * Returns the `Intl.NumberFormat` parts of a representative number, which surface every
113
- * non-numeric symbol a given locale/format renders.
114
- */
115
- function getFormatParts(locale, options) {
116
- return getFormatter(locale, options).formatToParts(SAMPLE_FORMAT_NUMBER);
117
- }
115
+ const charClassFrom = chars => `[${chars.map(escapeClassChar).join('')}]`;
116
+ const ANY_MINUS_CLASS = charClassFrom(['-'].concat(UNICODE_MINUS_SIGNS));
117
+ const ANY_PLUS_CLASS = charClassFrom(['+'].concat(UNICODE_PLUS_SIGNS));
118
+ const ANY_MINUS_RE = new RegExp(ANY_MINUS_CLASS, 'gu');
119
+ const ANY_PLUS_RE = new RegExp(ANY_PLUS_CLASS, 'gu');
120
+ const ANY_MINUS_DETECT_RE = new RegExp(ANY_MINUS_CLASS);
121
+ const ANY_PLUS_DETECT_RE = new RegExp(ANY_PLUS_CLASS);
118
122
  function getNumberLocaleDetails(locale, options) {
119
- const parts = getFormatParts(locale, options);
123
+ const parts = getFormatter(locale, options).formatToParts(11111.1);
120
124
  const result = {};
121
125
  parts.forEach(part => {
122
126
  result[part.type] = part.value;
123
127
  });
124
128
 
125
129
  // The formatting options may omit the decimal separator (e.g. integer formats), so resolve it
126
- // from the plain locale formatter, which renders one for every locale and numbering system. This
127
- // overrides any options-derived decimal too, which is safe because the separator is
128
- // locale-determined and identical across format styles. The ASCII seed is a formality to keep
129
- // the separator non-optional for callers; it is always replaced below.
130
- let decimal = '.';
130
+ // from the plain locale formatter. This overrides any options-derived decimal too, which is
131
+ // safe because the separator is locale-determined and identical across format styles.
131
132
  getFormatter(locale).formatToParts(0.1).forEach(part => {
132
133
  if (part.type === 'decimal') {
133
- decimal = part.value;
134
+ result[part.type] = part.value;
134
135
  }
135
136
  });
136
- return {
137
- ...result,
138
- decimal
139
- };
137
+ return result;
140
138
  }
141
139
  function parseNumber(formattedNumber, locale, options) {
140
+ if (formattedNumber == null) {
141
+ return null;
142
+ }
143
+
142
144
  // Normalize control characters and whitespace; remove bidi/format controls
143
- let input = formattedNumber.replace(FORMAT_CONTROL_GLOBAL_RE, '').trim();
145
+ let input = String(formattedNumber).replace(/\p{Cf}/gu, '').trim();
144
146
 
145
147
  // Normalize unicode minus/plus to ASCII, handle leading/trailing signs
146
148
  input = input.replace(ANY_MINUS_RE, '-').replace(ANY_PLUS_RE, '+');
147
149
  let isNegative = false;
148
150
 
149
- // Strips a matched sign (leading "-12" / trailing "1234-") while recording negativity.
150
- const takeSign = (match, sign) => {
151
- if (sign === '-') {
151
+ // Trailing sign, e.g. "1234-" / "1234+"
152
+ const trailing = input.match(/([+-])\s*$/);
153
+ if (trailing) {
154
+ if (trailing[1] === '-') {
152
155
  isNegative = true;
153
156
  }
154
- return '';
155
- };
156
- input = input.replace(/([+-])\s*$/, takeSign).replace(/^\s*([+-])/, takeSign);
157
+ input = input.replace(/([+-])\s*$/, '');
158
+ }
159
+ // Leading sign
160
+ const leading = input.match(/^\s*([+-])/);
161
+ if (leading) {
162
+ if (leading[1] === '-') {
163
+ isNegative = true;
164
+ }
165
+ input = input.replace(/^\s*[+-]/, '');
166
+ }
157
167
 
158
168
  // Heuristic locale detection
159
169
  let computedLocale = locale;
160
170
  if (computedLocale === undefined) {
161
- if (ARABIC_PERSIAN_DETECT_RE.test(input)) {
171
+ if (ARABIC_DETECT_RE.test(input) || PERSIAN_DETECT_RE.test(input)) {
162
172
  computedLocale = 'ar';
163
173
  } else if (HAN_DETECT_RE.test(input)) {
164
174
  computedLocale = 'zh';
@@ -190,17 +200,70 @@ function parseNumber(formattedNumber, locale, options) {
190
200
  groupRegex = new RegExp(escapeRegExp(group), 'g');
191
201
  }
192
202
  }
193
- const replacements = [[groupRegex, ''], [new RegExp(escapeRegExp(decimal), 'g'), '.'],
194
- // Fullwidth/Arabic punctuation
195
- [/[.٫]/g, '.'],
196
- // FULLWIDTH_DECIMAL, ARABIC DECIMAL SEPARATOR (U+066B)
197
- [/[,٬]/g, ''],
198
- // FULLWIDTH_GROUP, ARABIC THOUSANDS SEPARATOR (U+066C)
203
+ const replacements = [{
204
+ regex: groupRegex,
205
+ replacement: ''
206
+ }, {
207
+ regex: decimal ? new RegExp(escapeRegExp(decimal), 'g') : null,
208
+ replacement: '.'
209
+ },
210
+ // Fullwidth punctuation
211
+ {
212
+ regex: /./g,
213
+ replacement: '.'
214
+ },
215
+ // FULLWIDTH_DECIMAL
216
+ {
217
+ regex: /,/g,
218
+ replacement: ''
219
+ },
220
+ // FULLWIDTH_GROUP
221
+ // Arabic punctuation
222
+ {
223
+ regex: /٫/g,
224
+ replacement: '.'
225
+ },
226
+ // ARABIC DECIMAL SEPARATOR (U+066B)
227
+ {
228
+ regex: /٬/g,
229
+ replacement: ''
230
+ },
231
+ // ARABIC THOUSANDS SEPARATOR (U+066C)
199
232
  // Currency & unit labels
200
- [currency ? new RegExp(escapeRegExp(currency), 'g') : null, ''], [unitRegex, ''], [PERCENT_GLOBAL_RE, ''], [PERMILLE_GLOBAL_RE, ''], [exponentSeparator ? new RegExp(escapeRegExp(exponentSeparator), 'g') : null, 'e'],
233
+ {
234
+ regex: currency ? new RegExp(escapeRegExp(currency), 'g') : null,
235
+ replacement: ''
236
+ }, {
237
+ regex: unitRegex,
238
+ replacement: ''
239
+ }, {
240
+ regex: PERCENT_GLOBAL_RE,
241
+ replacement: ''
242
+ }, {
243
+ regex: PERMILLE_GLOBAL_RE,
244
+ replacement: ''
245
+ }, {
246
+ regex: exponentSeparator ? new RegExp(escapeRegExp(exponentSeparator), 'g') : null,
247
+ replacement: 'e'
248
+ },
201
249
  // Numeral systems to ASCII digits
202
- [NON_ASCII_DIGIT_RE, ch => String(ch.charCodeAt(0) % 16)], [HAN_RE, ch => String(Math.max(HAN_NUMERALS.indexOf(ch) - 1, 0))]];
203
- let unformatted = replacements.reduce((acc, [regex, replacement]) => {
250
+ {
251
+ regex: ARABIC_RE,
252
+ replacement: ch => String(ARABIC_NUMERALS.indexOf(ch))
253
+ }, {
254
+ regex: PERSIAN_RE,
255
+ replacement: ch => String(PERSIAN_NUMERALS.indexOf(ch))
256
+ }, {
257
+ regex: FULLWIDTH_RE,
258
+ replacement: ch => String(FULLWIDTH_NUMERALS.indexOf(ch))
259
+ }, {
260
+ regex: HAN_RE,
261
+ replacement: ch => HAN_NUMERAL_TO_DIGIT[ch]
262
+ }];
263
+ let unformatted = replacements.reduce((acc, {
264
+ regex,
265
+ replacement
266
+ }) => {
204
267
  return regex ? acc.replace(regex, replacement) : acc;
205
268
  }, input);
206
269
 
@@ -278,18 +341,27 @@ function removeFloatingPointErrors(value, format) {
278
341
  }
279
342
  return formatter.format(roundedValue) === roundedText ? roundedValue : value;
280
343
  }
281
- function snapToStep(value, base, step, nearest) {
344
+ function snapToStep(value, base, step, mode = 'directional') {
282
345
  const stepSize = Math.abs(step);
283
346
  const direction = Math.sign(step);
284
347
  const tolerance = stepSize * STEP_EPSILON_FACTOR * direction;
285
348
  const rawSteps = value - base + tolerance;
286
- if (nearest) {
349
+ if (mode === 'nearest') {
287
350
  return base + Math.round(rawSteps / step) * step;
288
351
  }
289
352
  const snappedSteps = direction > 0 ? Math.floor(rawSteps / stepSize) : Math.ceil(rawSteps / stepSize);
290
353
  return base + snappedSteps * stepSize;
291
354
  }
292
- function toValidatedNumber(value, step, minWithDefault, maxWithDefault, minWithZeroDefault, format, snapOnStep, small, shouldClamp) {
355
+ function toValidatedNumber(value, {
356
+ step,
357
+ minWithDefault,
358
+ maxWithDefault,
359
+ minWithZeroDefault,
360
+ format,
361
+ snapOnStep,
362
+ small,
363
+ clamp: shouldClamp
364
+ }) {
293
365
  if (value === null) {
294
366
  return value;
295
367
  }
@@ -297,9 +369,8 @@ function toValidatedNumber(value, step, minWithDefault, maxWithDefault, minWithZ
297
369
  if (step != null && snapOnStep && step !== 0) {
298
370
  const base = small || minWithDefault === Number.MIN_SAFE_INTEGER ? minWithZeroDefault : minWithDefault;
299
371
 
300
- // Snap before clamping so non-step-aligned boundaries stay reachable. Small (alt-key) steps
301
- // snap to the nearest multiple; regular steps snap directionally.
302
- nextValue = snapToStep(nextValue, base, step, small);
372
+ // Snap before clamping so non-step-aligned boundaries stay reachable.
373
+ nextValue = snapToStep(nextValue, base, step, small ? 'nearest' : 'directional');
303
374
  }
304
375
 
305
376
  // Clamp before rounding so a value just outside a fractional boundary (e.g. `0.4` with
@@ -354,6 +425,7 @@ const NumberFieldRoot = /*#__PURE__*/React.forwardRef(function NumberFieldRoot(c
354
425
  validityData,
355
426
  disabled: fieldDisabled,
356
427
  setFilled,
428
+ invalid,
357
429
  name: fieldName,
358
430
  state: fieldState,
359
431
  validation
@@ -402,46 +474,48 @@ const NumberFieldRoot = /*#__PURE__*/React.forwardRef(function NumberFieldRoot(c
402
474
  const [inputValue, setInputValue] = React.useState(() => formatNumber(value, locale, format));
403
475
  const [inputMode, setInputMode] = React.useState('numeric');
404
476
  const getAllowedNonNumericKeys = useStableCallback(() => {
405
- const parts = getFormatParts(locale, format);
406
- const keys = new Set(BASE_NON_NUMERIC_SYMBOLS);
407
- const addAll = chars => chars.forEach(char => keys.add(char));
408
-
409
- // Integer formats omit the decimal from `parts`, so fall back to the locale's separator in that
410
- // case; it must stay typeable regardless of whether the format renders a fraction.
411
- const decimal = parts.find(part => part.type === 'decimal')?.value ?? getNumberLocaleDetails(locale, format).decimal;
412
- keys.add(decimal);
413
-
414
- // Allow every non-digit character the formatter renders — separators, currency symbols, units
415
- // (e.g. `km/h`, `°C`), exponent separators, and locale literals — decomposed per character
416
- // because the input validates the typed string one character at a time. Deriving these from
417
- // the formatter covers multi-character and locale-specific symbols of every part type
418
- // uniformly. `compact` suffixes (e.g. `K`/`M`) are excluded because `parseNumber` can't reverse
419
- // them, so allowing them would yield a silently incorrect value.
420
- parts.forEach(part => {
421
- if (part.type === 'integer' || part.type === 'fraction' || part.type === 'exponentInteger' || part.type === 'compact') {
422
- return;
423
- }
424
- addAll(Array.from(part.value));
425
- if (SPACE_SEPARATOR_RE.test(part.value)) {
477
+ const {
478
+ decimal,
479
+ group,
480
+ currency,
481
+ literal
482
+ } = getNumberLocaleDetails(locale, format);
483
+ const keys = new Set();
484
+ BASE_NON_NUMERIC_SYMBOLS.forEach(symbol => keys.add(symbol));
485
+ if (decimal) {
486
+ keys.add(decimal);
487
+ }
488
+ if (group) {
489
+ keys.add(group);
490
+ if (SPACE_SEPARATOR_RE.test(group)) {
426
491
  keys.add(' ');
427
492
  }
428
- });
493
+ }
429
494
  const allowPercentSymbols = formatStyle === 'percent' || formatStyle === 'unit' && format?.unit === 'percent';
430
495
  const allowPermilleSymbols = formatStyle === 'percent' || formatStyle === 'unit' && format?.unit === 'permille';
431
-
432
- // Tolerate percent/permille variants the formatter doesn't emit but users may type or paste.
433
496
  if (allowPercentSymbols) {
434
- addAll(PERCENTAGES);
497
+ PERCENTAGES.forEach(key => keys.add(key));
435
498
  }
436
499
  if (allowPermilleSymbols) {
437
- addAll(PERMILLE);
500
+ PERMILLE.forEach(key => keys.add(key));
501
+ }
502
+ if (formatStyle === 'currency' && currency) {
503
+ keys.add(currency);
504
+ }
505
+ if (literal) {
506
+ // Some locales (e.g. de-DE) insert a literal space character between the number
507
+ // and the symbol, so allow those characters to be typed/removed.
508
+ Array.from(literal).forEach(char => keys.add(char));
509
+ if (SPACE_SEPARATOR_RE.test(literal)) {
510
+ keys.add(' ');
511
+ }
438
512
  }
439
513
 
440
514
  // Allow plus sign in all cases; minus sign when negatives are valid, or when out-of-range
441
515
  // entry is allowed so native underflow validation can be triggered from the keyboard.
442
- addAll(PLUS_SIGNS_WITH_ASCII);
516
+ PLUS_SIGNS_WITH_ASCII.forEach(key => keys.add(key));
443
517
  if (minWithDefault < 0 || allowOutOfRange) {
444
- addAll(MINUS_SIGNS_WITH_ASCII);
518
+ MINUS_SIGNS_WITH_ASCII.forEach(key => keys.add(key));
445
519
  }
446
520
  return keys;
447
521
  });
@@ -459,13 +533,21 @@ const NumberFieldRoot = /*#__PURE__*/React.forwardRef(function NumberFieldRoot(c
459
533
  const dir = details.direction;
460
534
 
461
535
  // Direct text entry (typing, pasting, clearing, autofill) behaves natively; step-based
462
- // interactions (keyboard arrows, buttons, wheel, scrub) do not. All direct-entry reasons
463
- // (`input-change`, `input-clear`, `input-blur`, `input-paste`) share the `input-` prefix.
464
- const isInputReason = details.reason.startsWith('input-') || details.reason === none;
536
+ // interactions (keyboard arrows, buttons, wheel, scrub) do not.
537
+ const isInputReason = details.reason === inputChange || details.reason === inputClear || details.reason === inputBlur || details.reason === inputPaste || details.reason === none;
465
538
 
466
539
  // Only allow out-of-range values for direct text entry. Step-based interactions still clamp.
467
540
  const shouldClampValue = !allowOutOfRange || !isInputReason;
468
- const validatedValue = toValidatedNumber(unvalidatedValue, dir ? getStepAmount(eventWithOptionalKeyState) * dir : undefined, minWithDefault, maxWithDefault, minWithZeroDefault, formatOptionsRef.current, snapOnStep, eventWithOptionalKeyState?.altKey ?? false, shouldClampValue);
541
+ const validatedValue = toValidatedNumber(unvalidatedValue, {
542
+ step: dir ? getStepAmount(eventWithOptionalKeyState) * dir : undefined,
543
+ format: formatOptionsRef.current,
544
+ minWithDefault,
545
+ maxWithDefault,
546
+ minWithZeroDefault,
547
+ snapOnStep,
548
+ small: eventWithOptionalKeyState?.altKey ?? false,
549
+ clamp: shouldClampValue
550
+ });
469
551
 
470
552
  // Notify about a change even when the numeric value is unchanged for input reasons: the
471
553
  // typed text may clamp/snap to the current value, or differ while validation normalizes
@@ -575,10 +657,10 @@ const NumberFieldRoot = /*#__PURE__*/React.forwardRef(function NumberFieldRoot(c
575
657
  const changed = incrementValue(amount, {
576
658
  direction: event.deltaY > 0 ? -1 : 1,
577
659
  event,
578
- reason: wheel
660
+ reason: 'wheel'
579
661
  });
580
662
  if (changed) {
581
- onValueCommitted(lastChangedValueRef.current, createGenericEventDetails(wheel, event));
663
+ onValueCommitted(lastChangedValueRef.current ?? valueRef.current, createGenericEventDetails(wheel, event));
582
664
  }
583
665
  }
584
666
  return addEventListener(element, 'wheel', handleWheel);
@@ -594,8 +676,12 @@ const NumberFieldRoot = /*#__PURE__*/React.forwardRef(function NumberFieldRoot(c
594
676
  }), [fieldState, disabled, readOnly, required, value, inputValue, isScrubbing]);
595
677
  const contextValue = React.useMemo(() => ({
596
678
  inputRef,
679
+ inputValue,
680
+ value,
597
681
  minWithDefault,
598
682
  maxWithDefault,
683
+ disabled,
684
+ readOnly,
599
685
  id,
600
686
  setValue,
601
687
  incrementValue,
@@ -607,16 +693,19 @@ const NumberFieldRoot = /*#__PURE__*/React.forwardRef(function NumberFieldRoot(c
607
693
  hasPendingCommitRef,
608
694
  name,
609
695
  nameProp,
696
+ required,
697
+ invalid,
610
698
  inputMode,
611
699
  getAllowedNonNumericKeys,
612
700
  min,
613
701
  max,
614
702
  setInputValue,
615
703
  locale,
704
+ isScrubbing,
616
705
  setIsScrubbing,
617
706
  state,
618
707
  onValueCommitted
619
- }), [inputRef, minWithDefault, maxWithDefault, id, setValue, incrementValue, getStepAmount, formatOptionsRef, valueRef, name, nameProp, inputMode, getAllowedNonNumericKeys, min, max, setInputValue, locale, state, onValueCommitted]);
708
+ }), [inputRef, inputValue, value, minWithDefault, maxWithDefault, disabled, readOnly, id, setValue, incrementValue, getStepAmount, formatOptionsRef, valueRef, name, nameProp, required, invalid, inputMode, getAllowedNonNumericKeys, min, max, setInputValue, locale, isScrubbing, state, onValueCommitted]);
620
709
  const element = useRenderElement('div', componentProps, {
621
710
  ref: forwardedRef,
622
711
  state,
@@ -631,7 +720,7 @@ const NumberFieldRoot = /*#__PURE__*/React.forwardRef(function NumberFieldRoot(c
631
720
  inputRef.current?.focus();
632
721
  },
633
722
  onChange(event) {
634
- // Workaround for https://github.com/react/react/issues/9023
723
+ // Workaround for https://github.com/facebook/react/issues/9023
635
724
  if (event.nativeEvent.defaultPrevented || disabled || readOnly) {
636
725
  return;
637
726
  }
@@ -656,7 +745,7 @@ const NumberFieldRoot = /*#__PURE__*/React.forwardRef(function NumberFieldRoot(c
656
745
  min: min,
657
746
  max: max
658
747
  // stepMismatch validation is broken unless an explicit `min` is added.
659
- // See https://github.com/react/react/issues/12334.
748
+ // See https://github.com/facebook/react/issues/12334.
660
749
  ,
661
750
  step: stepProp,
662
751
  disabled: disabled,
@@ -753,6 +842,7 @@ function isTouchLikePointerType(pointerType) {
753
842
  function usePressAndHold(params) {
754
843
  const {
755
844
  disabled,
845
+ readOnly = false,
756
846
  tick,
757
847
  onStop,
758
848
  tickDelay = DEFAULT_TICK_DELAY,
@@ -832,7 +922,8 @@ function usePressAndHold(params) {
832
922
  isTouchingButtonRef.current = false;
833
923
  },
834
924
  onPointerDown(event) {
835
- if (event.defaultPrevented || event.button || disabled) {
925
+ const isMainButton = !event.button || event.button === 0;
926
+ if (event.defaultPrevented || !isMainButton || disabled || readOnly) {
836
927
  return;
837
928
  }
838
929
  pointerTypeRef.current = event.pointerType;
@@ -875,10 +966,12 @@ function usePressAndHold(params) {
875
966
  }
876
967
  },
877
968
  onPointerMove(event) {
878
- if (disabled || !isTouchLikePointerType(event.pointerType) || !isPressedRef.current) {
969
+ if (disabled || readOnly || !isTouchLikePointerType(event.pointerType) || !isPressedRef.current) {
879
970
  return;
880
971
  }
881
- movesAfterTouchRef.current += 1;
972
+ if (movesAfterTouchRef.current != null) {
973
+ movesAfterTouchRef.current += 1;
974
+ }
882
975
  const {
883
976
  x,
884
977
  y
@@ -890,7 +983,7 @@ function usePressAndHold(params) {
890
983
  }
891
984
  },
892
985
  onMouseEnter(event) {
893
- if (event.defaultPrevented || disabled || !isPressedRef.current || isTouchingButtonRef.current || isTouchLikePointerType(pointerTypeRef.current)) {
986
+ if (event.defaultPrevented || disabled || readOnly || !isPressedRef.current || isTouchingButtonRef.current || isTouchLikePointerType(pointerTypeRef.current)) {
894
987
  return;
895
988
  }
896
989
  startAutoChange(event.nativeEvent);
@@ -923,47 +1016,32 @@ function usePressAndHold(params) {
923
1016
  };
924
1017
  }
925
1018
 
1019
+ const CHANGE_VALUE_TICK_DELAY = 60;
1020
+ const START_AUTO_CHANGE_DELAY = 400;
1021
+ const SCROLLING_POINTER_MOVE_DISTANCE = 8;
1022
+
926
1023
  const SELECT_NONE_STYLE = {
927
1024
  WebkitUserSelect: 'none',
928
1025
  userSelect: 'none'
929
1026
  };
930
- /**
931
- * Shared implementation for the increment and decrement stepper buttons. They differ only in the
932
- * direction they step and the boundary (`max` vs `min`) at which they become disabled.
933
- */
934
- function useNumberFieldStepperButton(componentProps, forwardedRef, isIncrement) {
935
- const {
936
- render,
937
- className,
938
- disabled: disabledProp = false,
939
- nativeButton = true,
940
- style,
941
- ...elementProps
942
- } = componentProps;
1027
+ function useNumberFieldButton(params) {
943
1028
  const {
944
1029
  allowInputSyncRef,
1030
+ disabled,
945
1031
  formatOptionsRef,
946
1032
  getStepAmount,
947
1033
  id,
948
1034
  incrementValue,
949
1035
  inputRef,
950
- maxWithDefault,
951
- minWithDefault,
1036
+ inputValue,
1037
+ isIncrement,
1038
+ locale,
1039
+ readOnly,
952
1040
  setValue,
953
- state,
954
1041
  valueRef,
955
- locale,
956
1042
  lastChangedValueRef,
957
1043
  onValueCommitted
958
- } = useNumberFieldRootContext();
959
- const {
960
- disabled: contextDisabled,
961
- readOnly,
962
- value,
963
- inputValue
964
- } = state;
965
- const isAtBoundary = value != null && (isIncrement ? value >= maxWithDefault : value <= minWithDefault);
966
- const disabled = disabledProp || contextDisabled || isAtBoundary;
1044
+ } = params;
967
1045
  const pressReason = isIncrement ? incrementPress : decrementPress;
968
1046
  function commitValue(nativeEvent) {
969
1047
  const shouldCommitInputValue = !allowInputSyncRef.current;
@@ -999,6 +1077,9 @@ function useNumberFieldStepperButton(componentProps, forwardedRef, isIncrement)
999
1077
  } = usePressAndHold({
1000
1078
  disabled: disabled || readOnly,
1001
1079
  elementRef: inputRef,
1080
+ tickDelay: CHANGE_VALUE_TICK_DELAY,
1081
+ startDelay: START_AUTO_CHANGE_DELAY,
1082
+ scrollDistance: SCROLLING_POINTER_MOVE_DISTANCE,
1002
1083
  tick(triggerEvent) {
1003
1084
  const amount = getStepAmount(triggerEvent);
1004
1085
  return incrementValue(amount, {
@@ -1043,7 +1124,8 @@ function useNumberFieldStepperButton(componentProps, forwardedRef, isIncrement)
1043
1124
  }
1044
1125
  },
1045
1126
  onPointerDown(event) {
1046
- if (event.defaultPrevented || readOnly || event.button || disabled) {
1127
+ const isMainButton = !event.button || event.button === 0;
1128
+ if (event.defaultPrevented || readOnly || !isMainButton || disabled) {
1047
1129
  return;
1048
1130
  }
1049
1131
 
@@ -1059,6 +1141,61 @@ function useNumberFieldStepperButton(componentProps, forwardedRef, isIncrement)
1059
1141
  pointerHandlers.onPointerDown(event);
1060
1142
  }
1061
1143
  };
1144
+ return props;
1145
+ }
1146
+
1147
+ /**
1148
+ * Shared implementation for the increment and decrement stepper buttons. They differ only in the
1149
+ * direction they step and the boundary (`max` vs `min`) at which they become disabled.
1150
+ */
1151
+ function useNumberFieldStepperButton(componentProps, forwardedRef, isIncrement) {
1152
+ const {
1153
+ render,
1154
+ className,
1155
+ disabled: disabledProp = false,
1156
+ nativeButton = true,
1157
+ style,
1158
+ ...elementProps
1159
+ } = componentProps;
1160
+ const {
1161
+ allowInputSyncRef,
1162
+ disabled: contextDisabled,
1163
+ formatOptionsRef,
1164
+ getStepAmount,
1165
+ id,
1166
+ incrementValue,
1167
+ inputRef,
1168
+ inputValue,
1169
+ maxWithDefault,
1170
+ minWithDefault,
1171
+ readOnly,
1172
+ setValue,
1173
+ state,
1174
+ value,
1175
+ valueRef,
1176
+ locale,
1177
+ lastChangedValueRef,
1178
+ onValueCommitted
1179
+ } = useNumberFieldRootContext();
1180
+ const isAtBoundary = value != null && (isIncrement ? value >= maxWithDefault : value <= minWithDefault);
1181
+ const disabled = disabledProp || contextDisabled || isAtBoundary;
1182
+ const props = useNumberFieldButton({
1183
+ isIncrement,
1184
+ inputRef,
1185
+ inputValue,
1186
+ disabled,
1187
+ readOnly,
1188
+ id,
1189
+ setValue,
1190
+ getStepAmount,
1191
+ incrementValue,
1192
+ allowInputSyncRef,
1193
+ formatOptionsRef,
1194
+ valueRef,
1195
+ locale,
1196
+ lastChangedValueRef,
1197
+ onValueCommitted
1198
+ });
1062
1199
  const {
1063
1200
  getButtonProps,
1064
1201
  buttonRef
@@ -1121,33 +1258,31 @@ const NumberFieldInput = /*#__PURE__*/React.forwardRef(function NumberFieldInput
1121
1258
  } = componentProps;
1122
1259
  const {
1123
1260
  allowInputSyncRef,
1261
+ disabled,
1124
1262
  formatOptionsRef,
1125
1263
  getAllowedNonNumericKeys,
1126
1264
  getStepAmount,
1127
1265
  id,
1128
1266
  incrementValue,
1129
1267
  inputMode,
1268
+ inputValue,
1130
1269
  max,
1131
1270
  min,
1132
1271
  name,
1133
1272
  nameProp,
1273
+ readOnly,
1274
+ required,
1134
1275
  setValue,
1135
1276
  state,
1136
1277
  setInputValue,
1137
1278
  locale,
1138
1279
  inputRef,
1280
+ value,
1139
1281
  onValueCommitted,
1140
1282
  lastChangedValueRef,
1141
1283
  hasPendingCommitRef,
1142
1284
  valueRef
1143
1285
  } = useNumberFieldRootContext();
1144
- const {
1145
- disabled,
1146
- readOnly,
1147
- required,
1148
- value,
1149
- inputValue
1150
- } = state;
1151
1286
  const {
1152
1287
  clearErrors
1153
1288
  } = useFormContext();
@@ -1282,7 +1417,7 @@ const NumberFieldInput = /*#__PURE__*/React.forwardRef(function NumberFieldInput
1282
1417
  blockRevalidationRef.current = false;
1283
1418
  return;
1284
1419
  }
1285
- committedValue = lastChangedValueRef.current;
1420
+ committedValue = lastChangedValueRef.current ?? committed;
1286
1421
  // If validation normalized back to the current value, `useValueChanged` won't fire to
1287
1422
  // reset the flag, so reset it here or the next external change won't revalidate.
1288
1423
  if (committedValue === value) {
@@ -1303,7 +1438,7 @@ const NumberFieldInput = /*#__PURE__*/React.forwardRef(function NumberFieldInput
1303
1438
  }
1304
1439
  },
1305
1440
  onChange(event) {
1306
- // Workaround for https://github.com/react/react/issues/9023
1441
+ // Workaround for https://github.com/facebook/react/issues/9023
1307
1442
  if (event.nativeEvent.defaultPrevented) {
1308
1443
  return;
1309
1444
  }
@@ -1319,10 +1454,7 @@ const NumberFieldInput = /*#__PURE__*/React.forwardRef(function NumberFieldInput
1319
1454
  // currently parseable into a number. This preserves good UX for IME
1320
1455
  // composition/partial input while still providing live numeric updates when possible.
1321
1456
  const allowedNonNumericKeys = getAllowedNonNumericKeys();
1322
- const isValidCharacterString = Array.from(targetValue).every(ch => isNumeralChar(ch) || ANY_MINUS_DETECT_RE.test(ch) || allowedNonNumericKeys.has(ch) ||
1323
- // Bidi/format controls are stripped by `parseNumber`; don't let them reject the string
1324
- // (RTL locales insert them around exponent/currency signs, e.g. scientific notation).
1325
- FORMAT_CONTROL_DETECT_RE.test(ch));
1457
+ const isValidCharacterString = Array.from(targetValue).every(ch => isNumeralChar(ch) || ANY_MINUS_DETECT_RE.test(ch) || allowedNonNumericKeys.has(ch));
1326
1458
  if (!isValidCharacterString) {
1327
1459
  return;
1328
1460
  }
@@ -1372,7 +1504,7 @@ const NumberFieldInput = /*#__PURE__*/React.forwardRef(function NumberFieldInput
1372
1504
  if (event.key === symbol) {
1373
1505
  const symbolIndex = inputValue.indexOf(symbol);
1374
1506
  const isSymbolHighlighted = selectionContainsIndex(symbolIndex);
1375
- isAllowedNonNumericKey = symbolIndex === -1 || isAllSelected || isSymbolHighlighted;
1507
+ isAllowedNonNumericKey = !inputValue.includes(symbol) || isAllSelected || isSymbolHighlighted;
1376
1508
  }
1377
1509
  });
1378
1510
  const isNavigateKey = NAVIGATE_KEYS.has(event.key);
@@ -1385,18 +1517,12 @@ const NumberFieldInput = /*#__PURE__*/React.forwardRef(function NumberFieldInput
1385
1517
  event.which === 229 || event.altKey && !isStepKey || event.ctrlKey || event.metaKey || isAllowedNonNumericKey || isNumeralChar(event.key) || isNavigateKey) {
1386
1518
  return;
1387
1519
  }
1388
-
1389
- // Home/End jump to the corresponding bound, but only when that bound is defined.
1390
- let boundaryValue = null;
1391
- if (event.key === 'Home' && min != null) {
1392
- boundaryValue = min;
1393
- } else if (event.key === 'End' && max != null) {
1394
- boundaryValue = max;
1395
- }
1520
+ const willSetHome = event.key === 'Home' && min != null;
1521
+ const willSetEnd = event.key === 'End' && max != null;
1396
1522
 
1397
1523
  // Let the browser handle multi-character keys we don't act on (PageUp, Insert, F-keys,
1398
1524
  // Home/End without min/max); invalid single characters are still blocked below.
1399
- if (event.key.length > 1 && !isStepKey && boundaryValue === null) {
1525
+ if (event.key.length > 1 && !isStepKey && !willSetHome && !willSetEnd) {
1400
1526
  return;
1401
1527
  }
1402
1528
 
@@ -1408,14 +1534,12 @@ const NumberFieldInput = /*#__PURE__*/React.forwardRef(function NumberFieldInput
1408
1534
  const amount = getStepAmount(event);
1409
1535
 
1410
1536
  // Prevent insertion of text or caret from moving.
1411
- event.preventDefault();
1412
- event.stopPropagation();
1537
+ stopEvent(event);
1413
1538
  const commitDetails = createGenericEventDetails(keyboard, nativeEvent);
1414
1539
  let changed = false;
1415
- if (isStepKey || boundaryValue !== null) {
1540
+ if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
1416
1541
  allowInputSyncRef.current = true;
1417
- }
1418
- if (isStepKey) {
1542
+
1419
1543
  // When stepping from the synced numeric state, refresh the commit ref to the current
1420
1544
  // value so a canceled step can't commit a stale `lastChangedValueRef` left over from an
1421
1545
  // earlier change (mirrors the button path).
@@ -1428,14 +1552,15 @@ const NumberFieldInput = /*#__PURE__*/React.forwardRef(function NumberFieldInput
1428
1552
  event: nativeEvent,
1429
1553
  reason: keyboard
1430
1554
  });
1431
- } else if (boundaryValue !== null) {
1432
- changed = setValue(boundaryValue, createChangeEventDetails(keyboard, nativeEvent));
1555
+ } else if (willSetHome) {
1556
+ allowInputSyncRef.current = true;
1557
+ changed = setValue(min, createChangeEventDetails(keyboard, nativeEvent));
1558
+ } else if (willSetEnd) {
1559
+ allowInputSyncRef.current = true;
1560
+ changed = setValue(max, createChangeEventDetails(keyboard, nativeEvent));
1433
1561
  }
1434
-
1435
- // `changed` is only true when `setValue` applied the change, which records the stored
1436
- // (clamped/snapped) value, so commit that rather than the pre-validation input.
1437
1562
  if (changed) {
1438
- onValueCommitted(lastChangedValueRef.current, commitDetails);
1563
+ onValueCommitted(lastChangedValueRef.current ?? valueRef.current, commitDetails);
1439
1564
  }
1440
1565
  },
1441
1566
  onPaste(event) {
@@ -1446,7 +1571,6 @@ const NumberFieldInput = /*#__PURE__*/React.forwardRef(function NumberFieldInput
1446
1571
  try {
1447
1572
  pastedData = event.clipboardData?.getData('text/plain') ?? '';
1448
1573
  } catch {
1449
- /* istanbul ignore else -- `process.env.NODE_ENV` is a build-time constant under test */
1450
1574
  if (process.env.NODE_ENV !== 'production') {
1451
1575
  const ownerStackMessage = SafeReact.captureOwnerStack?.() || '';
1452
1576
  warn('<NumberField.Input> could not read clipboard text during paste handling.', ownerStackMessage);
@@ -1459,12 +1583,9 @@ const NumberFieldInput = /*#__PURE__*/React.forwardRef(function NumberFieldInput
1459
1583
 
1460
1584
  // Insert the pasted text at the caret/selection instead of replacing the entire value,
1461
1585
  // matching native input behavior (e.g. pasting "5" into "123|" yields "1235").
1462
- // The component renders `type="text"`, which always reports a selection range. Overriding
1463
- // `type` with a selection-less one (`email`, `number`) is unsupported either way: the caret
1464
- // restore above throws on those, so there is no working behavior to preserve here.
1465
1586
  const input = event.currentTarget;
1466
- const selectionStart = input.selectionStart;
1467
- const selectionEnd = input.selectionEnd;
1587
+ const selectionStart = input.selectionStart ?? inputValue.length;
1588
+ const selectionEnd = input.selectionEnd ?? inputValue.length;
1468
1589
  const nextText = inputValue.slice(0, selectionStart) + pastedData + inputValue.slice(selectionEnd);
1469
1590
  const parsedValue = parseNumber(nextText, locale, formatOptionsRef.current);
1470
1591
  if (parsedValue !== null) {