@jobber/components 9.5.3 → 9.6.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 (178) hide show
  1. package/Combobox.d.ts +1 -0
  2. package/Combobox.js +17 -0
  3. package/dist/Autocomplete/index.cjs +0 -2
  4. package/dist/Autocomplete/index.mjs +0 -2
  5. package/dist/Autocomplete-cjs.js +4 -5
  6. package/dist/Autocomplete-es.js +1 -2
  7. package/dist/Card/index.cjs +2 -2
  8. package/dist/Card/index.mjs +2 -2
  9. package/dist/Chip/index.cjs +0 -2
  10. package/dist/Chip/index.mjs +0 -2
  11. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +0 -2
  12. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +0 -2
  13. package/dist/Chips/InternalChipDismissible/index.cjs +0 -2
  14. package/dist/Chips/InternalChipDismissible/index.mjs +0 -2
  15. package/dist/Chips/index.cjs +0 -2
  16. package/dist/Chips/index.mjs +0 -2
  17. package/dist/Combobox/Combobox.contentSlots.d.ts +5 -0
  18. package/dist/Combobox/Combobox.context.d.ts +34 -0
  19. package/dist/Combobox/Combobox.d.ts +69 -0
  20. package/dist/Combobox/Combobox.footer.d.ts +7 -0
  21. package/dist/Combobox/Combobox.items.d.ts +25 -0
  22. package/dist/Combobox/Combobox.responsive.d.ts +54 -0
  23. package/dist/Combobox/Combobox.selectAll.d.ts +4 -0
  24. package/dist/Combobox/Combobox.slotFactory.d.ts +7 -0
  25. package/dist/Combobox/Combobox.testUtils.d.ts +12 -0
  26. package/dist/Combobox/Combobox.types.d.ts +166 -0
  27. package/dist/Combobox/index.cjs +35 -0
  28. package/dist/Combobox/index.d.ts +2 -0
  29. package/dist/Combobox/index.mjs +29 -0
  30. package/dist/Combobox-cjs.js +675 -0
  31. package/dist/Combobox-es.js +673 -0
  32. package/dist/ComboboxPrimitive-cjs.js +88 -47
  33. package/dist/ComboboxPrimitive-es.js +39 -23
  34. package/dist/{ComboboxChipRemove-cjs.js → ComboboxSeparator-cjs.js} +501 -455
  35. package/dist/{ComboboxChipRemove-es.js → ComboboxSeparator-es.js} +500 -456
  36. package/dist/ConfirmationModal/index.cjs +0 -2
  37. package/dist/ConfirmationModal/index.mjs +0 -2
  38. package/dist/ContentBlock/index.cjs +1 -0
  39. package/dist/ContentBlock/index.mjs +1 -0
  40. package/dist/ContentBlock-cjs.js +2 -18
  41. package/dist/ContentBlock-es.js +1 -17
  42. package/dist/DataDump/index.cjs +2 -2
  43. package/dist/DataDump/index.mjs +2 -2
  44. package/dist/DataList/components/DataListActions/index.cjs +0 -2
  45. package/dist/DataList/components/DataListActions/index.mjs +0 -2
  46. package/dist/DataList/components/DataListBulkActions/index.cjs +0 -2
  47. package/dist/DataList/components/DataListBulkActions/index.mjs +0 -2
  48. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +0 -2
  49. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +0 -2
  50. package/dist/DataList/components/DataListFilters/index.cjs +0 -2
  51. package/dist/DataList/components/DataListFilters/index.mjs +0 -2
  52. package/dist/DataList/components/DataListHeader/index.cjs +0 -2
  53. package/dist/DataList/components/DataListHeader/index.mjs +0 -2
  54. package/dist/DataList/components/DataListItem/index.cjs +0 -2
  55. package/dist/DataList/components/DataListItem/index.mjs +0 -2
  56. package/dist/DataList/components/DataListItemActions/index.cjs +0 -2
  57. package/dist/DataList/components/DataListItemActions/index.mjs +0 -2
  58. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +0 -2
  59. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +0 -2
  60. package/dist/DataList/components/DataListItems/index.cjs +0 -2
  61. package/dist/DataList/components/DataListItems/index.mjs +0 -2
  62. package/dist/DataList/components/DataListLayout/index.cjs +0 -2
  63. package/dist/DataList/components/DataListLayout/index.mjs +0 -2
  64. package/dist/DataList/components/DataListLayoutActions/index.cjs +0 -2
  65. package/dist/DataList/components/DataListLayoutActions/index.mjs +0 -2
  66. package/dist/DataList/index.cjs +0 -2
  67. package/dist/DataList/index.mjs +0 -2
  68. package/dist/DatePicker/index.cjs +0 -2
  69. package/dist/DatePicker/index.mjs +0 -2
  70. package/dist/DatePicker-cjs.js +4 -5
  71. package/dist/DatePicker-es.js +1 -2
  72. package/dist/Dialog/index.cjs +2 -2
  73. package/dist/Dialog/index.mjs +2 -2
  74. package/dist/DrawerDescription-cjs.js +268 -99
  75. package/dist/DrawerDescription-es.js +270 -103
  76. package/dist/DrawerTitle-cjs.js +2 -6
  77. package/dist/DrawerTitle-es.js +3 -7
  78. package/dist/FieldDescription-cjs.js +184 -181
  79. package/dist/FieldDescription-es.js +187 -184
  80. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +0 -2
  81. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +0 -2
  82. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +0 -2
  83. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +0 -2
  84. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +0 -2
  85. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +0 -2
  86. package/dist/FilterPicker/index.cjs +0 -2
  87. package/dist/FilterPicker/index.mjs +0 -2
  88. package/dist/FilterPickerContent-cjs.js +4 -5
  89. package/dist/FilterPickerContent-es.js +1 -2
  90. package/dist/FormatFile/index.cjs +0 -2
  91. package/dist/FormatFile/index.mjs +0 -2
  92. package/dist/Gallery/index.cjs +0 -2
  93. package/dist/Gallery/index.mjs +0 -2
  94. package/dist/InputDate/index.cjs +0 -2
  95. package/dist/InputDate/index.mjs +0 -2
  96. package/dist/InputNumber/index.cjs +2 -2
  97. package/dist/InputNumber/index.mjs +2 -2
  98. package/dist/InputNumber/types.d.ts +5 -0
  99. package/dist/InputNumber-cjs.js +2 -2
  100. package/dist/InputNumber-es.js +2 -2
  101. package/dist/LightBox/index.cjs +0 -2
  102. package/dist/LightBox/index.mjs +0 -2
  103. package/dist/Menu/index.cjs +2 -2
  104. package/dist/Menu/index.mjs +2 -2
  105. package/dist/Menu-cjs.js +4 -5
  106. package/dist/Menu-es.js +1 -2
  107. package/dist/MenuSubmenuTrigger-cjs.js +206 -164
  108. package/dist/MenuSubmenuTrigger-es.js +194 -152
  109. package/dist/Modal/index.cjs +0 -2
  110. package/dist/Modal/index.mjs +1 -3
  111. package/dist/NumberFieldInput-cjs.js +187 -308
  112. package/dist/NumberFieldInput-es.js +187 -308
  113. package/dist/Page/index.cjs +2 -2
  114. package/dist/Page/index.mjs +2 -2
  115. package/dist/Popover/index.cjs +0 -2
  116. package/dist/Popover/index.mjs +0 -2
  117. package/dist/Popover-cjs.js +7 -8
  118. package/dist/Popover-es.js +1 -2
  119. package/dist/ScrollAreaViewport-cjs.js +747 -1093
  120. package/dist/ScrollAreaViewport-es.js +734 -1081
  121. package/dist/Select/index.cjs +2 -2
  122. package/dist/Select/index.mjs +2 -2
  123. package/dist/SelectGroupLabel-cjs.js +207 -306
  124. package/dist/SelectGroupLabel-es.js +203 -302
  125. package/dist/SelectPrimitive-cjs.js +11 -17
  126. package/dist/SelectPrimitive-es.js +1 -7
  127. package/dist/Tooltip/index.cjs +0 -2
  128. package/dist/Tooltip/index.mjs +0 -2
  129. package/dist/Tooltip-cjs.js +6 -7
  130. package/dist/Tooltip-es.js +1 -2
  131. package/dist/buttonRenderAdapter-cjs.js +2 -1
  132. package/dist/buttonRenderAdapter-es.js +2 -1
  133. package/dist/dialogReturnFocus-cjs.js +1 -1
  134. package/dist/dialogReturnFocus-es.js +2 -2
  135. package/dist/docs/InputNumber/InputNumber.md +2 -2
  136. package/dist/docs/SelectPrimitive/SelectPrimitive.md +1 -1
  137. package/dist/floating-ui.react-cjs.js +2557 -185
  138. package/dist/floating-ui.react-es.js +2512 -149
  139. package/dist/getMappedBreakpointWidth-cjs.js +21 -0
  140. package/dist/getMappedBreakpointWidth-es.js +18 -0
  141. package/dist/index.cjs +15 -10
  142. package/dist/index.d.mts +1 -0
  143. package/dist/index.d.ts +1 -0
  144. package/dist/index.mjs +14 -10
  145. package/dist/mergeClassName-cjs.js +12 -0
  146. package/dist/mergeClassName-es.js +10 -0
  147. package/dist/primitives/BottomSheet/index.cjs +1 -1
  148. package/dist/primitives/BottomSheet/index.mjs +1 -1
  149. package/dist/primitives/ComboboxPrimitive/ComboboxPrimitive.d.ts +2 -2
  150. package/dist/primitives/ComboboxPrimitive/index.cjs +4 -5
  151. package/dist/primitives/ComboboxPrimitive/index.mjs +4 -5
  152. package/dist/primitives/SelectPrimitive/index.cjs +1 -2
  153. package/dist/primitives/SelectPrimitive/index.mjs +1 -2
  154. package/dist/primitives/index.cjs +3 -3
  155. package/dist/primitives/index.mjs +3 -3
  156. package/dist/scrollEdges-cjs.js +1 -11
  157. package/dist/scrollEdges-es.js +1 -11
  158. package/dist/scrollable-cjs.js +58 -0
  159. package/dist/scrollable-es.js +54 -0
  160. package/dist/styles.css +759 -42
  161. package/dist/unstyledPrimitives/index.cjs +720 -478
  162. package/dist/unstyledPrimitives/index.mjs +663 -421
  163. package/dist/useCompositeListItem-cjs.js +2108 -361
  164. package/dist/useCompositeListItem-es.js +2076 -331
  165. package/dist/useLabel-cjs.js +2 -3
  166. package/dist/useLabel-es.js +2 -3
  167. package/dist/useRenderElement-cjs.js +9 -7
  168. package/dist/useRenderElement-es.js +9 -7
  169. package/dist/useScrollLock-cjs.js +819 -344
  170. package/dist/useScrollLock-es.js +732 -296
  171. package/dist/useScrollToActive-cjs.js +4 -5
  172. package/dist/useScrollToActive-es.js +1 -2
  173. package/dist/utils/meta/meta.json +43 -1
  174. package/package.json +8 -3
  175. package/dist/floating-ui.react-dom-cjs.js +0 -2103
  176. package/dist/floating-ui.react-dom-es.js +0 -2072
  177. package/dist/floating-ui.utils-cjs.js +0 -342
  178. package/dist/floating-ui.utils-es.js +0 -300
@@ -5,7 +5,6 @@ var useScrollLock = require('./useScrollLock-cjs.js');
5
5
  var useRenderElement = require('./useRenderElement-cjs.js');
6
6
  var useCompositeListItem = require('./useCompositeListItem-cjs.js');
7
7
  var jsxRuntime = require('react/jsx-runtime');
8
- var floatingUi_utils = require('./floating-ui.utils-cjs.js');
9
8
  var stringifyLocale = require('./stringifyLocale-cjs.js');
10
9
  var scrollEdges = require('./scrollEdges-cjs.js');
11
10
 
@@ -75,121 +74,112 @@ const stateAttributesMapping = {
75
74
  ...useCompositeListItem.fieldValidityMapping
76
75
  };
77
76
 
78
- const HAN_NUMERALS = ['零', '〇', '一', '二', '三', '四', '', '六', '', '八', '九'];
79
- // Map Han numeral characters to ASCII digits. Includes both forms of zero.
80
- const HAN_NUMERAL_TO_DIGIT = {
81
- 零: '0',
82
- 〇: '0',
83
- 一: '1',
84
- 二: '2',
85
- 三: '3',
86
- 四: '4',
87
- 五: '5',
88
- 六: '6',
89
- 七: '7',
90
- 八: '8',
91
- 九: '9'
92
- };
93
- const ARABIC_NUMERALS = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
94
- const PERSIAN_NUMERALS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
95
- const FULLWIDTH_NUMERALS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
77
+ // Han numerals in digit order, with both zero forms first ('' at 0, '' at 1), so a
78
+ // character's digit value is `max(indexOf - 1, 0)`.
79
+ const HAN_NUMERALS = '零〇一二三四五六七八九';
80
+
81
+ // Arabic-Indic (U+0660–0669), Persian (U+06F0–06F9), and fullwidth (U+FF10–FF19) digits are
82
+ // contiguous ranges whose bases are divisible by 16, so `charCode % 16` is the digit value for
83
+ // all three systems.
84
+ const NON_ASCII_DIGIT_RE = /[٠-٩۰-۹0-9]/g;
85
+ const HAN_RE = /[零〇一二三四五六七八九]/g;
96
86
  const PERCENTAGES = ['%', '٪', '%', '﹪'];
97
87
  const PERMILLE = ['‰', '؉'];
98
- const UNICODE_MINUS_SIGNS = ['−', '-', '‒', '–', '—', '﹣'];
99
- const UNICODE_PLUS_SIGNS = ['+', '﹢'];
100
88
 
101
89
  // Fullwidth punctuation common in CJK inputs
102
90
  const FULLWIDTH_DECIMAL = '.'; // U+FF0E
103
91
  const FULLWIDTH_GROUP = ','; // U+FF0C
104
92
 
105
- const ARABIC_RE = new RegExp(`[${ARABIC_NUMERALS.join('')}]`, 'g');
106
- const PERSIAN_RE = new RegExp(`[${PERSIAN_NUMERALS.join('')}]`, 'g');
107
- const FULLWIDTH_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`, 'g');
108
- const HAN_RE = new RegExp(`[${HAN_NUMERALS.join('')}]`, 'g');
109
- const PERCENT_RE = new RegExp(`[${PERCENTAGES.join('')}]`);
110
- const PERMILLE_RE = new RegExp(`[${PERMILLE.join('')}]`);
111
- const PERCENT_GLOBAL_RE = new RegExp(PERCENT_RE.source, 'g');
112
- const PERMILLE_GLOBAL_RE = new RegExp(PERMILLE_RE.source, 'g');
113
-
114
- // Detection regexes (non-global to avoid lastIndex side effects), derived from the numeral arrays
115
- // so they can't drift out of sync.
116
- const ARABIC_DETECT_RE = new RegExp(`[${ARABIC_NUMERALS.join('')}]`);
117
- const PERSIAN_DETECT_RE = new RegExp(`[${PERSIAN_NUMERALS.join('')}]`);
118
- const HAN_DETECT_RE = new RegExp(`[${HAN_NUMERALS.join('')}]`);
119
- const FULLWIDTH_DETECT_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`);
93
+ const PERCENT_RE = /[%٪%﹪]/;
94
+ const PERMILLE_RE = /[‰؉]/;
95
+ const PERCENT_GLOBAL_RE = /[%٪%﹪]/g;
96
+ const PERMILLE_GLOBAL_RE = /[‰؉]/g;
97
+
98
+ // Detection regexes (non-global to avoid lastIndex side effects). Arabic-Indic and Persian
99
+ // digits share one regex because both resolve to the `ar` locale heuristic.
100
+ const ARABIC_PERSIAN_DETECT_RE = /[٠-٩۰-۹]/;
101
+ const HAN_DETECT_RE = /[零〇一二三四五六七八九]/;
102
+ const ANY_NUMERAL_DETECT_RE = /[0-9٠-٩۰-۹0-9零〇一二三四五六七八九]/;
120
103
 
121
104
  // Whether the character is a digit in any numeral system the field accepts.
122
105
  function isNumeralChar(char) {
123
- 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);
106
+ return ANY_NUMERAL_DETECT_RE.test(char);
124
107
  }
125
108
  const BASE_NON_NUMERIC_SYMBOLS = ['.', ',', FULLWIDTH_DECIMAL, FULLWIDTH_GROUP, '٫', '٬'];
126
109
  const SPACE_SEPARATOR_RE = /\p{Zs}/u;
127
- const PLUS_SIGNS_WITH_ASCII = ['+', ...UNICODE_PLUS_SIGNS];
128
- const MINUS_SIGNS_WITH_ASCII = ['-', ...UNICODE_MINUS_SIGNS];
110
+ // Format/bidi control characters (e.g. the LRM/ALM marks RTL locales insert around exponent and
111
+ // currency signs). `parseNumber` strips these, so input validation must treat them as ignorable
112
+ // rather than rejecting the typed string. Non-global so it's safe for repeated `.test(char)`.
113
+ const FORMAT_CONTROL_DETECT_RE = /\p{Cf}/u;
114
+ const FORMAT_CONTROL_GLOBAL_RE = /\p{Cf}/gu;
115
+ const PLUS_SIGNS_WITH_ASCII = ['+', '+', '﹢'];
116
+ const MINUS_SIGNS_WITH_ASCII = ['-', '−', '-', '‒', '–', '—', '﹣'];
129
117
  const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
130
- const escapeClassChar = s => s.replace(/[-\\\]^]/g, m => `\\${m}`); // escape for use inside [...]
131
-
132
118
  function shiftDecimal(value, exponentDelta) {
133
119
  const [coefficient, exponent = '0'] = String(value).split('e');
134
120
  return Number(`${coefficient}e${Number(exponent) + exponentDelta}`);
135
121
  }
136
- const charClassFrom = chars => `[${chars.map(escapeClassChar).join('')}]`;
137
- const ANY_MINUS_CLASS = charClassFrom(['-'].concat(UNICODE_MINUS_SIGNS));
138
- const ANY_PLUS_CLASS = charClassFrom(['+'].concat(UNICODE_PLUS_SIGNS));
139
- const ANY_MINUS_RE = new RegExp(ANY_MINUS_CLASS, 'gu');
140
- const ANY_PLUS_RE = new RegExp(ANY_PLUS_CLASS, 'gu');
141
- const ANY_MINUS_DETECT_RE = new RegExp(ANY_MINUS_CLASS);
142
- const ANY_PLUS_DETECT_RE = new RegExp(ANY_PLUS_CLASS);
122
+ const ANY_MINUS_RE = /[-−-‒–—﹣]/gu;
123
+ const ANY_PLUS_RE = /[++﹢]/gu;
124
+ const ANY_MINUS_DETECT_RE = /[-−-‒–—﹣]/;
125
+ const ANY_PLUS_DETECT_RE = /[++﹢]/;
126
+
127
+ // A representative value with a grouping separator and a fractional part, so that the formatter
128
+ // emits every locale-specific part (group, decimal, currency, unit, literal, exponent, …). Shared
129
+ // so the locale-detail and allowed-character derivations enumerate the same parts.
130
+ const SAMPLE_FORMAT_NUMBER = 11111.1;
131
+
132
+ /**
133
+ * Returns the `Intl.NumberFormat` parts of a representative number, which surface every
134
+ * non-numeric symbol a given locale/format renders.
135
+ */
136
+ function getFormatParts(locale, options) {
137
+ return getFormatter(locale, options).formatToParts(SAMPLE_FORMAT_NUMBER);
138
+ }
143
139
  function getNumberLocaleDetails(locale, options) {
144
- const parts = getFormatter(locale, options).formatToParts(11111.1);
140
+ const parts = getFormatParts(locale, options);
145
141
  const result = {};
146
142
  parts.forEach(part => {
147
143
  result[part.type] = part.value;
148
144
  });
149
145
 
150
146
  // The formatting options may omit the decimal separator (e.g. integer formats), so resolve it
151
- // from the plain locale formatter. This overrides any options-derived decimal too, which is
152
- // safe because the separator is locale-determined and identical across format styles.
147
+ // from the plain locale formatter, which renders one for every locale and numbering system. This
148
+ // overrides any options-derived decimal too, which is safe because the separator is
149
+ // locale-determined and identical across format styles. The ASCII seed is a formality to keep
150
+ // the separator non-optional for callers; it is always replaced below.
151
+ let decimal = '.';
153
152
  getFormatter(locale).formatToParts(0.1).forEach(part => {
154
153
  if (part.type === 'decimal') {
155
- result[part.type] = part.value;
154
+ decimal = part.value;
156
155
  }
157
156
  });
158
- return result;
157
+ return {
158
+ ...result,
159
+ decimal
160
+ };
159
161
  }
160
162
  function parseNumber(formattedNumber, locale, options) {
161
- if (formattedNumber == null) {
162
- return null;
163
- }
164
-
165
163
  // Normalize control characters and whitespace; remove bidi/format controls
166
- let input = String(formattedNumber).replace(/\p{Cf}/gu, '').trim();
164
+ let input = formattedNumber.replace(FORMAT_CONTROL_GLOBAL_RE, '').trim();
167
165
 
168
166
  // Normalize unicode minus/plus to ASCII, handle leading/trailing signs
169
167
  input = input.replace(ANY_MINUS_RE, '-').replace(ANY_PLUS_RE, '+');
170
168
  let isNegative = false;
171
169
 
172
- // Trailing sign, e.g. "1234-" / "1234+"
173
- const trailing = input.match(/([+-])\s*$/);
174
- if (trailing) {
175
- if (trailing[1] === '-') {
170
+ // Strips a matched sign (leading "-12" / trailing "1234-") while recording negativity.
171
+ const takeSign = (match, sign) => {
172
+ if (sign === '-') {
176
173
  isNegative = true;
177
174
  }
178
- input = input.replace(/([+-])\s*$/, '');
179
- }
180
- // Leading sign
181
- const leading = input.match(/^\s*([+-])/);
182
- if (leading) {
183
- if (leading[1] === '-') {
184
- isNegative = true;
185
- }
186
- input = input.replace(/^\s*[+-]/, '');
187
- }
175
+ return '';
176
+ };
177
+ input = input.replace(/([+-])\s*$/, takeSign).replace(/^\s*([+-])/, takeSign);
188
178
 
189
179
  // Heuristic locale detection
190
180
  let computedLocale = locale;
191
181
  if (computedLocale === undefined) {
192
- if (ARABIC_DETECT_RE.test(input) || PERSIAN_DETECT_RE.test(input)) {
182
+ if (ARABIC_PERSIAN_DETECT_RE.test(input)) {
193
183
  computedLocale = 'ar';
194
184
  } else if (HAN_DETECT_RE.test(input)) {
195
185
  computedLocale = 'zh';
@@ -221,70 +211,17 @@ function parseNumber(formattedNumber, locale, options) {
221
211
  groupRegex = new RegExp(escapeRegExp(group), 'g');
222
212
  }
223
213
  }
224
- const replacements = [{
225
- regex: groupRegex,
226
- replacement: ''
227
- }, {
228
- regex: decimal ? new RegExp(escapeRegExp(decimal), 'g') : null,
229
- replacement: '.'
230
- },
231
- // Fullwidth punctuation
232
- {
233
- regex: /./g,
234
- replacement: '.'
235
- },
236
- // FULLWIDTH_DECIMAL
237
- {
238
- regex: /,/g,
239
- replacement: ''
240
- },
241
- // FULLWIDTH_GROUP
242
- // Arabic punctuation
243
- {
244
- regex: /٫/g,
245
- replacement: '.'
246
- },
247
- // ARABIC DECIMAL SEPARATOR (U+066B)
248
- {
249
- regex: /٬/g,
250
- replacement: ''
251
- },
252
- // ARABIC THOUSANDS SEPARATOR (U+066C)
214
+ const replacements = [[groupRegex, ''], [new RegExp(escapeRegExp(decimal), 'g'), '.'],
215
+ // Fullwidth/Arabic punctuation
216
+ [/[.٫]/g, '.'],
217
+ // FULLWIDTH_DECIMAL, ARABIC DECIMAL SEPARATOR (U+066B)
218
+ [/[,٬]/g, ''],
219
+ // FULLWIDTH_GROUP, ARABIC THOUSANDS SEPARATOR (U+066C)
253
220
  // Currency & unit labels
254
- {
255
- regex: currency ? new RegExp(escapeRegExp(currency), 'g') : null,
256
- replacement: ''
257
- }, {
258
- regex: unitRegex,
259
- replacement: ''
260
- }, {
261
- regex: PERCENT_GLOBAL_RE,
262
- replacement: ''
263
- }, {
264
- regex: PERMILLE_GLOBAL_RE,
265
- replacement: ''
266
- }, {
267
- regex: exponentSeparator ? new RegExp(escapeRegExp(exponentSeparator), 'g') : null,
268
- replacement: 'e'
269
- },
221
+ [currency ? new RegExp(escapeRegExp(currency), 'g') : null, ''], [unitRegex, ''], [PERCENT_GLOBAL_RE, ''], [PERMILLE_GLOBAL_RE, ''], [exponentSeparator ? new RegExp(escapeRegExp(exponentSeparator), 'g') : null, 'e'],
270
222
  // Numeral systems to ASCII digits
271
- {
272
- regex: ARABIC_RE,
273
- replacement: ch => String(ARABIC_NUMERALS.indexOf(ch))
274
- }, {
275
- regex: PERSIAN_RE,
276
- replacement: ch => String(PERSIAN_NUMERALS.indexOf(ch))
277
- }, {
278
- regex: FULLWIDTH_RE,
279
- replacement: ch => String(FULLWIDTH_NUMERALS.indexOf(ch))
280
- }, {
281
- regex: HAN_RE,
282
- replacement: ch => HAN_NUMERAL_TO_DIGIT[ch]
283
- }];
284
- let unformatted = replacements.reduce((acc, {
285
- regex,
286
- replacement
287
- }) => {
223
+ [NON_ASCII_DIGIT_RE, ch => String(ch.charCodeAt(0) % 16)], [HAN_RE, ch => String(Math.max(HAN_NUMERALS.indexOf(ch) - 1, 0))]];
224
+ let unformatted = replacements.reduce((acc, [regex, replacement]) => {
288
225
  return regex ? acc.replace(regex, replacement) : acc;
289
226
  }, input);
290
227
 
@@ -362,27 +299,18 @@ function removeFloatingPointErrors(value, format) {
362
299
  }
363
300
  return formatter.format(roundedValue) === roundedText ? roundedValue : value;
364
301
  }
365
- function snapToStep(value, base, step, mode = 'directional') {
302
+ function snapToStep(value, base, step, nearest) {
366
303
  const stepSize = Math.abs(step);
367
304
  const direction = Math.sign(step);
368
305
  const tolerance = stepSize * STEP_EPSILON_FACTOR * direction;
369
306
  const rawSteps = value - base + tolerance;
370
- if (mode === 'nearest') {
307
+ if (nearest) {
371
308
  return base + Math.round(rawSteps / step) * step;
372
309
  }
373
310
  const snappedSteps = direction > 0 ? Math.floor(rawSteps / stepSize) : Math.ceil(rawSteps / stepSize);
374
311
  return base + snappedSteps * stepSize;
375
312
  }
376
- function toValidatedNumber(value, {
377
- step,
378
- minWithDefault,
379
- maxWithDefault,
380
- minWithZeroDefault,
381
- format,
382
- snapOnStep,
383
- small,
384
- clamp: shouldClamp
385
- }) {
313
+ function toValidatedNumber(value, step, minWithDefault, maxWithDefault, minWithZeroDefault, format, snapOnStep, small, shouldClamp) {
386
314
  if (value === null) {
387
315
  return value;
388
316
  }
@@ -390,8 +318,9 @@ function toValidatedNumber(value, {
390
318
  if (step != null && snapOnStep && step !== 0) {
391
319
  const base = small || minWithDefault === Number.MIN_SAFE_INTEGER ? minWithZeroDefault : minWithDefault;
392
320
 
393
- // Snap before clamping so non-step-aligned boundaries stay reachable.
394
- nextValue = snapToStep(nextValue, base, step, small ? 'nearest' : 'directional');
321
+ // Snap before clamping so non-step-aligned boundaries stay reachable. Small (alt-key) steps
322
+ // snap to the nearest multiple; regular steps snap directionally.
323
+ nextValue = snapToStep(nextValue, base, step, small);
395
324
  }
396
325
 
397
326
  // Clamp before rounding so a value just outside a fractional boundary (e.g. `0.4` with
@@ -446,7 +375,6 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
446
375
  validityData,
447
376
  disabled: fieldDisabled,
448
377
  setFilled,
449
- invalid,
450
378
  name: fieldName,
451
379
  state: fieldState,
452
380
  validation
@@ -495,48 +423,46 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
495
423
  const [inputValue, setInputValue] = React__namespace.useState(() => formatNumber(value, locale, format));
496
424
  const [inputMode, setInputMode] = React__namespace.useState('numeric');
497
425
  const getAllowedNonNumericKeys = useScrollLock.useStableCallback(() => {
498
- const {
499
- decimal,
500
- group,
501
- currency,
502
- literal
503
- } = getNumberLocaleDetails(locale, format);
504
- const keys = new Set();
505
- BASE_NON_NUMERIC_SYMBOLS.forEach(symbol => keys.add(symbol));
506
- if (decimal) {
507
- keys.add(decimal);
508
- }
509
- if (group) {
510
- keys.add(group);
511
- if (SPACE_SEPARATOR_RE.test(group)) {
426
+ const parts = getFormatParts(locale, format);
427
+ const keys = new Set(BASE_NON_NUMERIC_SYMBOLS);
428
+ const addAll = chars => chars.forEach(char => keys.add(char));
429
+
430
+ // Integer formats omit the decimal from `parts`, so fall back to the locale's separator in that
431
+ // case; it must stay typeable regardless of whether the format renders a fraction.
432
+ const decimal = parts.find(part => part.type === 'decimal')?.value ?? getNumberLocaleDetails(locale, format).decimal;
433
+ keys.add(decimal);
434
+
435
+ // Allow every non-digit character the formatter renders — separators, currency symbols, units
436
+ // (e.g. `km/h`, `°C`), exponent separators, and locale literals — decomposed per character
437
+ // because the input validates the typed string one character at a time. Deriving these from
438
+ // the formatter covers multi-character and locale-specific symbols of every part type
439
+ // uniformly. `compact` suffixes (e.g. `K`/`M`) are excluded because `parseNumber` can't reverse
440
+ // them, so allowing them would yield a silently incorrect value.
441
+ parts.forEach(part => {
442
+ if (part.type === 'integer' || part.type === 'fraction' || part.type === 'exponentInteger' || part.type === 'compact') {
443
+ return;
444
+ }
445
+ addAll(Array.from(part.value));
446
+ if (SPACE_SEPARATOR_RE.test(part.value)) {
512
447
  keys.add(' ');
513
448
  }
514
- }
449
+ });
515
450
  const allowPercentSymbols = formatStyle === 'percent' || formatStyle === 'unit' && format?.unit === 'percent';
516
451
  const allowPermilleSymbols = formatStyle === 'percent' || formatStyle === 'unit' && format?.unit === 'permille';
452
+
453
+ // Tolerate percent/permille variants the formatter doesn't emit but users may type or paste.
517
454
  if (allowPercentSymbols) {
518
- PERCENTAGES.forEach(key => keys.add(key));
455
+ addAll(PERCENTAGES);
519
456
  }
520
457
  if (allowPermilleSymbols) {
521
- PERMILLE.forEach(key => keys.add(key));
522
- }
523
- if (formatStyle === 'currency' && currency) {
524
- keys.add(currency);
525
- }
526
- if (literal) {
527
- // Some locales (e.g. de-DE) insert a literal space character between the number
528
- // and the symbol, so allow those characters to be typed/removed.
529
- Array.from(literal).forEach(char => keys.add(char));
530
- if (SPACE_SEPARATOR_RE.test(literal)) {
531
- keys.add(' ');
532
- }
458
+ addAll(PERMILLE);
533
459
  }
534
460
 
535
461
  // Allow plus sign in all cases; minus sign when negatives are valid, or when out-of-range
536
462
  // entry is allowed so native underflow validation can be triggered from the keyboard.
537
- PLUS_SIGNS_WITH_ASCII.forEach(key => keys.add(key));
463
+ addAll(PLUS_SIGNS_WITH_ASCII);
538
464
  if (minWithDefault < 0 || allowOutOfRange) {
539
- MINUS_SIGNS_WITH_ASCII.forEach(key => keys.add(key));
465
+ addAll(MINUS_SIGNS_WITH_ASCII);
540
466
  }
541
467
  return keys;
542
468
  });
@@ -554,21 +480,13 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
554
480
  const dir = details.direction;
555
481
 
556
482
  // Direct text entry (typing, pasting, clearing, autofill) behaves natively; step-based
557
- // interactions (keyboard arrows, buttons, wheel, scrub) do not.
558
- const isInputReason = details.reason === useScrollLock.inputChange || details.reason === useScrollLock.inputClear || details.reason === useScrollLock.inputBlur || details.reason === useScrollLock.inputPaste || details.reason === useScrollLock.none;
483
+ // interactions (keyboard arrows, buttons, wheel, scrub) do not. All direct-entry reasons
484
+ // (`input-change`, `input-clear`, `input-blur`, `input-paste`) share the `input-` prefix.
485
+ const isInputReason = details.reason.startsWith('input-') || details.reason === useScrollLock.none;
559
486
 
560
487
  // Only allow out-of-range values for direct text entry. Step-based interactions still clamp.
561
488
  const shouldClampValue = !allowOutOfRange || !isInputReason;
562
- const validatedValue = toValidatedNumber(unvalidatedValue, {
563
- step: dir ? getStepAmount(eventWithOptionalKeyState) * dir : undefined,
564
- format: formatOptionsRef.current,
565
- minWithDefault,
566
- maxWithDefault,
567
- minWithZeroDefault,
568
- snapOnStep,
569
- small: eventWithOptionalKeyState?.altKey ?? false,
570
- clamp: shouldClampValue
571
- });
489
+ const validatedValue = toValidatedNumber(unvalidatedValue, dir ? getStepAmount(eventWithOptionalKeyState) * dir : undefined, minWithDefault, maxWithDefault, minWithZeroDefault, formatOptionsRef.current, snapOnStep, eventWithOptionalKeyState?.altKey ?? false, shouldClampValue);
572
490
 
573
491
  // Notify about a change even when the numeric value is unchanged for input reasons: the
574
492
  // typed text may clamp/snap to the current value, or differ while validation normalizes
@@ -678,10 +596,10 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
678
596
  const changed = incrementValue(amount, {
679
597
  direction: event.deltaY > 0 ? -1 : 1,
680
598
  event,
681
- reason: 'wheel'
599
+ reason: useScrollLock.wheel
682
600
  });
683
601
  if (changed) {
684
- onValueCommitted(lastChangedValueRef.current ?? valueRef.current, useScrollLock.createGenericEventDetails(useScrollLock.wheel, event));
602
+ onValueCommitted(lastChangedValueRef.current, useScrollLock.createGenericEventDetails(useScrollLock.wheel, event));
685
603
  }
686
604
  }
687
605
  return useScrollLock.addEventListener(element, 'wheel', handleWheel);
@@ -697,12 +615,8 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
697
615
  }), [fieldState, disabled, readOnly, required, value, inputValue, isScrubbing]);
698
616
  const contextValue = React__namespace.useMemo(() => ({
699
617
  inputRef,
700
- inputValue,
701
- value,
702
618
  minWithDefault,
703
619
  maxWithDefault,
704
- disabled,
705
- readOnly,
706
620
  id,
707
621
  setValue,
708
622
  incrementValue,
@@ -714,19 +628,16 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
714
628
  hasPendingCommitRef,
715
629
  name,
716
630
  nameProp,
717
- required,
718
- invalid,
719
631
  inputMode,
720
632
  getAllowedNonNumericKeys,
721
633
  min,
722
634
  max,
723
635
  setInputValue,
724
636
  locale,
725
- isScrubbing,
726
637
  setIsScrubbing,
727
638
  state,
728
639
  onValueCommitted
729
- }), [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]);
640
+ }), [inputRef, minWithDefault, maxWithDefault, id, setValue, incrementValue, getStepAmount, formatOptionsRef, valueRef, name, nameProp, inputMode, getAllowedNonNumericKeys, min, max, setInputValue, locale, state, onValueCommitted]);
730
641
  const element = useRenderElement.useRenderElement('div', componentProps, {
731
642
  ref: forwardedRef,
732
643
  state,
@@ -741,7 +652,7 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
741
652
  inputRef.current?.focus();
742
653
  },
743
654
  onChange(event) {
744
- // Workaround for https://github.com/facebook/react/issues/9023
655
+ // Workaround for https://github.com/react/react/issues/9023
745
656
  if (event.nativeEvent.defaultPrevented || disabled || readOnly) {
746
657
  return;
747
658
  }
@@ -766,7 +677,7 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
766
677
  min: min,
767
678
  max: max
768
679
  // stepMismatch validation is broken unless an explicit `min` is added.
769
- // See https://github.com/facebook/react/issues/12334.
680
+ // See https://github.com/react/react/issues/12334.
770
681
  ,
771
682
  step: stepProp,
772
683
  disabled: disabled,
@@ -863,7 +774,6 @@ function isTouchLikePointerType(pointerType) {
863
774
  function usePressAndHold(params) {
864
775
  const {
865
776
  disabled,
866
- readOnly = false,
867
777
  tick,
868
778
  onStop,
869
779
  tickDelay = DEFAULT_TICK_DELAY,
@@ -898,7 +808,7 @@ function usePressAndHold(params) {
898
808
  if (!element) {
899
809
  return;
900
810
  }
901
- const win = floatingUi_utils.getWindow(element);
811
+ const win = useScrollLock.getWindow(element);
902
812
  function handleContextMenu(event) {
903
813
  event.preventDefault();
904
814
  }
@@ -943,8 +853,7 @@ function usePressAndHold(params) {
943
853
  isTouchingButtonRef.current = false;
944
854
  },
945
855
  onPointerDown(event) {
946
- const isMainButton = !event.button || event.button === 0;
947
- if (event.defaultPrevented || !isMainButton || disabled || readOnly) {
856
+ if (event.defaultPrevented || event.button || disabled) {
948
857
  return;
949
858
  }
950
859
  pointerTypeRef.current = event.pointerType;
@@ -987,12 +896,10 @@ function usePressAndHold(params) {
987
896
  }
988
897
  },
989
898
  onPointerMove(event) {
990
- if (disabled || readOnly || !isTouchLikePointerType(event.pointerType) || !isPressedRef.current) {
899
+ if (disabled || !isTouchLikePointerType(event.pointerType) || !isPressedRef.current) {
991
900
  return;
992
901
  }
993
- if (movesAfterTouchRef.current != null) {
994
- movesAfterTouchRef.current += 1;
995
- }
902
+ movesAfterTouchRef.current += 1;
996
903
  const {
997
904
  x,
998
905
  y
@@ -1004,7 +911,7 @@ function usePressAndHold(params) {
1004
911
  }
1005
912
  },
1006
913
  onMouseEnter(event) {
1007
- if (event.defaultPrevented || disabled || readOnly || !isPressedRef.current || isTouchingButtonRef.current || isTouchLikePointerType(pointerTypeRef.current)) {
914
+ if (event.defaultPrevented || disabled || !isPressedRef.current || isTouchingButtonRef.current || isTouchLikePointerType(pointerTypeRef.current)) {
1008
915
  return;
1009
916
  }
1010
917
  startAutoChange(event.nativeEvent);
@@ -1037,32 +944,47 @@ function usePressAndHold(params) {
1037
944
  };
1038
945
  }
1039
946
 
1040
- const CHANGE_VALUE_TICK_DELAY = 60;
1041
- const START_AUTO_CHANGE_DELAY = 400;
1042
- const SCROLLING_POINTER_MOVE_DISTANCE = 8;
1043
-
1044
947
  const SELECT_NONE_STYLE = {
1045
948
  WebkitUserSelect: 'none',
1046
949
  userSelect: 'none'
1047
950
  };
1048
- function useNumberFieldButton(params) {
951
+ /**
952
+ * Shared implementation for the increment and decrement stepper buttons. They differ only in the
953
+ * direction they step and the boundary (`max` vs `min`) at which they become disabled.
954
+ */
955
+ function useNumberFieldStepperButton(componentProps, forwardedRef, isIncrement) {
956
+ const {
957
+ render,
958
+ className,
959
+ disabled: disabledProp = false,
960
+ nativeButton = true,
961
+ style,
962
+ ...elementProps
963
+ } = componentProps;
1049
964
  const {
1050
965
  allowInputSyncRef,
1051
- disabled,
1052
966
  formatOptionsRef,
1053
967
  getStepAmount,
1054
968
  id,
1055
969
  incrementValue,
1056
970
  inputRef,
1057
- inputValue,
1058
- isIncrement,
1059
- locale,
1060
- readOnly,
971
+ maxWithDefault,
972
+ minWithDefault,
1061
973
  setValue,
974
+ state,
1062
975
  valueRef,
976
+ locale,
1063
977
  lastChangedValueRef,
1064
978
  onValueCommitted
1065
- } = params;
979
+ } = useNumberFieldRootContext();
980
+ const {
981
+ disabled: contextDisabled,
982
+ readOnly,
983
+ value,
984
+ inputValue
985
+ } = state;
986
+ const isAtBoundary = value != null && (isIncrement ? value >= maxWithDefault : value <= minWithDefault);
987
+ const disabled = disabledProp || contextDisabled || isAtBoundary;
1066
988
  const pressReason = isIncrement ? useScrollLock.incrementPress : useScrollLock.decrementPress;
1067
989
  function commitValue(nativeEvent) {
1068
990
  const shouldCommitInputValue = !allowInputSyncRef.current;
@@ -1098,9 +1020,6 @@ function useNumberFieldButton(params) {
1098
1020
  } = usePressAndHold({
1099
1021
  disabled: disabled || readOnly,
1100
1022
  elementRef: inputRef,
1101
- tickDelay: CHANGE_VALUE_TICK_DELAY,
1102
- startDelay: START_AUTO_CHANGE_DELAY,
1103
- scrollDistance: SCROLLING_POINTER_MOVE_DISTANCE,
1104
1023
  tick(triggerEvent) {
1105
1024
  const amount = getStepAmount(triggerEvent);
1106
1025
  return incrementValue(amount, {
@@ -1145,8 +1064,7 @@ function useNumberFieldButton(params) {
1145
1064
  }
1146
1065
  },
1147
1066
  onPointerDown(event) {
1148
- const isMainButton = !event.button || event.button === 0;
1149
- if (event.defaultPrevented || readOnly || !isMainButton || disabled) {
1067
+ if (event.defaultPrevented || readOnly || event.button || disabled) {
1150
1068
  return;
1151
1069
  }
1152
1070
 
@@ -1162,61 +1080,6 @@ function useNumberFieldButton(params) {
1162
1080
  pointerHandlers.onPointerDown(event);
1163
1081
  }
1164
1082
  };
1165
- return props;
1166
- }
1167
-
1168
- /**
1169
- * Shared implementation for the increment and decrement stepper buttons. They differ only in the
1170
- * direction they step and the boundary (`max` vs `min`) at which they become disabled.
1171
- */
1172
- function useNumberFieldStepperButton(componentProps, forwardedRef, isIncrement) {
1173
- const {
1174
- render,
1175
- className,
1176
- disabled: disabledProp = false,
1177
- nativeButton = true,
1178
- style,
1179
- ...elementProps
1180
- } = componentProps;
1181
- const {
1182
- allowInputSyncRef,
1183
- disabled: contextDisabled,
1184
- formatOptionsRef,
1185
- getStepAmount,
1186
- id,
1187
- incrementValue,
1188
- inputRef,
1189
- inputValue,
1190
- maxWithDefault,
1191
- minWithDefault,
1192
- readOnly,
1193
- setValue,
1194
- state,
1195
- value,
1196
- valueRef,
1197
- locale,
1198
- lastChangedValueRef,
1199
- onValueCommitted
1200
- } = useNumberFieldRootContext();
1201
- const isAtBoundary = value != null && (isIncrement ? value >= maxWithDefault : value <= minWithDefault);
1202
- const disabled = disabledProp || contextDisabled || isAtBoundary;
1203
- const props = useNumberFieldButton({
1204
- isIncrement,
1205
- inputRef,
1206
- inputValue,
1207
- disabled,
1208
- readOnly,
1209
- id,
1210
- setValue,
1211
- getStepAmount,
1212
- incrementValue,
1213
- allowInputSyncRef,
1214
- formatOptionsRef,
1215
- valueRef,
1216
- locale,
1217
- lastChangedValueRef,
1218
- onValueCommitted
1219
- });
1220
1083
  const {
1221
1084
  getButtonProps,
1222
1085
  buttonRef
@@ -1279,31 +1142,33 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1279
1142
  } = componentProps;
1280
1143
  const {
1281
1144
  allowInputSyncRef,
1282
- disabled,
1283
1145
  formatOptionsRef,
1284
1146
  getAllowedNonNumericKeys,
1285
1147
  getStepAmount,
1286
1148
  id,
1287
1149
  incrementValue,
1288
1150
  inputMode,
1289
- inputValue,
1290
1151
  max,
1291
1152
  min,
1292
1153
  name,
1293
1154
  nameProp,
1294
- readOnly,
1295
- required,
1296
1155
  setValue,
1297
1156
  state,
1298
1157
  setInputValue,
1299
1158
  locale,
1300
1159
  inputRef,
1301
- value,
1302
1160
  onValueCommitted,
1303
1161
  lastChangedValueRef,
1304
1162
  hasPendingCommitRef,
1305
1163
  valueRef
1306
1164
  } = useNumberFieldRootContext();
1165
+ const {
1166
+ disabled,
1167
+ readOnly,
1168
+ required,
1169
+ value,
1170
+ inputValue
1171
+ } = state;
1307
1172
  const {
1308
1173
  clearErrors
1309
1174
  } = useCompositeListItem.useFormContext();
@@ -1438,7 +1303,7 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1438
1303
  blockRevalidationRef.current = false;
1439
1304
  return;
1440
1305
  }
1441
- committedValue = lastChangedValueRef.current ?? committed;
1306
+ committedValue = lastChangedValueRef.current;
1442
1307
  // If validation normalized back to the current value, `useValueChanged` won't fire to
1443
1308
  // reset the flag, so reset it here or the next external change won't revalidate.
1444
1309
  if (committedValue === value) {
@@ -1459,7 +1324,7 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1459
1324
  }
1460
1325
  },
1461
1326
  onChange(event) {
1462
- // Workaround for https://github.com/facebook/react/issues/9023
1327
+ // Workaround for https://github.com/react/react/issues/9023
1463
1328
  if (event.nativeEvent.defaultPrevented) {
1464
1329
  return;
1465
1330
  }
@@ -1475,7 +1340,10 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1475
1340
  // currently parseable into a number. This preserves good UX for IME
1476
1341
  // composition/partial input while still providing live numeric updates when possible.
1477
1342
  const allowedNonNumericKeys = getAllowedNonNumericKeys();
1478
- const isValidCharacterString = Array.from(targetValue).every(ch => isNumeralChar(ch) || ANY_MINUS_DETECT_RE.test(ch) || allowedNonNumericKeys.has(ch));
1343
+ const isValidCharacterString = Array.from(targetValue).every(ch => isNumeralChar(ch) || ANY_MINUS_DETECT_RE.test(ch) || allowedNonNumericKeys.has(ch) ||
1344
+ // Bidi/format controls are stripped by `parseNumber`; don't let them reject the string
1345
+ // (RTL locales insert them around exponent/currency signs, e.g. scientific notation).
1346
+ FORMAT_CONTROL_DETECT_RE.test(ch));
1479
1347
  if (!isValidCharacterString) {
1480
1348
  return;
1481
1349
  }
@@ -1525,7 +1393,7 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1525
1393
  if (event.key === symbol) {
1526
1394
  const symbolIndex = inputValue.indexOf(symbol);
1527
1395
  const isSymbolHighlighted = selectionContainsIndex(symbolIndex);
1528
- isAllowedNonNumericKey = !inputValue.includes(symbol) || isAllSelected || isSymbolHighlighted;
1396
+ isAllowedNonNumericKey = symbolIndex === -1 || isAllSelected || isSymbolHighlighted;
1529
1397
  }
1530
1398
  });
1531
1399
  const isNavigateKey = NAVIGATE_KEYS.has(event.key);
@@ -1538,12 +1406,18 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1538
1406
  event.which === 229 || event.altKey && !isStepKey || event.ctrlKey || event.metaKey || isAllowedNonNumericKey || isNumeralChar(event.key) || isNavigateKey) {
1539
1407
  return;
1540
1408
  }
1541
- const willSetHome = event.key === 'Home' && min != null;
1542
- const willSetEnd = event.key === 'End' && max != null;
1409
+
1410
+ // Home/End jump to the corresponding bound, but only when that bound is defined.
1411
+ let boundaryValue = null;
1412
+ if (event.key === 'Home' && min != null) {
1413
+ boundaryValue = min;
1414
+ } else if (event.key === 'End' && max != null) {
1415
+ boundaryValue = max;
1416
+ }
1543
1417
 
1544
1418
  // Let the browser handle multi-character keys we don't act on (PageUp, Insert, F-keys,
1545
1419
  // Home/End without min/max); invalid single characters are still blocked below.
1546
- if (event.key.length > 1 && !isStepKey && !willSetHome && !willSetEnd) {
1420
+ if (event.key.length > 1 && !isStepKey && boundaryValue === null) {
1547
1421
  return;
1548
1422
  }
1549
1423
 
@@ -1555,12 +1429,14 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1555
1429
  const amount = getStepAmount(event);
1556
1430
 
1557
1431
  // Prevent insertion of text or caret from moving.
1558
- useScrollLock.stopEvent(event);
1432
+ event.preventDefault();
1433
+ event.stopPropagation();
1559
1434
  const commitDetails = useScrollLock.createGenericEventDetails(useScrollLock.keyboard, nativeEvent);
1560
1435
  let changed = false;
1561
- if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
1436
+ if (isStepKey || boundaryValue !== null) {
1562
1437
  allowInputSyncRef.current = true;
1563
-
1438
+ }
1439
+ if (isStepKey) {
1564
1440
  // When stepping from the synced numeric state, refresh the commit ref to the current
1565
1441
  // value so a canceled step can't commit a stale `lastChangedValueRef` left over from an
1566
1442
  // earlier change (mirrors the button path).
@@ -1573,15 +1449,14 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1573
1449
  event: nativeEvent,
1574
1450
  reason: useScrollLock.keyboard
1575
1451
  });
1576
- } else if (willSetHome) {
1577
- allowInputSyncRef.current = true;
1578
- changed = setValue(min, useScrollLock.createChangeEventDetails(useScrollLock.keyboard, nativeEvent));
1579
- } else if (willSetEnd) {
1580
- allowInputSyncRef.current = true;
1581
- changed = setValue(max, useScrollLock.createChangeEventDetails(useScrollLock.keyboard, nativeEvent));
1452
+ } else if (boundaryValue !== null) {
1453
+ changed = setValue(boundaryValue, useScrollLock.createChangeEventDetails(useScrollLock.keyboard, nativeEvent));
1582
1454
  }
1455
+
1456
+ // `changed` is only true when `setValue` applied the change, which records the stored
1457
+ // (clamped/snapped) value, so commit that rather than the pre-validation input.
1583
1458
  if (changed) {
1584
- onValueCommitted(lastChangedValueRef.current ?? valueRef.current, commitDetails);
1459
+ onValueCommitted(lastChangedValueRef.current, commitDetails);
1585
1460
  }
1586
1461
  },
1587
1462
  onPaste(event) {
@@ -1592,6 +1467,7 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1592
1467
  try {
1593
1468
  pastedData = event.clipboardData?.getData('text/plain') ?? '';
1594
1469
  } catch {
1470
+ /* istanbul ignore else -- `process.env.NODE_ENV` is a build-time constant under test */
1595
1471
  if (process.env.NODE_ENV !== 'production') {
1596
1472
  const ownerStackMessage = useScrollLock.SafeReact.captureOwnerStack?.() || '';
1597
1473
  useRenderElement.warn('<NumberField.Input> could not read clipboard text during paste handling.', ownerStackMessage);
@@ -1604,9 +1480,12 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1604
1480
 
1605
1481
  // Insert the pasted text at the caret/selection instead of replacing the entire value,
1606
1482
  // matching native input behavior (e.g. pasting "5" into "123|" yields "1235").
1483
+ // The component renders `type="text"`, which always reports a selection range. Overriding
1484
+ // `type` with a selection-less one (`email`, `number`) is unsupported either way: the caret
1485
+ // restore above throws on those, so there is no working behavior to preserve here.
1607
1486
  const input = event.currentTarget;
1608
- const selectionStart = input.selectionStart ?? inputValue.length;
1609
- const selectionEnd = input.selectionEnd ?? inputValue.length;
1487
+ const selectionStart = input.selectionStart;
1488
+ const selectionEnd = input.selectionEnd;
1610
1489
  const nextText = inputValue.slice(0, selectionStart) + pastedData + inputValue.slice(selectionEnd);
1611
1490
  const parsedValue = parseNumber(nextText, locale, formatOptionsRef.current);
1612
1491
  if (parsedValue !== null) {