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