@jobber/components 9.6.0 → 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 (187) hide show
  1. package/dist/ActivityIndicator-cjs.js +1 -0
  2. package/dist/ActivityIndicator-es.js +1 -0
  3. package/dist/Autocomplete/index.cjs +2 -0
  4. package/dist/Autocomplete/index.mjs +2 -0
  5. package/dist/Autocomplete-cjs.js +6 -4
  6. package/dist/Autocomplete-es.js +3 -1
  7. package/dist/Banner-cjs.js +1 -0
  8. package/dist/Banner-es.js +1 -0
  9. package/dist/Card/index.cjs +2 -1
  10. package/dist/Card/index.mjs +2 -1
  11. package/dist/Chip/index.cjs +2 -0
  12. package/dist/Chip/index.mjs +2 -0
  13. package/dist/Chip-cjs.js +1 -0
  14. package/dist/Chip-es.js +1 -0
  15. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +2 -0
  16. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +2 -0
  17. package/dist/Chips/InternalChipDismissible/index.cjs +2 -0
  18. package/dist/Chips/InternalChipDismissible/index.mjs +2 -0
  19. package/dist/Chips/index.cjs +2 -0
  20. package/dist/Chips/index.mjs +2 -0
  21. package/dist/Combobox/index.cjs +4 -2
  22. package/dist/Combobox/index.mjs +4 -2
  23. package/dist/{ComboboxSeparator-cjs.js → ComboboxChipRemove-cjs.js} +455 -501
  24. package/dist/{ComboboxSeparator-es.js → ComboboxChipRemove-es.js} +456 -500
  25. package/dist/ComboboxPrimitive-cjs.js +30 -29
  26. package/dist/ComboboxPrimitive-es.js +3 -2
  27. package/dist/ConfirmationModal/index.cjs +2 -0
  28. package/dist/ConfirmationModal/index.mjs +2 -0
  29. package/dist/ConfirmationModal-cjs.js +1 -0
  30. package/dist/ConfirmationModal-es.js +1 -0
  31. package/dist/DataDump/index.cjs +2 -1
  32. package/dist/DataDump/index.mjs +2 -1
  33. package/dist/DataDump-cjs.js +1 -0
  34. package/dist/DataDump-es.js +1 -0
  35. package/dist/DataList/components/DataListActions/index.cjs +2 -0
  36. package/dist/DataList/components/DataListActions/index.mjs +2 -0
  37. package/dist/DataList/components/DataListBulkActions/index.cjs +2 -0
  38. package/dist/DataList/components/DataListBulkActions/index.mjs +2 -0
  39. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +2 -0
  40. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +2 -0
  41. package/dist/DataList/components/DataListFilters/index.cjs +2 -0
  42. package/dist/DataList/components/DataListFilters/index.mjs +2 -0
  43. package/dist/DataList/components/DataListHeader/index.cjs +2 -0
  44. package/dist/DataList/components/DataListHeader/index.mjs +2 -0
  45. package/dist/DataList/components/DataListItem/index.cjs +2 -0
  46. package/dist/DataList/components/DataListItem/index.mjs +2 -0
  47. package/dist/DataList/components/DataListItemActions/index.cjs +2 -0
  48. package/dist/DataList/components/DataListItemActions/index.mjs +2 -0
  49. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +2 -0
  50. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +2 -0
  51. package/dist/DataList/components/DataListItems/index.cjs +2 -0
  52. package/dist/DataList/components/DataListItems/index.mjs +2 -0
  53. package/dist/DataList/components/DataListLayout/index.cjs +2 -0
  54. package/dist/DataList/components/DataListLayout/index.mjs +2 -0
  55. package/dist/DataList/components/DataListLayoutActions/index.cjs +2 -0
  56. package/dist/DataList/components/DataListLayoutActions/index.mjs +2 -0
  57. package/dist/DataList/index.cjs +2 -0
  58. package/dist/DataList/index.mjs +2 -0
  59. package/dist/DataTable-cjs.js +1 -0
  60. package/dist/DataTable-es.js +1 -0
  61. package/dist/DatePicker/index.cjs +2 -0
  62. package/dist/DatePicker/index.mjs +2 -0
  63. package/dist/DatePicker-cjs.js +8 -7
  64. package/dist/DatePicker-es.js +6 -5
  65. package/dist/Dialog/index.cjs +2 -1
  66. package/dist/Dialog/index.mjs +2 -1
  67. package/dist/DrawerDescription-cjs.js +99 -268
  68. package/dist/DrawerDescription-es.js +103 -270
  69. package/dist/DrawerGrid-cjs.js +1 -0
  70. package/dist/DrawerGrid-es.js +1 -0
  71. package/dist/DrawerTitle-cjs.js +6 -2
  72. package/dist/DrawerTitle-es.js +7 -3
  73. package/dist/FeatureSwitch-cjs.js +1 -0
  74. package/dist/FeatureSwitch-es.js +1 -0
  75. package/dist/FieldDescription-cjs.js +181 -184
  76. package/dist/FieldDescription-es.js +184 -187
  77. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +2 -0
  78. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +2 -0
  79. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +2 -0
  80. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +2 -0
  81. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +2 -0
  82. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +2 -0
  83. package/dist/FilterPicker/index.cjs +2 -0
  84. package/dist/FilterPicker/index.mjs +2 -0
  85. package/dist/FilterPickerContent-cjs.js +5 -4
  86. package/dist/FilterPickerContent-es.js +2 -1
  87. package/dist/FormFieldWrapper-cjs.js +1 -0
  88. package/dist/FormFieldWrapper-es.js +1 -0
  89. package/dist/FormatFile/index.cjs +2 -0
  90. package/dist/FormatFile/index.mjs +2 -0
  91. package/dist/FormatFile-cjs.js +1 -0
  92. package/dist/FormatFile-es.js +1 -0
  93. package/dist/Gallery/index.cjs +2 -0
  94. package/dist/Gallery/index.mjs +2 -0
  95. package/dist/InputAvatar-cjs.js +1 -0
  96. package/dist/InputAvatar-es.js +1 -0
  97. package/dist/InputDate/index.cjs +2 -0
  98. package/dist/InputDate/index.mjs +2 -0
  99. package/dist/InputDate-cjs.js +1 -0
  100. package/dist/InputDate-es.js +1 -0
  101. package/dist/InputNumber/index.cjs +2 -1
  102. package/dist/InputNumber/index.mjs +2 -1
  103. package/dist/InternalChipDismissible-cjs.js +1 -0
  104. package/dist/InternalChipDismissible-es.js +1 -0
  105. package/dist/LightBox/index.cjs +2 -0
  106. package/dist/LightBox/index.mjs +2 -0
  107. package/dist/LightBox-cjs.js +1 -0
  108. package/dist/LightBox-es.js +1 -0
  109. package/dist/Menu/index.cjs +2 -1
  110. package/dist/Menu/index.mjs +2 -1
  111. package/dist/Menu-cjs.js +6 -4
  112. package/dist/Menu-es.js +3 -1
  113. package/dist/MenuSubmenuTrigger-cjs.js +164 -206
  114. package/dist/MenuSubmenuTrigger-es.js +152 -194
  115. package/dist/Modal/index.cjs +3 -1
  116. package/dist/Modal/index.mjs +4 -2
  117. package/dist/NumberFieldInput-cjs.js +308 -187
  118. package/dist/NumberFieldInput-es.js +308 -187
  119. package/dist/Page/index.cjs +2 -1
  120. package/dist/Page/index.mjs +2 -1
  121. package/dist/Page-cjs.js +1 -0
  122. package/dist/Page-es.js +1 -0
  123. package/dist/Popover/index.cjs +2 -0
  124. package/dist/Popover/index.mjs +2 -0
  125. package/dist/Popover-cjs.js +9 -7
  126. package/dist/Popover-es.js +3 -1
  127. package/dist/ProgressBar-cjs.js +1 -0
  128. package/dist/ProgressBar-es.js +1 -0
  129. package/dist/ScrollAreaViewport-cjs.js +1093 -747
  130. package/dist/ScrollAreaViewport-es.js +1081 -734
  131. package/dist/Select/index.cjs +2 -1
  132. package/dist/Select/index.mjs +2 -1
  133. package/dist/SelectGroupLabel-cjs.js +306 -207
  134. package/dist/SelectGroupLabel-es.js +302 -203
  135. package/dist/SideDrawer-cjs.js +1 -0
  136. package/dist/SideDrawer-es.js +1 -0
  137. package/dist/Spinner-cjs.js +1 -0
  138. package/dist/Spinner-es.js +1 -0
  139. package/dist/Tooltip/index.cjs +2 -0
  140. package/dist/Tooltip/index.mjs +2 -0
  141. package/dist/Tooltip-cjs.js +7 -6
  142. package/dist/Tooltip-es.js +2 -1
  143. package/dist/dialogReturnFocus-cjs.js +1 -1
  144. package/dist/dialogReturnFocus-es.js +2 -2
  145. package/dist/floating-ui.react-cjs.js +192 -2564
  146. package/dist/floating-ui.react-dom-cjs.js +2103 -0
  147. package/dist/floating-ui.react-dom-es.js +2072 -0
  148. package/dist/floating-ui.react-es.js +156 -2519
  149. package/dist/floating-ui.utils-cjs.js +342 -0
  150. package/dist/floating-ui.utils-es.js +300 -0
  151. package/dist/index.cjs +4 -3
  152. package/dist/index.mjs +4 -3
  153. package/dist/primitives/BottomSheet/index.cjs +1 -1
  154. package/dist/primitives/BottomSheet/index.mjs +1 -1
  155. package/dist/primitives/ComboboxPrimitive/index.cjs +4 -2
  156. package/dist/primitives/ComboboxPrimitive/index.mjs +4 -2
  157. package/dist/primitives/SelectPrimitive/index.cjs +2 -0
  158. package/dist/primitives/SelectPrimitive/index.mjs +2 -0
  159. package/dist/primitives/index.cjs +3 -2
  160. package/dist/primitives/index.mjs +3 -2
  161. package/dist/scrollEdges-cjs.js +11 -1
  162. package/dist/scrollEdges-es.js +11 -1
  163. package/dist/sharedHelpers/i18n/index.cjs +1 -0
  164. package/dist/sharedHelpers/i18n/index.d.ts +1 -0
  165. package/dist/sharedHelpers/i18n/index.mjs +1 -1
  166. package/dist/sharedHelpers/i18n/releasedLanguages.d.ts +12 -0
  167. package/dist/sharedHelpers/i18n/useReleasedLocale.d.ts +6 -0
  168. package/dist/showToast-cjs.js +1 -1
  169. package/dist/showToast-es.js +1 -1
  170. package/dist/testUtils/releasedLanguages.d.ts +16 -0
  171. package/dist/unstyledPrimitives/index.cjs +478 -720
  172. package/dist/unstyledPrimitives/index.mjs +421 -663
  173. package/dist/useAtlantisI18n-cjs.js +44 -11
  174. package/dist/useAtlantisI18n-es.js +44 -12
  175. package/dist/useCompositeListItem-cjs.js +361 -2108
  176. package/dist/useCompositeListItem-es.js +331 -2076
  177. package/dist/useLabel-cjs.js +3 -2
  178. package/dist/useLabel-es.js +3 -2
  179. package/dist/useRenderElement-cjs.js +7 -9
  180. package/dist/useRenderElement-es.js +7 -9
  181. package/dist/useScrollLock-cjs.js +344 -819
  182. package/dist/useScrollLock-es.js +296 -732
  183. package/dist/useScrollToActive-cjs.js +5 -4
  184. package/dist/useScrollToActive-es.js +2 -1
  185. package/package.json +3 -3
  186. package/dist/scrollable-cjs.js +0 -58
  187. package/dist/scrollable-es.js +0 -54
@@ -5,6 +5,7 @@ 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');
8
9
  var stringifyLocale = require('./stringifyLocale-cjs.js');
9
10
  var scrollEdges = require('./scrollEdges-cjs.js');
10
11
 
@@ -74,112 +75,121 @@ const stateAttributesMapping = {
74
75
  ...useCompositeListItem.fieldValidityMapping
75
76
  };
76
77
 
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;
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'];
86
96
  const PERCENTAGES = ['%', '٪', '%', '﹪'];
87
97
  const PERMILLE = ['‰', '؉'];
98
+ const UNICODE_MINUS_SIGNS = ['−', '-', '‒', '–', '—', '﹣'];
99
+ const UNICODE_PLUS_SIGNS = ['+', '﹢'];
88
100
 
89
101
  // Fullwidth punctuation common in CJK inputs
90
102
  const FULLWIDTH_DECIMAL = '.'; // U+FF0E
91
103
  const FULLWIDTH_GROUP = ','; // U+FF0C
92
104
 
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零〇一二三四五六七八九]/;
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('')}]`);
103
120
 
104
121
  // Whether the character is a digit in any numeral system the field accepts.
105
122
  function isNumeralChar(char) {
106
- return ANY_NUMERAL_DETECT_RE.test(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);
107
124
  }
108
125
  const BASE_NON_NUMERIC_SYMBOLS = ['.', ',', FULLWIDTH_DECIMAL, FULLWIDTH_GROUP, '٫', '٬'];
109
126
  const SPACE_SEPARATOR_RE = /\p{Zs}/u;
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 = ['-', '−', '-', '‒', '–', '—', '﹣'];
127
+ const PLUS_SIGNS_WITH_ASCII = ['+', ...UNICODE_PLUS_SIGNS];
128
+ const MINUS_SIGNS_WITH_ASCII = ['-', ...UNICODE_MINUS_SIGNS];
117
129
  const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
130
+ const escapeClassChar = s => s.replace(/[-\\\]^]/g, m => `\\${m}`); // escape for use inside [...]
131
+
118
132
  function shiftDecimal(value, exponentDelta) {
119
133
  const [coefficient, exponent = '0'] = String(value).split('e');
120
134
  return Number(`${coefficient}e${Number(exponent) + exponentDelta}`);
121
135
  }
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
- }
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);
139
143
  function getNumberLocaleDetails(locale, options) {
140
- const parts = getFormatParts(locale, options);
144
+ const parts = getFormatter(locale, options).formatToParts(11111.1);
141
145
  const result = {};
142
146
  parts.forEach(part => {
143
147
  result[part.type] = part.value;
144
148
  });
145
149
 
146
150
  // The formatting options may omit the decimal separator (e.g. integer formats), so resolve it
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 = '.';
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.
152
153
  getFormatter(locale).formatToParts(0.1).forEach(part => {
153
154
  if (part.type === 'decimal') {
154
- decimal = part.value;
155
+ result[part.type] = part.value;
155
156
  }
156
157
  });
157
- return {
158
- ...result,
159
- decimal
160
- };
158
+ return result;
161
159
  }
162
160
  function parseNumber(formattedNumber, locale, options) {
161
+ if (formattedNumber == null) {
162
+ return null;
163
+ }
164
+
163
165
  // Normalize control characters and whitespace; remove bidi/format controls
164
- let input = formattedNumber.replace(FORMAT_CONTROL_GLOBAL_RE, '').trim();
166
+ let input = String(formattedNumber).replace(/\p{Cf}/gu, '').trim();
165
167
 
166
168
  // Normalize unicode minus/plus to ASCII, handle leading/trailing signs
167
169
  input = input.replace(ANY_MINUS_RE, '-').replace(ANY_PLUS_RE, '+');
168
170
  let isNegative = false;
169
171
 
170
- // Strips a matched sign (leading "-12" / trailing "1234-") while recording negativity.
171
- const takeSign = (match, sign) => {
172
- if (sign === '-') {
172
+ // Trailing sign, e.g. "1234-" / "1234+"
173
+ const trailing = input.match(/([+-])\s*$/);
174
+ if (trailing) {
175
+ if (trailing[1] === '-') {
173
176
  isNegative = true;
174
177
  }
175
- return '';
176
- };
177
- input = input.replace(/([+-])\s*$/, takeSign).replace(/^\s*([+-])/, takeSign);
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
+ }
178
188
 
179
189
  // Heuristic locale detection
180
190
  let computedLocale = locale;
181
191
  if (computedLocale === undefined) {
182
- if (ARABIC_PERSIAN_DETECT_RE.test(input)) {
192
+ if (ARABIC_DETECT_RE.test(input) || PERSIAN_DETECT_RE.test(input)) {
183
193
  computedLocale = 'ar';
184
194
  } else if (HAN_DETECT_RE.test(input)) {
185
195
  computedLocale = 'zh';
@@ -211,17 +221,70 @@ function parseNumber(formattedNumber, locale, options) {
211
221
  groupRegex = new RegExp(escapeRegExp(group), 'g');
212
222
  }
213
223
  }
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)
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)
220
253
  // Currency & unit labels
221
- [currency ? new RegExp(escapeRegExp(currency), 'g') : null, ''], [unitRegex, ''], [PERCENT_GLOBAL_RE, ''], [PERMILLE_GLOBAL_RE, ''], [exponentSeparator ? new RegExp(escapeRegExp(exponentSeparator), 'g') : null, 'e'],
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
+ },
222
270
  // Numeral systems to ASCII digits
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]) => {
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
+ }) => {
225
288
  return regex ? acc.replace(regex, replacement) : acc;
226
289
  }, input);
227
290
 
@@ -299,18 +362,27 @@ function removeFloatingPointErrors(value, format) {
299
362
  }
300
363
  return formatter.format(roundedValue) === roundedText ? roundedValue : value;
301
364
  }
302
- function snapToStep(value, base, step, nearest) {
365
+ function snapToStep(value, base, step, mode = 'directional') {
303
366
  const stepSize = Math.abs(step);
304
367
  const direction = Math.sign(step);
305
368
  const tolerance = stepSize * STEP_EPSILON_FACTOR * direction;
306
369
  const rawSteps = value - base + tolerance;
307
- if (nearest) {
370
+ if (mode === 'nearest') {
308
371
  return base + Math.round(rawSteps / step) * step;
309
372
  }
310
373
  const snappedSteps = direction > 0 ? Math.floor(rawSteps / stepSize) : Math.ceil(rawSteps / stepSize);
311
374
  return base + snappedSteps * stepSize;
312
375
  }
313
- function toValidatedNumber(value, step, minWithDefault, maxWithDefault, minWithZeroDefault, format, snapOnStep, small, shouldClamp) {
376
+ function toValidatedNumber(value, {
377
+ step,
378
+ minWithDefault,
379
+ maxWithDefault,
380
+ minWithZeroDefault,
381
+ format,
382
+ snapOnStep,
383
+ small,
384
+ clamp: shouldClamp
385
+ }) {
314
386
  if (value === null) {
315
387
  return value;
316
388
  }
@@ -318,9 +390,8 @@ function toValidatedNumber(value, step, minWithDefault, maxWithDefault, minWithZ
318
390
  if (step != null && snapOnStep && step !== 0) {
319
391
  const base = small || minWithDefault === Number.MIN_SAFE_INTEGER ? minWithZeroDefault : minWithDefault;
320
392
 
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);
393
+ // Snap before clamping so non-step-aligned boundaries stay reachable.
394
+ nextValue = snapToStep(nextValue, base, step, small ? 'nearest' : 'directional');
324
395
  }
325
396
 
326
397
  // Clamp before rounding so a value just outside a fractional boundary (e.g. `0.4` with
@@ -375,6 +446,7 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
375
446
  validityData,
376
447
  disabled: fieldDisabled,
377
448
  setFilled,
449
+ invalid,
378
450
  name: fieldName,
379
451
  state: fieldState,
380
452
  validation
@@ -423,46 +495,48 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
423
495
  const [inputValue, setInputValue] = React__namespace.useState(() => formatNumber(value, locale, format));
424
496
  const [inputMode, setInputMode] = React__namespace.useState('numeric');
425
497
  const getAllowedNonNumericKeys = useScrollLock.useStableCallback(() => {
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)) {
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)) {
447
512
  keys.add(' ');
448
513
  }
449
- });
514
+ }
450
515
  const allowPercentSymbols = formatStyle === 'percent' || formatStyle === 'unit' && format?.unit === 'percent';
451
516
  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.
454
517
  if (allowPercentSymbols) {
455
- addAll(PERCENTAGES);
518
+ PERCENTAGES.forEach(key => keys.add(key));
456
519
  }
457
520
  if (allowPermilleSymbols) {
458
- addAll(PERMILLE);
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
+ }
459
533
  }
460
534
 
461
535
  // Allow plus sign in all cases; minus sign when negatives are valid, or when out-of-range
462
536
  // entry is allowed so native underflow validation can be triggered from the keyboard.
463
- addAll(PLUS_SIGNS_WITH_ASCII);
537
+ PLUS_SIGNS_WITH_ASCII.forEach(key => keys.add(key));
464
538
  if (minWithDefault < 0 || allowOutOfRange) {
465
- addAll(MINUS_SIGNS_WITH_ASCII);
539
+ MINUS_SIGNS_WITH_ASCII.forEach(key => keys.add(key));
466
540
  }
467
541
  return keys;
468
542
  });
@@ -480,13 +554,21 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
480
554
  const dir = details.direction;
481
555
 
482
556
  // Direct text entry (typing, pasting, clearing, autofill) behaves natively; step-based
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;
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;
486
559
 
487
560
  // Only allow out-of-range values for direct text entry. Step-based interactions still clamp.
488
561
  const shouldClampValue = !allowOutOfRange || !isInputReason;
489
- const validatedValue = toValidatedNumber(unvalidatedValue, dir ? getStepAmount(eventWithOptionalKeyState) * dir : undefined, minWithDefault, maxWithDefault, minWithZeroDefault, formatOptionsRef.current, snapOnStep, eventWithOptionalKeyState?.altKey ?? false, shouldClampValue);
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
+ });
490
572
 
491
573
  // Notify about a change even when the numeric value is unchanged for input reasons: the
492
574
  // typed text may clamp/snap to the current value, or differ while validation normalizes
@@ -596,10 +678,10 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
596
678
  const changed = incrementValue(amount, {
597
679
  direction: event.deltaY > 0 ? -1 : 1,
598
680
  event,
599
- reason: useScrollLock.wheel
681
+ reason: 'wheel'
600
682
  });
601
683
  if (changed) {
602
- onValueCommitted(lastChangedValueRef.current, useScrollLock.createGenericEventDetails(useScrollLock.wheel, event));
684
+ onValueCommitted(lastChangedValueRef.current ?? valueRef.current, useScrollLock.createGenericEventDetails(useScrollLock.wheel, event));
603
685
  }
604
686
  }
605
687
  return useScrollLock.addEventListener(element, 'wheel', handleWheel);
@@ -615,8 +697,12 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
615
697
  }), [fieldState, disabled, readOnly, required, value, inputValue, isScrubbing]);
616
698
  const contextValue = React__namespace.useMemo(() => ({
617
699
  inputRef,
700
+ inputValue,
701
+ value,
618
702
  minWithDefault,
619
703
  maxWithDefault,
704
+ disabled,
705
+ readOnly,
620
706
  id,
621
707
  setValue,
622
708
  incrementValue,
@@ -628,16 +714,19 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
628
714
  hasPendingCommitRef,
629
715
  name,
630
716
  nameProp,
717
+ required,
718
+ invalid,
631
719
  inputMode,
632
720
  getAllowedNonNumericKeys,
633
721
  min,
634
722
  max,
635
723
  setInputValue,
636
724
  locale,
725
+ isScrubbing,
637
726
  setIsScrubbing,
638
727
  state,
639
728
  onValueCommitted
640
- }), [inputRef, minWithDefault, maxWithDefault, id, setValue, incrementValue, getStepAmount, formatOptionsRef, valueRef, name, nameProp, inputMode, getAllowedNonNumericKeys, min, max, setInputValue, locale, state, 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]);
641
730
  const element = useRenderElement.useRenderElement('div', componentProps, {
642
731
  ref: forwardedRef,
643
732
  state,
@@ -652,7 +741,7 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
652
741
  inputRef.current?.focus();
653
742
  },
654
743
  onChange(event) {
655
- // Workaround for https://github.com/react/react/issues/9023
744
+ // Workaround for https://github.com/facebook/react/issues/9023
656
745
  if (event.nativeEvent.defaultPrevented || disabled || readOnly) {
657
746
  return;
658
747
  }
@@ -677,7 +766,7 @@ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function Number
677
766
  min: min,
678
767
  max: max
679
768
  // stepMismatch validation is broken unless an explicit `min` is added.
680
- // See https://github.com/react/react/issues/12334.
769
+ // See https://github.com/facebook/react/issues/12334.
681
770
  ,
682
771
  step: stepProp,
683
772
  disabled: disabled,
@@ -774,6 +863,7 @@ function isTouchLikePointerType(pointerType) {
774
863
  function usePressAndHold(params) {
775
864
  const {
776
865
  disabled,
866
+ readOnly = false,
777
867
  tick,
778
868
  onStop,
779
869
  tickDelay = DEFAULT_TICK_DELAY,
@@ -808,7 +898,7 @@ function usePressAndHold(params) {
808
898
  if (!element) {
809
899
  return;
810
900
  }
811
- const win = useScrollLock.getWindow(element);
901
+ const win = floatingUi_utils.getWindow(element);
812
902
  function handleContextMenu(event) {
813
903
  event.preventDefault();
814
904
  }
@@ -853,7 +943,8 @@ function usePressAndHold(params) {
853
943
  isTouchingButtonRef.current = false;
854
944
  },
855
945
  onPointerDown(event) {
856
- if (event.defaultPrevented || event.button || disabled) {
946
+ const isMainButton = !event.button || event.button === 0;
947
+ if (event.defaultPrevented || !isMainButton || disabled || readOnly) {
857
948
  return;
858
949
  }
859
950
  pointerTypeRef.current = event.pointerType;
@@ -896,10 +987,12 @@ function usePressAndHold(params) {
896
987
  }
897
988
  },
898
989
  onPointerMove(event) {
899
- if (disabled || !isTouchLikePointerType(event.pointerType) || !isPressedRef.current) {
990
+ if (disabled || readOnly || !isTouchLikePointerType(event.pointerType) || !isPressedRef.current) {
900
991
  return;
901
992
  }
902
- movesAfterTouchRef.current += 1;
993
+ if (movesAfterTouchRef.current != null) {
994
+ movesAfterTouchRef.current += 1;
995
+ }
903
996
  const {
904
997
  x,
905
998
  y
@@ -911,7 +1004,7 @@ function usePressAndHold(params) {
911
1004
  }
912
1005
  },
913
1006
  onMouseEnter(event) {
914
- if (event.defaultPrevented || disabled || !isPressedRef.current || isTouchingButtonRef.current || isTouchLikePointerType(pointerTypeRef.current)) {
1007
+ if (event.defaultPrevented || disabled || readOnly || !isPressedRef.current || isTouchingButtonRef.current || isTouchLikePointerType(pointerTypeRef.current)) {
915
1008
  return;
916
1009
  }
917
1010
  startAutoChange(event.nativeEvent);
@@ -944,47 +1037,32 @@ function usePressAndHold(params) {
944
1037
  };
945
1038
  }
946
1039
 
1040
+ const CHANGE_VALUE_TICK_DELAY = 60;
1041
+ const START_AUTO_CHANGE_DELAY = 400;
1042
+ const SCROLLING_POINTER_MOVE_DISTANCE = 8;
1043
+
947
1044
  const SELECT_NONE_STYLE = {
948
1045
  WebkitUserSelect: 'none',
949
1046
  userSelect: 'none'
950
1047
  };
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;
1048
+ function useNumberFieldButton(params) {
964
1049
  const {
965
1050
  allowInputSyncRef,
1051
+ disabled,
966
1052
  formatOptionsRef,
967
1053
  getStepAmount,
968
1054
  id,
969
1055
  incrementValue,
970
1056
  inputRef,
971
- maxWithDefault,
972
- minWithDefault,
1057
+ inputValue,
1058
+ isIncrement,
1059
+ locale,
1060
+ readOnly,
973
1061
  setValue,
974
- state,
975
1062
  valueRef,
976
- locale,
977
1063
  lastChangedValueRef,
978
1064
  onValueCommitted
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;
1065
+ } = params;
988
1066
  const pressReason = isIncrement ? useScrollLock.incrementPress : useScrollLock.decrementPress;
989
1067
  function commitValue(nativeEvent) {
990
1068
  const shouldCommitInputValue = !allowInputSyncRef.current;
@@ -1020,6 +1098,9 @@ function useNumberFieldStepperButton(componentProps, forwardedRef, isIncrement)
1020
1098
  } = usePressAndHold({
1021
1099
  disabled: disabled || readOnly,
1022
1100
  elementRef: inputRef,
1101
+ tickDelay: CHANGE_VALUE_TICK_DELAY,
1102
+ startDelay: START_AUTO_CHANGE_DELAY,
1103
+ scrollDistance: SCROLLING_POINTER_MOVE_DISTANCE,
1023
1104
  tick(triggerEvent) {
1024
1105
  const amount = getStepAmount(triggerEvent);
1025
1106
  return incrementValue(amount, {
@@ -1064,7 +1145,8 @@ function useNumberFieldStepperButton(componentProps, forwardedRef, isIncrement)
1064
1145
  }
1065
1146
  },
1066
1147
  onPointerDown(event) {
1067
- if (event.defaultPrevented || readOnly || event.button || disabled) {
1148
+ const isMainButton = !event.button || event.button === 0;
1149
+ if (event.defaultPrevented || readOnly || !isMainButton || disabled) {
1068
1150
  return;
1069
1151
  }
1070
1152
 
@@ -1080,6 +1162,61 @@ function useNumberFieldStepperButton(componentProps, forwardedRef, isIncrement)
1080
1162
  pointerHandlers.onPointerDown(event);
1081
1163
  }
1082
1164
  };
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
+ });
1083
1220
  const {
1084
1221
  getButtonProps,
1085
1222
  buttonRef
@@ -1142,33 +1279,31 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1142
1279
  } = componentProps;
1143
1280
  const {
1144
1281
  allowInputSyncRef,
1282
+ disabled,
1145
1283
  formatOptionsRef,
1146
1284
  getAllowedNonNumericKeys,
1147
1285
  getStepAmount,
1148
1286
  id,
1149
1287
  incrementValue,
1150
1288
  inputMode,
1289
+ inputValue,
1151
1290
  max,
1152
1291
  min,
1153
1292
  name,
1154
1293
  nameProp,
1294
+ readOnly,
1295
+ required,
1155
1296
  setValue,
1156
1297
  state,
1157
1298
  setInputValue,
1158
1299
  locale,
1159
1300
  inputRef,
1301
+ value,
1160
1302
  onValueCommitted,
1161
1303
  lastChangedValueRef,
1162
1304
  hasPendingCommitRef,
1163
1305
  valueRef
1164
1306
  } = useNumberFieldRootContext();
1165
- const {
1166
- disabled,
1167
- readOnly,
1168
- required,
1169
- value,
1170
- inputValue
1171
- } = state;
1172
1307
  const {
1173
1308
  clearErrors
1174
1309
  } = useCompositeListItem.useFormContext();
@@ -1303,7 +1438,7 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1303
1438
  blockRevalidationRef.current = false;
1304
1439
  return;
1305
1440
  }
1306
- committedValue = lastChangedValueRef.current;
1441
+ committedValue = lastChangedValueRef.current ?? committed;
1307
1442
  // If validation normalized back to the current value, `useValueChanged` won't fire to
1308
1443
  // reset the flag, so reset it here or the next external change won't revalidate.
1309
1444
  if (committedValue === value) {
@@ -1324,7 +1459,7 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1324
1459
  }
1325
1460
  },
1326
1461
  onChange(event) {
1327
- // Workaround for https://github.com/react/react/issues/9023
1462
+ // Workaround for https://github.com/facebook/react/issues/9023
1328
1463
  if (event.nativeEvent.defaultPrevented) {
1329
1464
  return;
1330
1465
  }
@@ -1340,10 +1475,7 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1340
1475
  // currently parseable into a number. This preserves good UX for IME
1341
1476
  // composition/partial input while still providing live numeric updates when possible.
1342
1477
  const allowedNonNumericKeys = getAllowedNonNumericKeys();
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));
1478
+ const isValidCharacterString = Array.from(targetValue).every(ch => isNumeralChar(ch) || ANY_MINUS_DETECT_RE.test(ch) || allowedNonNumericKeys.has(ch));
1347
1479
  if (!isValidCharacterString) {
1348
1480
  return;
1349
1481
  }
@@ -1393,7 +1525,7 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1393
1525
  if (event.key === symbol) {
1394
1526
  const symbolIndex = inputValue.indexOf(symbol);
1395
1527
  const isSymbolHighlighted = selectionContainsIndex(symbolIndex);
1396
- isAllowedNonNumericKey = symbolIndex === -1 || isAllSelected || isSymbolHighlighted;
1528
+ isAllowedNonNumericKey = !inputValue.includes(symbol) || isAllSelected || isSymbolHighlighted;
1397
1529
  }
1398
1530
  });
1399
1531
  const isNavigateKey = NAVIGATE_KEYS.has(event.key);
@@ -1406,18 +1538,12 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1406
1538
  event.which === 229 || event.altKey && !isStepKey || event.ctrlKey || event.metaKey || isAllowedNonNumericKey || isNumeralChar(event.key) || isNavigateKey) {
1407
1539
  return;
1408
1540
  }
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
- }
1541
+ const willSetHome = event.key === 'Home' && min != null;
1542
+ const willSetEnd = event.key === 'End' && max != null;
1417
1543
 
1418
1544
  // Let the browser handle multi-character keys we don't act on (PageUp, Insert, F-keys,
1419
1545
  // Home/End without min/max); invalid single characters are still blocked below.
1420
- if (event.key.length > 1 && !isStepKey && boundaryValue === null) {
1546
+ if (event.key.length > 1 && !isStepKey && !willSetHome && !willSetEnd) {
1421
1547
  return;
1422
1548
  }
1423
1549
 
@@ -1429,14 +1555,12 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1429
1555
  const amount = getStepAmount(event);
1430
1556
 
1431
1557
  // Prevent insertion of text or caret from moving.
1432
- event.preventDefault();
1433
- event.stopPropagation();
1558
+ useScrollLock.stopEvent(event);
1434
1559
  const commitDetails = useScrollLock.createGenericEventDetails(useScrollLock.keyboard, nativeEvent);
1435
1560
  let changed = false;
1436
- if (isStepKey || boundaryValue !== null) {
1561
+ if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
1437
1562
  allowInputSyncRef.current = true;
1438
- }
1439
- if (isStepKey) {
1563
+
1440
1564
  // When stepping from the synced numeric state, refresh the commit ref to the current
1441
1565
  // value so a canceled step can't commit a stale `lastChangedValueRef` left over from an
1442
1566
  // earlier change (mirrors the button path).
@@ -1449,14 +1573,15 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1449
1573
  event: nativeEvent,
1450
1574
  reason: useScrollLock.keyboard
1451
1575
  });
1452
- } else if (boundaryValue !== null) {
1453
- changed = setValue(boundaryValue, useScrollLock.createChangeEventDetails(useScrollLock.keyboard, nativeEvent));
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));
1454
1582
  }
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.
1458
1583
  if (changed) {
1459
- onValueCommitted(lastChangedValueRef.current, commitDetails);
1584
+ onValueCommitted(lastChangedValueRef.current ?? valueRef.current, commitDetails);
1460
1585
  }
1461
1586
  },
1462
1587
  onPaste(event) {
@@ -1467,7 +1592,6 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1467
1592
  try {
1468
1593
  pastedData = event.clipboardData?.getData('text/plain') ?? '';
1469
1594
  } catch {
1470
- /* istanbul ignore else -- `process.env.NODE_ENV` is a build-time constant under test */
1471
1595
  if (process.env.NODE_ENV !== 'production') {
1472
1596
  const ownerStackMessage = useScrollLock.SafeReact.captureOwnerStack?.() || '';
1473
1597
  useRenderElement.warn('<NumberField.Input> could not read clipboard text during paste handling.', ownerStackMessage);
@@ -1480,12 +1604,9 @@ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function Numbe
1480
1604
 
1481
1605
  // Insert the pasted text at the caret/selection instead of replacing the entire value,
1482
1606
  // 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.
1486
1607
  const input = event.currentTarget;
1487
- const selectionStart = input.selectionStart;
1488
- const selectionEnd = input.selectionEnd;
1608
+ const selectionStart = input.selectionStart ?? inputValue.length;
1609
+ const selectionEnd = input.selectionEnd ?? inputValue.length;
1489
1610
  const nextText = inputValue.slice(0, selectionStart) + pastedData + inputValue.slice(selectionEnd);
1490
1611
  const parsedValue = parseNumber(nextText, locale, formatOptionsRef.current);
1491
1612
  if (parsedValue !== null) {