@jobber/components 7.4.0 → 7.5.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.
@@ -624,6 +624,16 @@ function useClick(context, props = {}) {
624
624
  } : useRenderElement.EMPTY_OBJECT, [enabled, reference]);
625
625
  }
626
626
 
627
+ /**
628
+ * Returns a function that forces a rerender.
629
+ */
630
+ function useForcedRerendering() {
631
+ const [, setState] = React__namespace.useState({});
632
+ return React__namespace.useCallback(() => {
633
+ setState({});
634
+ }, []);
635
+ }
636
+
627
637
  function useFloatingRootContext(options) {
628
638
  const {
629
639
  open = false,
@@ -1715,7 +1725,7 @@ let DialogViewportDataAttributes = function (DialogViewportDataAttributes) {
1715
1725
  return DialogViewportDataAttributes;
1716
1726
  }({});
1717
1727
 
1718
- const stateAttributesMapping$6 = {
1728
+ const stateAttributesMapping$8 = {
1719
1729
  ...DrawerRoot.popupStateMapping,
1720
1730
  ...DrawerRoot.transitionStatusMapping,
1721
1731
  nested(value) {
@@ -1764,7 +1774,7 @@ const DialogViewport = /*#__PURE__*/React__namespace.forwardRef(function DialogV
1764
1774
  enabled: shouldRender,
1765
1775
  state,
1766
1776
  ref: [forwardedRef, store.useStateSetter('viewportElement')],
1767
- stateAttributesMapping: stateAttributesMapping$6,
1777
+ stateAttributesMapping: stateAttributesMapping$8,
1768
1778
  props: [{
1769
1779
  role: 'presentation',
1770
1780
  hidden: !mounted,
@@ -4258,7 +4268,7 @@ const ComboboxIcon = /*#__PURE__*/React__namespace.forwardRef(function ComboboxI
4258
4268
  });
4259
4269
  if (process.env.NODE_ENV !== "production") ComboboxIcon.displayName = "ComboboxIcon";
4260
4270
 
4261
- const stateAttributesMapping$5 = {
4271
+ const stateAttributesMapping$7 = {
4262
4272
  ...DrawerRoot.transitionStatusMapping,
4263
4273
  ...DrawerRoot.triggerOpenStateMapping
4264
4274
  };
@@ -4354,7 +4364,7 @@ const ComboboxClear = /*#__PURE__*/React__namespace.forwardRef(function Combobox
4354
4364
  store.state.inputRef.current?.focus();
4355
4365
  }
4356
4366
  }, elementProps, getButtonProps],
4357
- stateAttributesMapping: stateAttributesMapping$5
4367
+ stateAttributesMapping: stateAttributesMapping$7
4358
4368
  });
4359
4369
  const shouldRender = keepMounted || mounted;
4360
4370
  if (!shouldRender) {
@@ -4550,7 +4560,7 @@ const ComboboxPortal = /*#__PURE__*/React__namespace.forwardRef(function Combobo
4550
4560
  });
4551
4561
  if (process.env.NODE_ENV !== "production") ComboboxPortal.displayName = "ComboboxPortal";
4552
4562
 
4553
- const stateAttributesMapping$4 = {
4563
+ const stateAttributesMapping$6 = {
4554
4564
  ...DrawerRoot.popupStateMapping,
4555
4565
  ...DrawerRoot.transitionStatusMapping
4556
4566
  };
@@ -4576,7 +4586,7 @@ const ComboboxBackdrop = /*#__PURE__*/React__namespace.forwardRef(function Combo
4576
4586
  return useRenderElement.useRenderElement('div', componentProps, {
4577
4587
  state,
4578
4588
  ref: forwardedRef,
4579
- stateAttributesMapping: stateAttributesMapping$4,
4589
+ stateAttributesMapping: stateAttributesMapping$6,
4580
4590
  props: [{
4581
4591
  role: 'presentation',
4582
4592
  hidden: !mounted,
@@ -5201,7 +5211,7 @@ const ComboboxPositioner = /*#__PURE__*/React__namespace.forwardRef(function Com
5201
5211
  });
5202
5212
  if (process.env.NODE_ENV !== "production") ComboboxPositioner.displayName = "ComboboxPositioner";
5203
5213
 
5204
- const stateAttributesMapping$3 = {
5214
+ const stateAttributesMapping$5 = {
5205
5215
  ...DrawerRoot.popupStateMapping,
5206
5216
  ...DrawerRoot.transitionStatusMapping
5207
5217
  };
@@ -5262,7 +5272,7 @@ const ComboboxPopup = /*#__PURE__*/React__namespace.forwardRef(function Combobox
5262
5272
  }
5263
5273
  }
5264
5274
  }, getDisabledMountTransitionStyles(transitionStatus), elementProps],
5265
- stateAttributesMapping: stateAttributesMapping$3
5275
+ stateAttributesMapping: stateAttributesMapping$5
5266
5276
  });
5267
5277
 
5268
5278
  // Default initial focus logic:
@@ -6110,7 +6120,7 @@ const ComboboxChipRemove = /*#__PURE__*/React__namespace.forwardRef(function Com
6110
6120
  });
6111
6121
  if (process.env.NODE_ENV !== "production") ComboboxChipRemove.displayName = "ComboboxChipRemove";
6112
6122
 
6113
- var index_parts$1 = /*#__PURE__*/Object.freeze({
6123
+ var index_parts$2 = /*#__PURE__*/Object.freeze({
6114
6124
  __proto__: null,
6115
6125
  Arrow: ComboboxArrow,
6116
6126
  Backdrop: ComboboxBackdrop,
@@ -6158,7 +6168,7 @@ const DrawerClose = DialogClose;
6158
6168
  */
6159
6169
  const DrawerDescription = DialogDescription;
6160
6170
 
6161
- const stateAttributesMapping$2 = {
6171
+ const stateAttributesMapping$4 = {
6162
6172
  active(value) {
6163
6173
  if (value) {
6164
6174
  return {
@@ -6228,12 +6238,12 @@ const DrawerIndent = /*#__PURE__*/React__namespace.forwardRef(function DrawerInd
6228
6238
  [DrawerRoot.DrawerBackdropCssVars.swipeProgress]: '0'
6229
6239
  }
6230
6240
  }, elementProps],
6231
- stateAttributesMapping: stateAttributesMapping$2
6241
+ stateAttributesMapping: stateAttributesMapping$4
6232
6242
  });
6233
6243
  });
6234
6244
  if (process.env.NODE_ENV !== "production") DrawerIndent.displayName = "DrawerIndent";
6235
6245
 
6236
- const stateAttributesMapping$1 = {
6246
+ const stateAttributesMapping$3 = {
6237
6247
  active(value) {
6238
6248
  if (value) {
6239
6249
  return {
@@ -6267,7 +6277,7 @@ const DrawerIndentBackground = /*#__PURE__*/React__namespace.forwardRef(function
6267
6277
  ref: forwardedRef,
6268
6278
  state,
6269
6279
  props: elementProps,
6270
- stateAttributesMapping: stateAttributesMapping$1
6280
+ stateAttributesMapping: stateAttributesMapping$3
6271
6281
  });
6272
6282
  });
6273
6283
  if (process.env.NODE_ENV !== "production") DrawerIndentBackground.displayName = "DrawerIndentBackground";
@@ -7367,7 +7377,7 @@ const SWIPE_AREA_SWIPING_HOOK = {
7367
7377
  const SWIPE_AREA_DISABLED_HOOK = {
7368
7378
  [DrawerSwipeAreaDataAttributes.disabled]: ''
7369
7379
  };
7370
- const stateAttributesMapping = {
7380
+ const stateAttributesMapping$2 = {
7371
7381
  open(value) {
7372
7382
  return value ? SWIPE_AREA_OPEN_HOOK : SWIPE_AREA_CLOSED_HOOK;
7373
7383
  },
@@ -7672,7 +7682,7 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
7672
7682
  return useRenderElement.useRenderElement('div', componentProps, {
7673
7683
  state,
7674
7684
  ref: [forwardedRef, swipeAreaRef, registerTrigger],
7675
- stateAttributesMapping,
7685
+ stateAttributesMapping: stateAttributesMapping$2,
7676
7686
  props: [{
7677
7687
  role: 'presentation',
7678
7688
  'aria-hidden': true,
@@ -8692,7 +8702,7 @@ function shouldDismissFromStartEdge(direction, axis) {
8692
8702
  return null;
8693
8703
  }
8694
8704
 
8695
- var index_parts = /*#__PURE__*/Object.freeze({
8705
+ var index_parts$1 = /*#__PURE__*/Object.freeze({
8696
8706
  __proto__: null,
8697
8707
  Backdrop: DrawerRoot.DrawerBackdrop,
8698
8708
  Close: DrawerClose,
@@ -8712,6 +8722,1849 @@ var index_parts = /*#__PURE__*/Object.freeze({
8712
8722
  createHandle: createDialogHandle
8713
8723
  });
8714
8724
 
8725
+ const cache = new Map();
8726
+ function getFormatter(locale, options) {
8727
+ const optionsString = JSON.stringify({
8728
+ locale,
8729
+ options
8730
+ });
8731
+ const cachedFormatter = cache.get(optionsString);
8732
+ if (cachedFormatter) {
8733
+ return cachedFormatter;
8734
+ }
8735
+ const formatter = new Intl.NumberFormat(locale, options);
8736
+ cache.set(optionsString, formatter);
8737
+ return formatter;
8738
+ }
8739
+ function formatNumber(value, locale, options) {
8740
+ if (value == null) {
8741
+ return '';
8742
+ }
8743
+ return getFormatter(locale, options).format(value);
8744
+ }
8745
+ function formatNumberMaxPrecision(value, locale, options) {
8746
+ return formatNumber(value, locale, {
8747
+ ...options,
8748
+ maximumFractionDigits: 20
8749
+ });
8750
+ }
8751
+
8752
+ const EMPTY = 0;
8753
+ class Interval extends DrawerRoot.Timeout {
8754
+ static create() {
8755
+ return new Interval();
8756
+ }
8757
+
8758
+ /**
8759
+ * Executes `fn` at `delay` interval, clearing any previously scheduled call.
8760
+ */
8761
+ start(delay, fn) {
8762
+ this.clear();
8763
+ this.currentId = setInterval(() => {
8764
+ fn();
8765
+ }, delay);
8766
+ }
8767
+ clear = () => {
8768
+ if (this.currentId !== EMPTY) {
8769
+ clearInterval(this.currentId);
8770
+ this.currentId = EMPTY;
8771
+ }
8772
+ };
8773
+ }
8774
+
8775
+ /**
8776
+ * A `setInterval` with automatic cleanup and guard.
8777
+ */
8778
+ function useInterval() {
8779
+ const timeout = useRenderElement.useRefWithInit(Interval.create).current;
8780
+ DrawerRoot.useOnMount(timeout.disposeEffect);
8781
+ return timeout;
8782
+ }
8783
+
8784
+ const NumberFieldRootContext = /*#__PURE__*/React__namespace.createContext(undefined);
8785
+ if (process.env.NODE_ENV !== "production") NumberFieldRootContext.displayName = "NumberFieldRootContext";
8786
+ function useNumberFieldRootContext() {
8787
+ const context = React__namespace.useContext(NumberFieldRootContext);
8788
+ if (context === undefined) {
8789
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: NumberFieldRootContext is missing. NumberField parts must be placed within <NumberField.Root>.' : useRenderElement.formatErrorMessage(43));
8790
+ }
8791
+ return context;
8792
+ }
8793
+
8794
+ const stateAttributesMapping$1 = {
8795
+ inputValue: () => null,
8796
+ value: () => null,
8797
+ ...fieldValidityMapping
8798
+ };
8799
+
8800
+ const HAN_NUMERALS = ['零', '〇', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
8801
+ // Map Han numeral characters to ASCII digits. Includes both forms of zero.
8802
+ const HAN_NUMERAL_TO_DIGIT = {
8803
+ 零: '0',
8804
+ 〇: '0',
8805
+ 一: '1',
8806
+ 二: '2',
8807
+ 三: '3',
8808
+ 四: '4',
8809
+ 五: '5',
8810
+ 六: '6',
8811
+ 七: '7',
8812
+ 八: '8',
8813
+ 九: '9'
8814
+ };
8815
+ const ARABIC_NUMERALS = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
8816
+ const PERSIAN_NUMERALS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
8817
+ const FULLWIDTH_NUMERALS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
8818
+ const PERCENTAGES = ['%', '٪', '%', '﹪'];
8819
+ const PERMILLE = ['‰', '؉'];
8820
+ const UNICODE_MINUS_SIGNS = ['−', '-', '‒', '–', '—', '﹣'];
8821
+ const UNICODE_PLUS_SIGNS = ['+', '﹢'];
8822
+
8823
+ // Fullwidth punctuation common in CJK inputs
8824
+ const FULLWIDTH_DECIMAL = '.'; // U+FF0E
8825
+ const FULLWIDTH_GROUP = ','; // U+FF0C
8826
+
8827
+ const ARABIC_RE = new RegExp(`[${ARABIC_NUMERALS.join('')}]`, 'g');
8828
+ const PERSIAN_RE = new RegExp(`[${PERSIAN_NUMERALS.join('')}]`, 'g');
8829
+ const FULLWIDTH_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`, 'g');
8830
+ const HAN_RE = new RegExp(`[${HAN_NUMERALS.join('')}]`, 'g');
8831
+ const PERCENT_RE = new RegExp(`[${PERCENTAGES.join('')}]`);
8832
+ const PERMILLE_RE = new RegExp(`[${PERMILLE.join('')}]`);
8833
+
8834
+ // Detection regexes (non-global to avoid lastIndex side effects)
8835
+ const ARABIC_DETECT_RE = /[٠١٢٣٤٥٦٧٨٩]/;
8836
+ const PERSIAN_DETECT_RE = /[۰۱۲۳۴۵۶۷۸۹]/;
8837
+ const HAN_DETECT_RE = /[零〇一二三四五六七八九]/;
8838
+ const FULLWIDTH_DETECT_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`);
8839
+ const BASE_NON_NUMERIC_SYMBOLS = ['.', ',', FULLWIDTH_DECIMAL, FULLWIDTH_GROUP, '٫', '٬'];
8840
+ const SPACE_SEPARATOR_RE = /\p{Zs}/u;
8841
+ const PLUS_SIGNS_WITH_ASCII = ['+', ...UNICODE_PLUS_SIGNS];
8842
+ const MINUS_SIGNS_WITH_ASCII = ['-', ...UNICODE_MINUS_SIGNS];
8843
+ const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
8844
+ const escapeClassChar = s => s.replace(/[-\\\]^]/g, m => `\\${m}`); // escape for use inside [...]
8845
+
8846
+ const charClassFrom = chars => `[${chars.map(escapeClassChar).join('')}]`;
8847
+ const ANY_MINUS_CLASS = charClassFrom(['-'].concat(UNICODE_MINUS_SIGNS));
8848
+ const ANY_PLUS_CLASS = charClassFrom(['+'].concat(UNICODE_PLUS_SIGNS));
8849
+ const ANY_MINUS_RE = new RegExp(ANY_MINUS_CLASS, 'gu');
8850
+ const ANY_PLUS_RE = new RegExp(ANY_PLUS_CLASS, 'gu');
8851
+ const ANY_MINUS_DETECT_RE = new RegExp(ANY_MINUS_CLASS);
8852
+ const ANY_PLUS_DETECT_RE = new RegExp(ANY_PLUS_CLASS);
8853
+ function getNumberLocaleDetails(locale, options) {
8854
+ const parts = getFormatter(locale, options).formatToParts(11111.1);
8855
+ const result = {};
8856
+ parts.forEach(part => {
8857
+ result[part.type] = part.value;
8858
+ });
8859
+
8860
+ // The formatting options may result in not returning a decimal.
8861
+ getFormatter(locale).formatToParts(0.1).forEach(part => {
8862
+ if (part.type === 'decimal') {
8863
+ result[part.type] = part.value;
8864
+ }
8865
+ });
8866
+ return result;
8867
+ }
8868
+ function parseNumber(formattedNumber, locale, options) {
8869
+ if (formattedNumber == null) {
8870
+ return null;
8871
+ }
8872
+
8873
+ // Normalize control characters and whitespace; remove bidi/format controls
8874
+ let input = String(formattedNumber).replace(/\p{Cf}/gu, '').trim();
8875
+
8876
+ // Normalize unicode minus/plus to ASCII, handle leading/trailing signs
8877
+ input = input.replace(ANY_MINUS_RE, '-').replace(ANY_PLUS_RE, '+');
8878
+ let isNegative = false;
8879
+
8880
+ // Trailing sign, e.g. "1234-" / "1234+"
8881
+ const trailing = input.match(/([+-])\s*$/);
8882
+ if (trailing) {
8883
+ if (trailing[1] === '-') {
8884
+ isNegative = true;
8885
+ }
8886
+ input = input.replace(/([+-])\s*$/, '');
8887
+ }
8888
+ // Leading sign
8889
+ const leading = input.match(/^\s*([+-])/);
8890
+ if (leading) {
8891
+ if (leading[1] === '-') {
8892
+ isNegative = true;
8893
+ }
8894
+ input = input.replace(/^\s*[+-]/, '');
8895
+ }
8896
+
8897
+ // Heuristic locale detection
8898
+ let computedLocale = locale;
8899
+ if (computedLocale === undefined) {
8900
+ if (ARABIC_DETECT_RE.test(input) || PERSIAN_DETECT_RE.test(input)) {
8901
+ computedLocale = 'ar';
8902
+ } else if (HAN_DETECT_RE.test(input)) {
8903
+ computedLocale = 'zh';
8904
+ }
8905
+ }
8906
+ const {
8907
+ group,
8908
+ decimal,
8909
+ currency
8910
+ } = getNumberLocaleDetails(computedLocale, options);
8911
+
8912
+ // Build robust unit regex from all unit parts (such as "km/h")
8913
+ const unitParts = getFormatter(computedLocale, options).formatToParts(1).filter(p => p.type === 'unit').map(p => escapeRegExp(p.value));
8914
+ const unitRegex = unitParts.length ? new RegExp(unitParts.join('|'), 'g') : null;
8915
+ let groupRegex = null;
8916
+ if (group) {
8917
+ const isSpaceGroup = /\p{Zs}/u.test(group);
8918
+ const isApostropheGroup = group === "'" || group === '’';
8919
+
8920
+ // Check if the group separator is a space-like character.
8921
+ // If so, we'll replace all such characters with an empty string.
8922
+ if (isSpaceGroup) {
8923
+ groupRegex = /\p{Zs}/gu;
8924
+ } else if (isApostropheGroup) {
8925
+ // Some environments format numbers with ASCII apostrophe and others with a curly apostrophe.
8926
+ groupRegex = /['’]/g;
8927
+ } else {
8928
+ groupRegex = new RegExp(escapeRegExp(group), 'g');
8929
+ }
8930
+ }
8931
+ const replacements = [{
8932
+ regex: group ? groupRegex : null,
8933
+ replacement: ''
8934
+ }, {
8935
+ regex: decimal ? new RegExp(escapeRegExp(decimal), 'g') : null,
8936
+ replacement: '.'
8937
+ },
8938
+ // Fullwidth punctuation
8939
+ {
8940
+ regex: /./g,
8941
+ replacement: '.'
8942
+ },
8943
+ // FULLWIDTH_DECIMAL
8944
+ {
8945
+ regex: /,/g,
8946
+ replacement: ''
8947
+ },
8948
+ // FULLWIDTH_GROUP
8949
+ // Arabic punctuation
8950
+ {
8951
+ regex: /٫/g,
8952
+ replacement: '.'
8953
+ },
8954
+ // ARABIC DECIMAL SEPARATOR (U+066B)
8955
+ {
8956
+ regex: /٬/g,
8957
+ replacement: ''
8958
+ },
8959
+ // ARABIC THOUSANDS SEPARATOR (U+066C)
8960
+ // Currency & unit labels
8961
+ {
8962
+ regex: currency ? new RegExp(escapeRegExp(currency), 'g') : null,
8963
+ replacement: ''
8964
+ }, {
8965
+ regex: unitRegex,
8966
+ replacement: ''
8967
+ },
8968
+ // Numeral systems to ASCII digits
8969
+ {
8970
+ regex: ARABIC_RE,
8971
+ replacement: ch => String(ARABIC_NUMERALS.indexOf(ch))
8972
+ }, {
8973
+ regex: PERSIAN_RE,
8974
+ replacement: ch => String(PERSIAN_NUMERALS.indexOf(ch))
8975
+ }, {
8976
+ regex: FULLWIDTH_RE,
8977
+ replacement: ch => String(FULLWIDTH_NUMERALS.indexOf(ch))
8978
+ }, {
8979
+ regex: HAN_RE,
8980
+ replacement: ch => HAN_NUMERAL_TO_DIGIT[ch]
8981
+ }];
8982
+ let unformatted = replacements.reduce((acc, {
8983
+ regex,
8984
+ replacement
8985
+ }) => {
8986
+ return regex ? acc.replace(regex, replacement) : acc;
8987
+ }, input);
8988
+
8989
+ // Mixed-locale safety: keep only the last '.' as decimal
8990
+ const lastDot = unformatted.lastIndexOf('.');
8991
+ if (lastDot !== -1) {
8992
+ unformatted = `${unformatted.slice(0, lastDot).replace(/\./g, '')}.${unformatted.slice(lastDot + 1).replace(/\./g, '')}`;
8993
+ }
8994
+
8995
+ // Guard against Infinity inputs (ASCII and symbol)
8996
+ if (/^[-+]?Infinity$/i.test(input) || /[∞]/.test(input)) {
8997
+ return null;
8998
+ }
8999
+ const parseTarget = (isNegative ? '-' : '') + unformatted;
9000
+ let num = parseFloat(parseTarget);
9001
+ const style = options?.style;
9002
+ const isUnitPercent = style === 'unit' && options?.unit === 'percent';
9003
+ const hasPercentSymbol = PERCENT_RE.test(formattedNumber) || style === 'percent';
9004
+ const hasPermilleSymbol = PERMILLE_RE.test(formattedNumber);
9005
+ if (hasPermilleSymbol) {
9006
+ num /= 1000;
9007
+ } else if (!isUnitPercent && hasPercentSymbol) {
9008
+ num /= 100;
9009
+ }
9010
+ if (Number.isNaN(num)) {
9011
+ return null;
9012
+ }
9013
+ return num;
9014
+ }
9015
+
9016
+ const CHANGE_VALUE_TICK_DELAY = 60;
9017
+ const START_AUTO_CHANGE_DELAY = 400;
9018
+ const TOUCH_TIMEOUT = 50;
9019
+ const MAX_POINTER_MOVES_AFTER_TOUCH = 3;
9020
+ const SCROLLING_POINTER_MOVE_DISTANCE = 8;
9021
+ const DEFAULT_STEP = 1;
9022
+
9023
+ const STEP_EPSILON_FACTOR = 1e-10;
9024
+ function getFractionDigits(format) {
9025
+ const defaultOptions = getFormatter('en-US').resolvedOptions();
9026
+ const minimumFractionDigits = format?.minimumFractionDigits ?? defaultOptions.minimumFractionDigits ?? 0;
9027
+ const maximumFractionDigits = Math.max(format?.maximumFractionDigits ?? defaultOptions.maximumFractionDigits ?? 20, minimumFractionDigits);
9028
+ return {
9029
+ maximumFractionDigits,
9030
+ minimumFractionDigits
9031
+ };
9032
+ }
9033
+ function roundToFractionDigits(value, maximumFractionDigits) {
9034
+ if (!Number.isFinite(value)) {
9035
+ return value;
9036
+ }
9037
+ const digits = Math.min(Math.max(maximumFractionDigits, 0), 20);
9038
+ return Number(value.toFixed(digits));
9039
+ }
9040
+ function removeFloatingPointErrors(value, format) {
9041
+ const {
9042
+ maximumFractionDigits
9043
+ } = getFractionDigits(format);
9044
+ return roundToFractionDigits(value, maximumFractionDigits);
9045
+ }
9046
+ function snapToStep(clampedValue, base, step, mode = 'directional') {
9047
+ if (step === 0) {
9048
+ return clampedValue;
9049
+ }
9050
+ const stepSize = Math.abs(step);
9051
+ const direction = Math.sign(step);
9052
+ const tolerance = stepSize * STEP_EPSILON_FACTOR * direction;
9053
+ const divisor = mode === 'nearest' ? step : stepSize;
9054
+ const rawSteps = (clampedValue - base + tolerance) / divisor;
9055
+ let snappedSteps;
9056
+ if (mode === 'nearest') {
9057
+ snappedSteps = Math.round(rawSteps);
9058
+ } else if (direction > 0) {
9059
+ snappedSteps = Math.floor(rawSteps);
9060
+ } else {
9061
+ snappedSteps = Math.ceil(rawSteps);
9062
+ }
9063
+ const stepForResult = mode === 'nearest' ? step : stepSize;
9064
+ return base + snappedSteps * stepForResult;
9065
+ }
9066
+ function toValidatedNumber(value, {
9067
+ step,
9068
+ minWithDefault,
9069
+ maxWithDefault,
9070
+ minWithZeroDefault,
9071
+ format,
9072
+ snapOnStep,
9073
+ small,
9074
+ clamp: shouldClamp
9075
+ }) {
9076
+ if (value === null) {
9077
+ return value;
9078
+ }
9079
+ const clampedValue = shouldClamp ? DrawerRoot.clamp(value, minWithDefault, maxWithDefault) : value;
9080
+ if (step != null && snapOnStep) {
9081
+ if (step === 0) {
9082
+ return removeFloatingPointErrors(clampedValue, format);
9083
+ }
9084
+
9085
+ // If a real minimum is provided, use it
9086
+ let base = minWithZeroDefault;
9087
+ if (!small && minWithDefault !== Number.MIN_SAFE_INTEGER) {
9088
+ base = minWithDefault;
9089
+ }
9090
+ const snappedValue = snapToStep(clampedValue, base, step, small ? 'nearest' : 'directional');
9091
+ return removeFloatingPointErrors(snappedValue, format);
9092
+ }
9093
+ return removeFloatingPointErrors(clampedValue, format);
9094
+ }
9095
+
9096
+ const NumberFieldRoot = /*#__PURE__*/React__namespace.forwardRef(function NumberFieldRoot(componentProps, forwardedRef) {
9097
+ const {
9098
+ id: idProp,
9099
+ min,
9100
+ max,
9101
+ smallStep = 0.1,
9102
+ step: stepProp = 1,
9103
+ largeStep = 10,
9104
+ required = false,
9105
+ disabled: disabledProp = false,
9106
+ readOnly = false,
9107
+ name: nameProp,
9108
+ defaultValue,
9109
+ value: valueProp,
9110
+ onValueChange: onValueChangeProp,
9111
+ onValueCommitted: onValueCommittedProp,
9112
+ allowWheelScrub = false,
9113
+ snapOnStep = false,
9114
+ allowOutOfRange = false,
9115
+ format,
9116
+ locale,
9117
+ render,
9118
+ className,
9119
+ inputRef: inputRefProp,
9120
+ ...elementProps
9121
+ } = componentProps;
9122
+ const {
9123
+ setDirty,
9124
+ validityData,
9125
+ disabled: fieldDisabled,
9126
+ setFilled,
9127
+ invalid,
9128
+ name: fieldName,
9129
+ state: fieldState,
9130
+ validation,
9131
+ shouldValidateOnChange
9132
+ } = useFieldRootContext();
9133
+ const disabled = fieldDisabled || disabledProp;
9134
+ const name = fieldName ?? nameProp;
9135
+ const step = stepProp === 'any' ? 1 : stepProp;
9136
+ const [isScrubbing, setIsScrubbing] = React__namespace.useState(false);
9137
+ const minWithDefault = min ?? Number.MIN_SAFE_INTEGER;
9138
+ const maxWithDefault = max ?? Number.MAX_SAFE_INTEGER;
9139
+ const minWithZeroDefault = min ?? 0;
9140
+ const formatStyle = format?.style;
9141
+ const inputRef = React__namespace.useRef(null);
9142
+ const hiddenInputRef = useRenderElement.useMergedRefs(inputRefProp, validation.inputRef);
9143
+ const id = useLabelableId({
9144
+ id: idProp
9145
+ });
9146
+ const [valueUnwrapped, setValueUnwrapped] = DrawerRoot.useControlled({
9147
+ controlled: valueProp,
9148
+ default: defaultValue,
9149
+ name: 'NumberField',
9150
+ state: 'value'
9151
+ });
9152
+ const value = valueUnwrapped ?? null;
9153
+ const valueRef = DrawerRoot.useValueAsRef(value);
9154
+ DrawerRoot.useIsoLayoutEffect(() => {
9155
+ setFilled(value !== null);
9156
+ }, [setFilled, value]);
9157
+ const forceRender = useForcedRerendering();
9158
+ const formatOptionsRef = DrawerRoot.useValueAsRef(format);
9159
+ const hasPendingCommitRef = React__namespace.useRef(false);
9160
+ const onValueCommitted = DrawerRoot.useStableCallback((nextValue, eventDetails) => {
9161
+ hasPendingCommitRef.current = false;
9162
+ onValueCommittedProp?.(nextValue, eventDetails);
9163
+ });
9164
+ const startTickTimeout = DrawerRoot.useTimeout();
9165
+ const tickInterval = useInterval();
9166
+ const intentionalTouchCheckTimeout = DrawerRoot.useTimeout();
9167
+ const isPressedRef = React__namespace.useRef(false);
9168
+ const movesAfterTouchRef = React__namespace.useRef(0);
9169
+ const allowInputSyncRef = React__namespace.useRef(true);
9170
+ const lastChangedValueRef = React__namespace.useRef(null);
9171
+ const unsubscribeFromGlobalContextMenuRef = React__namespace.useRef(() => {});
9172
+
9173
+ // During SSR, the value is formatted on the server, whose locale may differ from the client's
9174
+ // locale. This causes a hydration mismatch, which we manually suppress. This is preferable to
9175
+ // rendering an empty input field and then updating it with the formatted value, as the user
9176
+ // can still see the value prior to hydration, even if it's not formatted correctly.
9177
+ const [inputValue, setInputValue] = React__namespace.useState(() => {
9178
+ if (valueProp !== undefined) {
9179
+ return getControlledInputValue(value, locale, format);
9180
+ }
9181
+ return formatNumber(value, locale, format);
9182
+ });
9183
+ const [inputMode, setInputMode] = React__namespace.useState('numeric');
9184
+ const getAllowedNonNumericKeys = DrawerRoot.useStableCallback(() => {
9185
+ const {
9186
+ decimal,
9187
+ group,
9188
+ currency,
9189
+ literal
9190
+ } = getNumberLocaleDetails(locale, format);
9191
+ const keys = new Set();
9192
+ BASE_NON_NUMERIC_SYMBOLS.forEach(symbol => keys.add(symbol));
9193
+ if (decimal) {
9194
+ keys.add(decimal);
9195
+ }
9196
+ if (group) {
9197
+ keys.add(group);
9198
+ if (SPACE_SEPARATOR_RE.test(group)) {
9199
+ keys.add(' ');
9200
+ }
9201
+ }
9202
+ const allowPercentSymbols = formatStyle === 'percent' || formatStyle === 'unit' && format?.unit === 'percent';
9203
+ const allowPermilleSymbols = formatStyle === 'percent' || formatStyle === 'unit' && format?.unit === 'permille';
9204
+ if (allowPercentSymbols) {
9205
+ PERCENTAGES.forEach(key => keys.add(key));
9206
+ }
9207
+ if (allowPermilleSymbols) {
9208
+ PERMILLE.forEach(key => keys.add(key));
9209
+ }
9210
+ if (formatStyle === 'currency' && currency) {
9211
+ keys.add(currency);
9212
+ }
9213
+ if (literal) {
9214
+ // Some locales (e.g. de-DE) insert a literal space character between the number
9215
+ // and the symbol, so allow those characters to be typed/removed.
9216
+ Array.from(literal).forEach(char => keys.add(char));
9217
+ if (SPACE_SEPARATOR_RE.test(literal)) {
9218
+ keys.add(' ');
9219
+ }
9220
+ }
9221
+
9222
+ // Allow plus sign in all cases; minus sign only when negatives are valid
9223
+ PLUS_SIGNS_WITH_ASCII.forEach(key => keys.add(key));
9224
+ if (minWithDefault < 0) {
9225
+ MINUS_SIGNS_WITH_ASCII.forEach(key => keys.add(key));
9226
+ }
9227
+ return keys;
9228
+ });
9229
+ const getStepAmount = DrawerRoot.useStableCallback(event => {
9230
+ if (event?.altKey) {
9231
+ return smallStep;
9232
+ }
9233
+ if (event?.shiftKey) {
9234
+ return largeStep;
9235
+ }
9236
+ return step;
9237
+ });
9238
+ const setValue = DrawerRoot.useStableCallback((unvalidatedValue, details) => {
9239
+ const eventWithOptionalKeyState = details.event;
9240
+ const dir = details.direction;
9241
+ const reason = details.reason;
9242
+ // Only allow out-of-range values for direct text entry (native-like behavior).
9243
+ // Step-based interactions (keyboard arrows, buttons, wheel, scrub) still clamp to min/max.
9244
+ const shouldClampValue = !allowOutOfRange || !(reason === DrawerRoot.inputChange || reason === DrawerRoot.inputBlur || reason === DrawerRoot.inputPaste || reason === DrawerRoot.inputClear || reason === DrawerRoot.none);
9245
+ const validatedValue = toValidatedNumber(unvalidatedValue, {
9246
+ step: dir ? getStepAmount(eventWithOptionalKeyState) * dir : undefined,
9247
+ format: formatOptionsRef.current,
9248
+ minWithDefault,
9249
+ maxWithDefault,
9250
+ minWithZeroDefault,
9251
+ snapOnStep,
9252
+ small: eventWithOptionalKeyState?.altKey ?? false,
9253
+ clamp: shouldClampValue
9254
+ });
9255
+
9256
+ // Determine whether we should notify about a change even if the numeric value is unchanged.
9257
+ // This is needed when the user input is clamped/snapped to the same current value, or when
9258
+ // the source value differs but validation normalizes to the existing value.
9259
+ const isInputReason = details.reason === DrawerRoot.inputChange || details.reason === DrawerRoot.inputClear || details.reason === DrawerRoot.inputBlur || details.reason === DrawerRoot.inputPaste || details.reason === DrawerRoot.none;
9260
+ const shouldFireChange = validatedValue !== value || isInputReason && (unvalidatedValue !== value || allowInputSyncRef.current === false);
9261
+ if (shouldFireChange) {
9262
+ lastChangedValueRef.current = validatedValue;
9263
+ onValueChangeProp?.(validatedValue, details);
9264
+ if (details.isCanceled) {
9265
+ return shouldFireChange;
9266
+ }
9267
+ setValueUnwrapped(validatedValue);
9268
+ setDirty(validatedValue !== validityData.initialValue);
9269
+ hasPendingCommitRef.current = true;
9270
+ }
9271
+
9272
+ // Keep the visible input in sync immediately when programmatic changes occur
9273
+ // (increment/decrement, wheel, etc). During direct typing we don't want
9274
+ // to overwrite the user-provided text until blur, so we gate on
9275
+ // `allowInputSyncRef`.
9276
+ if (allowInputSyncRef.current) {
9277
+ setInputValue(formatNumber(validatedValue, locale, format));
9278
+ }
9279
+
9280
+ // Formatting can change even if the numeric value hasn't, so ensure a re-render when needed.
9281
+ forceRender();
9282
+ return shouldFireChange;
9283
+ });
9284
+ const incrementValue = DrawerRoot.useStableCallback((amount, {
9285
+ direction,
9286
+ currentValue,
9287
+ event,
9288
+ reason
9289
+ }) => {
9290
+ const prevValue = currentValue == null ? valueRef.current : currentValue;
9291
+ const nextValue = typeof prevValue === 'number' ? prevValue + amount * direction : Math.max(0, min ?? 0);
9292
+ const nativeEvent = event;
9293
+ return setValue(nextValue, DrawerRoot.createChangeEventDetails(reason, nativeEvent, undefined, {
9294
+ direction
9295
+ }));
9296
+ });
9297
+ const stopAutoChange = DrawerRoot.useStableCallback(() => {
9298
+ intentionalTouchCheckTimeout.clear();
9299
+ startTickTimeout.clear();
9300
+ tickInterval.clear();
9301
+ unsubscribeFromGlobalContextMenuRef.current();
9302
+ movesAfterTouchRef.current = 0;
9303
+ });
9304
+ const startAutoChange = DrawerRoot.useStableCallback((isIncrement, triggerEvent) => {
9305
+ stopAutoChange();
9306
+ if (!inputRef.current) {
9307
+ return;
9308
+ }
9309
+ const win = index_esm.getWindow(inputRef.current);
9310
+ function handleContextMenu(event) {
9311
+ event.preventDefault();
9312
+ }
9313
+
9314
+ // A global context menu is necessary to prevent the context menu from appearing when the touch
9315
+ // is slightly outside of the element's hit area.
9316
+ win.addEventListener('contextmenu', handleContextMenu);
9317
+ unsubscribeFromGlobalContextMenuRef.current = () => {
9318
+ win.removeEventListener('contextmenu', handleContextMenu);
9319
+ };
9320
+ win.addEventListener('pointerup', event => {
9321
+ isPressedRef.current = false;
9322
+ stopAutoChange();
9323
+ const committed = lastChangedValueRef.current ?? valueRef.current;
9324
+ const commitReason = isIncrement ? DrawerRoot.incrementPress : DrawerRoot.decrementPress;
9325
+ onValueCommitted(committed, DrawerRoot.createGenericEventDetails(commitReason, event));
9326
+ }, {
9327
+ once: true
9328
+ });
9329
+ function tick() {
9330
+ const amount = getStepAmount(triggerEvent) ?? DEFAULT_STEP;
9331
+ return incrementValue(amount, {
9332
+ direction: isIncrement ? 1 : -1,
9333
+ event: triggerEvent,
9334
+ reason: isIncrement ? 'increment-press' : 'decrement-press'
9335
+ });
9336
+ }
9337
+ if (!tick()) {
9338
+ stopAutoChange();
9339
+ return;
9340
+ }
9341
+ startTickTimeout.start(START_AUTO_CHANGE_DELAY, () => {
9342
+ tickInterval.start(CHANGE_VALUE_TICK_DELAY, () => {
9343
+ if (!tick()) {
9344
+ stopAutoChange();
9345
+ }
9346
+ });
9347
+ });
9348
+ });
9349
+
9350
+ // We need to update the input value when the external `value` prop changes. This ends up acting
9351
+ // as a single source of truth to update the input value, bypassing the need to manually set it in
9352
+ // each event handler internally in this hook.
9353
+ // This is done inside a layout effect as an alternative to the technique to set state during
9354
+ // render as we're accessing a ref, which must be inside an effect.
9355
+ // https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
9356
+ //
9357
+ // ESLint is disabled because it needs to run even if the parsed value hasn't changed, since the
9358
+ // value still can be formatted differently.
9359
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9360
+ DrawerRoot.useIsoLayoutEffect(function syncFormattedInputValueOnValueChange() {
9361
+ // This ensures the value is only updated on blur rather than every keystroke, but still
9362
+ // allows the input value to be updated when the value is changed externally.
9363
+ if (!allowInputSyncRef.current) {
9364
+ return;
9365
+ }
9366
+ const nextInputValue = valueProp !== undefined ? getControlledInputValue(value, locale, format) : formatNumber(value, locale, format);
9367
+ if (nextInputValue !== inputValue) {
9368
+ setInputValue(nextInputValue);
9369
+ }
9370
+ });
9371
+ DrawerRoot.useIsoLayoutEffect(function setDynamicInputModeForIOS() {
9372
+ if (!DrawerRoot.isIOS) {
9373
+ return;
9374
+ }
9375
+
9376
+ // iOS numeric software keyboard doesn't have a minus key, so we need to use the default
9377
+ // keyboard to let the user input a negative number.
9378
+ let computedInputMode = 'text';
9379
+ if (minWithDefault >= 0) {
9380
+ // iOS numeric software keyboard doesn't have a decimal key for "numeric" input mode, but
9381
+ // this is better than the "text" input if possible to use.
9382
+ computedInputMode = 'decimal';
9383
+ }
9384
+ setInputMode(computedInputMode);
9385
+ }, [minWithDefault, formatStyle]);
9386
+ React__namespace.useEffect(() => {
9387
+ return () => stopAutoChange();
9388
+ }, [stopAutoChange]);
9389
+
9390
+ // The `onWheel` prop can't be prevented, so we need to use a global event listener.
9391
+ React__namespace.useEffect(function registerElementWheelListener() {
9392
+ const element = inputRef.current;
9393
+ if (disabled || readOnly || !allowWheelScrub || !element) {
9394
+ return undefined;
9395
+ }
9396
+ function handleWheel(event) {
9397
+ if (
9398
+ // Allow pinch-zooming.
9399
+ event.ctrlKey || DrawerRoot.ownerDocument(inputRef.current).activeElement !== inputRef.current) {
9400
+ return;
9401
+ }
9402
+
9403
+ // Prevent the default behavior to avoid scrolling the page.
9404
+ event.preventDefault();
9405
+ const amount = getStepAmount(event) ?? DEFAULT_STEP;
9406
+ incrementValue(amount, {
9407
+ direction: event.deltaY > 0 ? -1 : 1,
9408
+ event,
9409
+ reason: 'wheel'
9410
+ });
9411
+ }
9412
+ element.addEventListener('wheel', handleWheel);
9413
+ return () => {
9414
+ element.removeEventListener('wheel', handleWheel);
9415
+ };
9416
+ }, [allowWheelScrub, incrementValue, disabled, readOnly, largeStep, step, getStepAmount]);
9417
+ const state = React__namespace.useMemo(() => ({
9418
+ ...fieldState,
9419
+ disabled,
9420
+ readOnly,
9421
+ required,
9422
+ value,
9423
+ inputValue,
9424
+ scrubbing: isScrubbing
9425
+ }), [fieldState, disabled, readOnly, required, value, inputValue, isScrubbing]);
9426
+ const contextValue = React__namespace.useMemo(() => ({
9427
+ inputRef,
9428
+ inputValue,
9429
+ value,
9430
+ startAutoChange,
9431
+ stopAutoChange,
9432
+ minWithDefault,
9433
+ maxWithDefault,
9434
+ disabled,
9435
+ readOnly,
9436
+ id,
9437
+ setValue,
9438
+ incrementValue,
9439
+ getStepAmount,
9440
+ allowInputSyncRef,
9441
+ formatOptionsRef,
9442
+ valueRef,
9443
+ lastChangedValueRef,
9444
+ hasPendingCommitRef,
9445
+ isPressedRef,
9446
+ intentionalTouchCheckTimeout,
9447
+ movesAfterTouchRef,
9448
+ name,
9449
+ required,
9450
+ invalid,
9451
+ inputMode,
9452
+ getAllowedNonNumericKeys,
9453
+ min,
9454
+ max,
9455
+ setInputValue,
9456
+ locale,
9457
+ isScrubbing,
9458
+ setIsScrubbing,
9459
+ state,
9460
+ onValueCommitted
9461
+ }), [inputRef, inputValue, value, startAutoChange, stopAutoChange, minWithDefault, maxWithDefault, disabled, readOnly, id, setValue, incrementValue, getStepAmount, formatOptionsRef, valueRef, intentionalTouchCheckTimeout, name, required, invalid, inputMode, getAllowedNonNumericKeys, min, max, setInputValue, locale, isScrubbing, state, onValueCommitted]);
9462
+ const element = useRenderElement.useRenderElement('div', componentProps, {
9463
+ ref: forwardedRef,
9464
+ state,
9465
+ props: elementProps,
9466
+ stateAttributesMapping: stateAttributesMapping$1
9467
+ });
9468
+ return /*#__PURE__*/jsxRuntime.jsxs(NumberFieldRootContext.Provider, {
9469
+ value: contextValue,
9470
+ children: [element, /*#__PURE__*/jsxRuntime.jsx("input", {
9471
+ ...validation.getInputValidationProps({
9472
+ onFocus() {
9473
+ inputRef.current?.focus();
9474
+ },
9475
+ onChange(event) {
9476
+ // Workaround for https://github.com/facebook/react/issues/9023
9477
+ if (event.nativeEvent.defaultPrevented) {
9478
+ return;
9479
+ }
9480
+
9481
+ // Handle browser autofill.
9482
+ const nextValue = event.currentTarget.valueAsNumber;
9483
+ const parsedValue = Number.isNaN(nextValue) ? null : nextValue;
9484
+ const details = DrawerRoot.createChangeEventDetails(DrawerRoot.none, event.nativeEvent);
9485
+ setDirty(parsedValue !== validityData.initialValue);
9486
+ setValue(parsedValue, details);
9487
+ if (shouldValidateOnChange()) {
9488
+ validation.commit(parsedValue);
9489
+ }
9490
+ }
9491
+ }),
9492
+ ref: hiddenInputRef,
9493
+ type: "number",
9494
+ name: name,
9495
+ value: value ?? '',
9496
+ min: min,
9497
+ max: max
9498
+ // stepMismatch validation is broken unless an explicit `min` is added.
9499
+ // See https://github.com/facebook/react/issues/12334.
9500
+ ,
9501
+ step: stepProp,
9502
+ disabled: disabled,
9503
+ required: required,
9504
+ "aria-hidden": true,
9505
+ tabIndex: -1,
9506
+ style: name ? DrawerRoot.visuallyHiddenInput : DrawerRoot.visuallyHidden
9507
+ })]
9508
+ });
9509
+ });
9510
+ if (process.env.NODE_ENV !== "production") NumberFieldRoot.displayName = "NumberFieldRoot";
9511
+ function getControlledInputValue(value, locale, format) {
9512
+ const explicitPrecision = format?.maximumFractionDigits != null || format?.minimumFractionDigits != null;
9513
+ return explicitPrecision ? formatNumber(value, locale, format) : formatNumberMaxPrecision(value, locale, format);
9514
+ }
9515
+
9516
+ /**
9517
+ * Groups the input with the increment and decrement buttons.
9518
+ * Renders a `<div>` element.
9519
+ *
9520
+ * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
9521
+ */
9522
+ const NumberFieldGroup = /*#__PURE__*/React__namespace.forwardRef(function NumberFieldGroup(componentProps, forwardedRef) {
9523
+ const {
9524
+ render,
9525
+ className,
9526
+ ...elementProps
9527
+ } = componentProps;
9528
+ const {
9529
+ state
9530
+ } = useNumberFieldRootContext();
9531
+ const element = useRenderElement.useRenderElement('div', componentProps, {
9532
+ ref: forwardedRef,
9533
+ state,
9534
+ props: [{
9535
+ role: 'group'
9536
+ }, elementProps],
9537
+ stateAttributesMapping: stateAttributesMapping$1
9538
+ });
9539
+ return element;
9540
+ });
9541
+ if (process.env.NODE_ENV !== "production") NumberFieldGroup.displayName = "NumberFieldGroup";
9542
+
9543
+ // Treat pen as touch-like to avoid forcing the software keyboard on stylus taps.
9544
+ // Linux Chrome may emit "pen" historically for mouse usage due to a bug, but the touch path
9545
+ // still works with minor behavioral differences.
9546
+ function isTouchLikePointerType(pointerType) {
9547
+ return pointerType === 'touch' || pointerType === 'pen';
9548
+ }
9549
+ function useNumberFieldButton(params) {
9550
+ const {
9551
+ allowInputSyncRef,
9552
+ disabled,
9553
+ formatOptionsRef,
9554
+ getStepAmount,
9555
+ id,
9556
+ incrementValue,
9557
+ inputRef,
9558
+ inputValue,
9559
+ intentionalTouchCheckTimeout,
9560
+ isIncrement,
9561
+ isPressedRef,
9562
+ locale,
9563
+ movesAfterTouchRef,
9564
+ readOnly,
9565
+ setValue,
9566
+ startAutoChange,
9567
+ stopAutoChange,
9568
+ valueRef,
9569
+ lastChangedValueRef,
9570
+ onValueCommitted
9571
+ } = params;
9572
+ const incrementDownCoordsRef = React__namespace.useRef({
9573
+ x: 0,
9574
+ y: 0
9575
+ });
9576
+ const isTouchingButtonRef = React__namespace.useRef(false);
9577
+ const ignoreClickRef = React__namespace.useRef(false);
9578
+ const pointerTypeRef = React__namespace.useRef('');
9579
+ const pressReason = isIncrement ? 'increment-press' : 'decrement-press';
9580
+ function commitValue(nativeEvent) {
9581
+ allowInputSyncRef.current = true;
9582
+
9583
+ // The input may be dirty but not yet blurred, so the value won't have been committed.
9584
+ const parsedValue = parseNumber(inputValue, locale, formatOptionsRef.current);
9585
+ if (parsedValue !== null) {
9586
+ // The increment value function needs to know the current input value to increment it
9587
+ // correctly.
9588
+ valueRef.current = parsedValue;
9589
+ setValue(parsedValue, DrawerRoot.createChangeEventDetails(pressReason, nativeEvent, undefined, {
9590
+ direction: isIncrement ? 1 : -1
9591
+ }));
9592
+ }
9593
+ }
9594
+ const props = {
9595
+ disabled,
9596
+ 'aria-readonly': readOnly || undefined,
9597
+ 'aria-label': isIncrement ? 'Increase' : 'Decrease',
9598
+ 'aria-controls': id,
9599
+ // Keyboard users shouldn't have access to the buttons, since they can use the input element
9600
+ // to change the value. On the other hand, `aria-hidden` is not applied because touch screen
9601
+ // readers should be able to use the buttons.
9602
+ tabIndex: -1,
9603
+ style: {
9604
+ WebkitUserSelect: 'none',
9605
+ userSelect: 'none'
9606
+ },
9607
+ onTouchStart() {
9608
+ isTouchingButtonRef.current = true;
9609
+ },
9610
+ onTouchEnd() {
9611
+ isTouchingButtonRef.current = false;
9612
+ },
9613
+ onClick(event) {
9614
+ const isDisabled = disabled || readOnly;
9615
+ if (event.defaultPrevented || isDisabled || (
9616
+ // If it's not a keyboard/virtual click, ignore.
9617
+ isTouchLikePointerType(pointerTypeRef.current) ? ignoreClickRef.current : event.detail !== 0)) {
9618
+ return;
9619
+ }
9620
+ commitValue(event.nativeEvent);
9621
+ const amount = getStepAmount(event) ?? DEFAULT_STEP;
9622
+ const prev = valueRef.current;
9623
+ incrementValue(amount, {
9624
+ direction: isIncrement ? 1 : -1,
9625
+ event: event.nativeEvent,
9626
+ reason: pressReason
9627
+ });
9628
+ const committed = lastChangedValueRef.current ?? valueRef.current;
9629
+ if (committed !== prev) {
9630
+ onValueCommitted(committed, DrawerRoot.createGenericEventDetails(pressReason, event.nativeEvent));
9631
+ }
9632
+ },
9633
+ onPointerDown(event) {
9634
+ const isMainButton = !event.button || event.button === 0;
9635
+ if (event.defaultPrevented || readOnly || !isMainButton || disabled) {
9636
+ return;
9637
+ }
9638
+ pointerTypeRef.current = event.pointerType;
9639
+ ignoreClickRef.current = false;
9640
+ isPressedRef.current = true;
9641
+ incrementDownCoordsRef.current = {
9642
+ x: event.clientX,
9643
+ y: event.clientY
9644
+ };
9645
+ commitValue(event.nativeEvent);
9646
+ const isTouchPointer = isTouchLikePointerType(event.pointerType);
9647
+ if (!isTouchPointer) {
9648
+ event.preventDefault();
9649
+ inputRef.current?.focus();
9650
+ startAutoChange(isIncrement, event);
9651
+ } else {
9652
+ // We need to check if the pointerdown was intentional, and not the result of a scroll
9653
+ // or pinch-zoom. In that case, we don't want to change the value.
9654
+ intentionalTouchCheckTimeout.start(TOUCH_TIMEOUT, () => {
9655
+ const moves = movesAfterTouchRef.current;
9656
+ movesAfterTouchRef.current = 0;
9657
+ // Only start auto-change if the touch is still pressed (prevents races
9658
+ // with pointerup occurring before the timeout fires on quick taps).
9659
+ const stillPressed = isPressedRef.current;
9660
+ if (stillPressed && moves != null && moves < MAX_POINTER_MOVES_AFTER_TOUCH) {
9661
+ startAutoChange(isIncrement, event);
9662
+ ignoreClickRef.current = true; // synthesized click should be ignored
9663
+ } else {
9664
+ // No auto-change (simple tap or scroll gesture), allow the click handler
9665
+ // to perform a single increment and commit.
9666
+ ignoreClickRef.current = false;
9667
+ stopAutoChange();
9668
+ }
9669
+ });
9670
+ }
9671
+ },
9672
+ onPointerUp(event) {
9673
+ // Ensure we mark the press as released for touch flows even if auto-change never started,
9674
+ // so the delayed auto-change check won’t start after a quick tap.
9675
+ if (isTouchLikePointerType(event.pointerType)) {
9676
+ isPressedRef.current = false;
9677
+ }
9678
+ },
9679
+ onPointerMove(event) {
9680
+ const isDisabled = disabled || readOnly;
9681
+ if (isDisabled || !isTouchLikePointerType(event.pointerType) || !isPressedRef.current) {
9682
+ return;
9683
+ }
9684
+ if (movesAfterTouchRef.current != null) {
9685
+ movesAfterTouchRef.current += 1;
9686
+ }
9687
+ const {
9688
+ x,
9689
+ y
9690
+ } = incrementDownCoordsRef.current;
9691
+ const dx = x - event.clientX;
9692
+ const dy = y - event.clientY;
9693
+
9694
+ // An alternative to this technique is to detect when the NumberField's parent container
9695
+ // has been scrolled
9696
+ if (dx ** 2 + dy ** 2 > SCROLLING_POINTER_MOVE_DISTANCE ** 2) {
9697
+ stopAutoChange();
9698
+ }
9699
+ },
9700
+ onMouseEnter(event) {
9701
+ const isDisabled = disabled || readOnly;
9702
+ if (event.defaultPrevented || isDisabled || !isPressedRef.current || isTouchingButtonRef.current || isTouchLikePointerType(pointerTypeRef.current)) {
9703
+ return;
9704
+ }
9705
+ startAutoChange(isIncrement, event);
9706
+ },
9707
+ onMouseLeave() {
9708
+ if (isTouchingButtonRef.current) {
9709
+ return;
9710
+ }
9711
+ stopAutoChange();
9712
+ },
9713
+ onMouseUp() {
9714
+ if (isTouchingButtonRef.current) {
9715
+ return;
9716
+ }
9717
+ stopAutoChange();
9718
+ }
9719
+ };
9720
+ return props;
9721
+ }
9722
+
9723
+ /**
9724
+ * A stepper button that increases the field value when clicked.
9725
+ * Renders an `<button>` element.
9726
+ *
9727
+ * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
9728
+ */
9729
+ const NumberFieldIncrement = /*#__PURE__*/React__namespace.forwardRef(function NumberFieldIncrement(componentProps, forwardedRef) {
9730
+ const {
9731
+ render,
9732
+ className,
9733
+ disabled: disabledProp = false,
9734
+ nativeButton = true,
9735
+ ...elementProps
9736
+ } = componentProps;
9737
+ const {
9738
+ allowInputSyncRef,
9739
+ disabled: contextDisabled,
9740
+ formatOptionsRef,
9741
+ getStepAmount,
9742
+ id,
9743
+ incrementValue,
9744
+ inputRef,
9745
+ inputValue,
9746
+ intentionalTouchCheckTimeout,
9747
+ isPressedRef,
9748
+ locale,
9749
+ maxWithDefault,
9750
+ movesAfterTouchRef,
9751
+ readOnly,
9752
+ setValue,
9753
+ startAutoChange,
9754
+ state,
9755
+ stopAutoChange,
9756
+ value,
9757
+ valueRef,
9758
+ lastChangedValueRef,
9759
+ onValueCommitted
9760
+ } = useNumberFieldRootContext();
9761
+ const isMax = value != null && value >= maxWithDefault;
9762
+ const disabled = disabledProp || contextDisabled || isMax;
9763
+ const props = useNumberFieldButton({
9764
+ isIncrement: true,
9765
+ inputRef,
9766
+ startAutoChange,
9767
+ stopAutoChange,
9768
+ inputValue,
9769
+ disabled,
9770
+ readOnly,
9771
+ id,
9772
+ setValue,
9773
+ getStepAmount,
9774
+ incrementValue,
9775
+ allowInputSyncRef,
9776
+ formatOptionsRef,
9777
+ valueRef,
9778
+ isPressedRef,
9779
+ intentionalTouchCheckTimeout,
9780
+ movesAfterTouchRef,
9781
+ locale,
9782
+ lastChangedValueRef,
9783
+ onValueCommitted
9784
+ });
9785
+ const {
9786
+ getButtonProps,
9787
+ buttonRef
9788
+ } = useButton({
9789
+ disabled,
9790
+ native: nativeButton,
9791
+ focusableWhenDisabled: true
9792
+ });
9793
+ const buttonState = React__namespace.useMemo(() => ({
9794
+ ...state,
9795
+ disabled
9796
+ }), [state, disabled]);
9797
+ const element = useRenderElement.useRenderElement('button', componentProps, {
9798
+ ref: [forwardedRef, buttonRef],
9799
+ state: buttonState,
9800
+ props: [props, elementProps, getButtonProps],
9801
+ stateAttributesMapping: stateAttributesMapping$1
9802
+ });
9803
+ return element;
9804
+ });
9805
+ if (process.env.NODE_ENV !== "production") NumberFieldIncrement.displayName = "NumberFieldIncrement";
9806
+
9807
+ /**
9808
+ * A stepper button that decreases the field value when clicked.
9809
+ * Renders an `<button>` element.
9810
+ *
9811
+ * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
9812
+ */
9813
+ const NumberFieldDecrement = /*#__PURE__*/React__namespace.forwardRef(function NumberFieldDecrement(componentProps, forwardedRef) {
9814
+ const {
9815
+ render,
9816
+ className,
9817
+ disabled: disabledProp = false,
9818
+ nativeButton = true,
9819
+ ...elementProps
9820
+ } = componentProps;
9821
+ const {
9822
+ allowInputSyncRef,
9823
+ disabled: contextDisabled,
9824
+ formatOptionsRef,
9825
+ getStepAmount,
9826
+ id,
9827
+ incrementValue,
9828
+ inputRef,
9829
+ inputValue,
9830
+ intentionalTouchCheckTimeout,
9831
+ isPressedRef,
9832
+ minWithDefault,
9833
+ movesAfterTouchRef,
9834
+ readOnly,
9835
+ setValue,
9836
+ startAutoChange,
9837
+ state,
9838
+ stopAutoChange,
9839
+ value,
9840
+ valueRef,
9841
+ locale,
9842
+ lastChangedValueRef,
9843
+ onValueCommitted
9844
+ } = useNumberFieldRootContext();
9845
+ const isMin = value != null && value <= minWithDefault;
9846
+ const disabled = disabledProp || contextDisabled || isMin;
9847
+ const props = useNumberFieldButton({
9848
+ isIncrement: false,
9849
+ inputRef,
9850
+ startAutoChange,
9851
+ stopAutoChange,
9852
+ inputValue,
9853
+ disabled,
9854
+ readOnly,
9855
+ id,
9856
+ setValue,
9857
+ getStepAmount,
9858
+ incrementValue,
9859
+ allowInputSyncRef,
9860
+ formatOptionsRef,
9861
+ valueRef,
9862
+ isPressedRef,
9863
+ intentionalTouchCheckTimeout,
9864
+ movesAfterTouchRef,
9865
+ locale,
9866
+ lastChangedValueRef,
9867
+ onValueCommitted
9868
+ });
9869
+ const {
9870
+ getButtonProps,
9871
+ buttonRef
9872
+ } = useButton({
9873
+ disabled,
9874
+ native: nativeButton,
9875
+ focusableWhenDisabled: true
9876
+ });
9877
+ const buttonState = React__namespace.useMemo(() => ({
9878
+ ...state,
9879
+ disabled
9880
+ }), [state, disabled]);
9881
+ const element = useRenderElement.useRenderElement('button', componentProps, {
9882
+ ref: [forwardedRef, buttonRef],
9883
+ state: buttonState,
9884
+ props: [props, elementProps, getButtonProps],
9885
+ stateAttributesMapping: stateAttributesMapping$1
9886
+ });
9887
+ return element;
9888
+ });
9889
+ if (process.env.NODE_ENV !== "production") NumberFieldDecrement.displayName = "NumberFieldDecrement";
9890
+
9891
+ const stateAttributesMapping = {
9892
+ ...fieldValidityMapping,
9893
+ ...stateAttributesMapping$1
9894
+ };
9895
+ const NAVIGATE_KEYS = new Set(['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Escape']);
9896
+
9897
+ /**
9898
+ * The native input control in the number field.
9899
+ * Renders an `<input>` element.
9900
+ *
9901
+ * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
9902
+ */
9903
+ const NumberFieldInput = /*#__PURE__*/React__namespace.forwardRef(function NumberFieldInput(componentProps, forwardedRef) {
9904
+ const {
9905
+ render,
9906
+ className,
9907
+ ...elementProps
9908
+ } = componentProps;
9909
+ const {
9910
+ allowInputSyncRef,
9911
+ disabled,
9912
+ formatOptionsRef,
9913
+ getAllowedNonNumericKeys,
9914
+ getStepAmount,
9915
+ id,
9916
+ incrementValue,
9917
+ inputMode,
9918
+ inputValue,
9919
+ max,
9920
+ min,
9921
+ name,
9922
+ readOnly,
9923
+ required,
9924
+ setValue,
9925
+ state,
9926
+ setInputValue,
9927
+ locale,
9928
+ inputRef,
9929
+ value,
9930
+ onValueCommitted,
9931
+ lastChangedValueRef,
9932
+ hasPendingCommitRef,
9933
+ valueRef
9934
+ } = useNumberFieldRootContext();
9935
+ const {
9936
+ clearErrors
9937
+ } = useFormContext();
9938
+ const {
9939
+ validationMode,
9940
+ setTouched,
9941
+ setFocused,
9942
+ invalid,
9943
+ shouldValidateOnChange,
9944
+ validation
9945
+ } = useFieldRootContext();
9946
+ const {
9947
+ labelId
9948
+ } = useLabelableContext();
9949
+ const hasTouchedInputRef = React__namespace.useRef(false);
9950
+ const blockRevalidationRef = React__namespace.useRef(false);
9951
+ useField({
9952
+ id,
9953
+ commit: validation.commit,
9954
+ value,
9955
+ controlRef: inputRef,
9956
+ name,
9957
+ getValue: () => value ?? null
9958
+ });
9959
+ DrawerRoot.useValueChanged(value, previousValue => {
9960
+ const validateOnChange = shouldValidateOnChange();
9961
+ clearErrors(name);
9962
+ if (validateOnChange) {
9963
+ validation.commit(value);
9964
+ }
9965
+ if (previousValue === value || validateOnChange) {
9966
+ return;
9967
+ }
9968
+ if (blockRevalidationRef.current) {
9969
+ blockRevalidationRef.current = false;
9970
+ return;
9971
+ }
9972
+ validation.commit(value, true);
9973
+ });
9974
+ const inputProps = {
9975
+ id,
9976
+ required,
9977
+ disabled,
9978
+ readOnly,
9979
+ inputMode,
9980
+ value: inputValue,
9981
+ type: 'text',
9982
+ autoComplete: 'off',
9983
+ autoCorrect: 'off',
9984
+ spellCheck: 'false',
9985
+ 'aria-roledescription': 'Number field',
9986
+ 'aria-invalid': invalid || undefined,
9987
+ 'aria-labelledby': labelId,
9988
+ // If the server's locale does not match the client's locale, the formatting may not match,
9989
+ // causing a hydration mismatch.
9990
+ suppressHydrationWarning: true,
9991
+ onFocus(event) {
9992
+ if (event.defaultPrevented || readOnly || disabled) {
9993
+ return;
9994
+ }
9995
+ setFocused(true);
9996
+ if (hasTouchedInputRef.current) {
9997
+ return;
9998
+ }
9999
+ hasTouchedInputRef.current = true;
10000
+
10001
+ // Browsers set selection at the start of the input field by default. We want to set it at
10002
+ // the end for the first focus.
10003
+ const target = event.currentTarget;
10004
+ const length = target.value.length;
10005
+ target.setSelectionRange(length, length);
10006
+ },
10007
+ onBlur(event) {
10008
+ if (event.defaultPrevented || readOnly || disabled) {
10009
+ return;
10010
+ }
10011
+ setTouched(true);
10012
+ setFocused(false);
10013
+ const hadManualInput = !allowInputSyncRef.current;
10014
+ const hadPendingProgrammaticChange = hasPendingCommitRef.current;
10015
+ allowInputSyncRef.current = true;
10016
+ if (inputValue.trim() === '') {
10017
+ setValue(null, DrawerRoot.createChangeEventDetails(DrawerRoot.inputClear, event.nativeEvent));
10018
+ if (validationMode === 'onBlur') {
10019
+ validation.commit(null);
10020
+ }
10021
+ onValueCommitted(null, DrawerRoot.createGenericEventDetails(DrawerRoot.inputClear, event.nativeEvent));
10022
+ return;
10023
+ }
10024
+ const formatOptions = formatOptionsRef.current;
10025
+ const parsedValue = parseNumber(inputValue, locale, formatOptions);
10026
+ if (parsedValue === null) {
10027
+ return;
10028
+ }
10029
+
10030
+ // If an explicit precision is requested, round the committed numeric value.
10031
+ const hasExplicitPrecision = formatOptions?.maximumFractionDigits != null || formatOptions?.minimumFractionDigits != null;
10032
+ const maxFrac = formatOptions?.maximumFractionDigits;
10033
+ const committed = hasExplicitPrecision && typeof maxFrac === 'number' ? Number(parsedValue.toFixed(maxFrac)) : parsedValue;
10034
+ const nextEventDetails = DrawerRoot.createGenericEventDetails(DrawerRoot.inputBlur, event.nativeEvent);
10035
+ const shouldUpdateValue = value !== committed;
10036
+ const shouldCommit = hadManualInput || shouldUpdateValue || hadPendingProgrammaticChange;
10037
+ if (validationMode === 'onBlur') {
10038
+ validation.commit(committed);
10039
+ }
10040
+ if (shouldUpdateValue) {
10041
+ blockRevalidationRef.current = true;
10042
+ setValue(committed, DrawerRoot.createChangeEventDetails(DrawerRoot.inputBlur, event.nativeEvent));
10043
+ }
10044
+ if (shouldCommit) {
10045
+ onValueCommitted(committed, nextEventDetails);
10046
+ }
10047
+
10048
+ // Normalize only the displayed text
10049
+ const canonicalText = formatNumber(committed, locale, formatOptions);
10050
+ const maxPrecisionText = formatNumberMaxPrecision(parsedValue, locale, formatOptions);
10051
+ const shouldPreserveFullPrecision = !hasExplicitPrecision && parsedValue === value && inputValue === maxPrecisionText;
10052
+ if (!shouldPreserveFullPrecision && inputValue !== canonicalText) {
10053
+ setInputValue(canonicalText);
10054
+ }
10055
+ },
10056
+ onChange(event) {
10057
+ // Workaround for https://github.com/facebook/react/issues/9023
10058
+ if (event.nativeEvent.defaultPrevented) {
10059
+ return;
10060
+ }
10061
+ allowInputSyncRef.current = false;
10062
+ const targetValue = event.target.value;
10063
+ if (targetValue.trim() === '') {
10064
+ setInputValue(targetValue);
10065
+ setValue(null, DrawerRoot.createChangeEventDetails(DrawerRoot.inputClear, event.nativeEvent));
10066
+ return;
10067
+ }
10068
+
10069
+ // Update the input text immediately and only fire onValueChange if the typed value is
10070
+ // currently parseable into a number. This preserves good UX for IME
10071
+ // composition/partial input while still providing live numeric updates when possible.
10072
+ const allowedNonNumericKeys = getAllowedNonNumericKeys();
10073
+ const isValidCharacterString = Array.from(targetValue).every(ch => {
10074
+ const isAsciiDigit = ch >= '0' && ch <= '9';
10075
+ const isArabicNumeral = ARABIC_DETECT_RE.test(ch);
10076
+ const isHanNumeral = HAN_DETECT_RE.test(ch);
10077
+ const isPersianNumeral = PERSIAN_DETECT_RE.test(ch);
10078
+ const isFullwidthNumeral = FULLWIDTH_DETECT_RE.test(ch);
10079
+ const isMinus = ANY_MINUS_DETECT_RE.test(ch);
10080
+ return isAsciiDigit || isArabicNumeral || isHanNumeral || isPersianNumeral || isFullwidthNumeral || isMinus || allowedNonNumericKeys.has(ch);
10081
+ });
10082
+ if (!isValidCharacterString) {
10083
+ return;
10084
+ }
10085
+ const parsedValue = parseNumber(targetValue, locale, formatOptionsRef.current);
10086
+ setInputValue(targetValue);
10087
+ if (parsedValue !== null) {
10088
+ setValue(parsedValue, DrawerRoot.createChangeEventDetails(DrawerRoot.inputChange, event.nativeEvent));
10089
+ }
10090
+ },
10091
+ onKeyDown(event) {
10092
+ if (event.defaultPrevented || readOnly || disabled) {
10093
+ return;
10094
+ }
10095
+ const nativeEvent = event.nativeEvent;
10096
+ allowInputSyncRef.current = true;
10097
+ const allowedNonNumericKeys = getAllowedNonNumericKeys();
10098
+ let isAllowedNonNumericKey = allowedNonNumericKeys.has(event.key);
10099
+ const {
10100
+ decimal,
10101
+ currency,
10102
+ percentSign
10103
+ } = getNumberLocaleDetails(locale, formatOptionsRef.current);
10104
+ const selectionStart = event.currentTarget.selectionStart;
10105
+ const selectionEnd = event.currentTarget.selectionEnd;
10106
+ const isAllSelected = selectionStart === 0 && selectionEnd === inputValue.length;
10107
+
10108
+ // Normalize handling of plus/minus signs via precomputed regexes
10109
+ const selectionContainsIndex = index => selectionStart != null && selectionEnd != null && index >= selectionStart && index < selectionEnd;
10110
+ if (ANY_MINUS_DETECT_RE.test(event.key) && Array.from(allowedNonNumericKeys).some(k => ANY_MINUS_DETECT_RE.test(k || ''))) {
10111
+ // Only allow one sign unless replacing the existing one or all text is selected
10112
+ const existingIndex = inputValue.search(ANY_MINUS_RE);
10113
+ const isReplacingExisting = existingIndex != null && existingIndex !== -1 && selectionContainsIndex(existingIndex);
10114
+ isAllowedNonNumericKey = !(ANY_MINUS_DETECT_RE.test(inputValue) || ANY_PLUS_DETECT_RE.test(inputValue)) || isAllSelected || isReplacingExisting;
10115
+ }
10116
+ if (ANY_PLUS_DETECT_RE.test(event.key) && Array.from(allowedNonNumericKeys).some(k => ANY_PLUS_DETECT_RE.test(k || ''))) {
10117
+ const existingIndex = inputValue.search(ANY_PLUS_RE);
10118
+ const isReplacingExisting = existingIndex != null && existingIndex !== -1 && selectionContainsIndex(existingIndex);
10119
+ isAllowedNonNumericKey = !(ANY_MINUS_DETECT_RE.test(inputValue) || ANY_PLUS_DETECT_RE.test(inputValue)) || isAllSelected || isReplacingExisting;
10120
+ }
10121
+
10122
+ // Only allow one of each symbol.
10123
+ [decimal, currency, percentSign].forEach(symbol => {
10124
+ if (event.key === symbol) {
10125
+ const symbolIndex = inputValue.indexOf(symbol);
10126
+ const isSymbolHighlighted = selectionContainsIndex(symbolIndex);
10127
+ isAllowedNonNumericKey = !inputValue.includes(symbol) || isAllSelected || isSymbolHighlighted;
10128
+ }
10129
+ });
10130
+ const isAsciiDigit = event.key >= '0' && event.key <= '9';
10131
+ const isArabicNumeral = ARABIC_DETECT_RE.test(event.key);
10132
+ const isHanNumeral = HAN_DETECT_RE.test(event.key);
10133
+ const isFullwidthNumeral = FULLWIDTH_DETECT_RE.test(event.key);
10134
+ const isNavigateKey = NAVIGATE_KEYS.has(event.key);
10135
+ if (
10136
+ // Allow composition events (e.g., pinyin)
10137
+ // event.nativeEvent.isComposing does not work in Safari:
10138
+ // https://bugs.webkit.org/show_bug.cgi?id=165004
10139
+ event.which === 229 || event.altKey || event.ctrlKey || event.metaKey || isAllowedNonNumericKey || isAsciiDigit || isArabicNumeral || isFullwidthNumeral || isHanNumeral || isNavigateKey) {
10140
+ return;
10141
+ }
10142
+
10143
+ // We need to commit the number at this point if the input hasn't been blurred.
10144
+ const parsedValue = parseNumber(inputValue, locale, formatOptionsRef.current);
10145
+ const amount = getStepAmount(event) ?? DEFAULT_STEP;
10146
+
10147
+ // Prevent insertion of text or caret from moving.
10148
+ DrawerRoot.stopEvent(event);
10149
+ const commitDetails = DrawerRoot.createGenericEventDetails(DrawerRoot.keyboard, nativeEvent);
10150
+ if (event.key === 'ArrowUp') {
10151
+ incrementValue(amount, {
10152
+ direction: 1,
10153
+ currentValue: parsedValue,
10154
+ event: nativeEvent,
10155
+ reason: DrawerRoot.keyboard
10156
+ });
10157
+ onValueCommitted(lastChangedValueRef.current ?? valueRef.current, commitDetails);
10158
+ } else if (event.key === 'ArrowDown') {
10159
+ incrementValue(amount, {
10160
+ direction: -1,
10161
+ currentValue: parsedValue,
10162
+ event: nativeEvent,
10163
+ reason: DrawerRoot.keyboard
10164
+ });
10165
+ onValueCommitted(lastChangedValueRef.current ?? valueRef.current, commitDetails);
10166
+ } else if (event.key === 'Home' && min != null) {
10167
+ setValue(min, DrawerRoot.createChangeEventDetails(DrawerRoot.keyboard, nativeEvent));
10168
+ onValueCommitted(lastChangedValueRef.current ?? valueRef.current, commitDetails);
10169
+ } else if (event.key === 'End' && max != null) {
10170
+ setValue(max, DrawerRoot.createChangeEventDetails(DrawerRoot.keyboard, nativeEvent));
10171
+ onValueCommitted(lastChangedValueRef.current ?? valueRef.current, commitDetails);
10172
+ }
10173
+ },
10174
+ onPaste(event) {
10175
+ if (event.defaultPrevented || readOnly || disabled) {
10176
+ return;
10177
+ }
10178
+
10179
+ // Prevent `onChange` from being called.
10180
+ event.preventDefault();
10181
+ const clipboardData = event.clipboardData || window.Clipboard;
10182
+ const pastedData = clipboardData.getData('text/plain');
10183
+ const parsedValue = parseNumber(pastedData, locale, formatOptionsRef.current);
10184
+ if (parsedValue !== null) {
10185
+ allowInputSyncRef.current = false;
10186
+ setValue(parsedValue, DrawerRoot.createChangeEventDetails(DrawerRoot.inputPaste, event.nativeEvent));
10187
+ setInputValue(pastedData);
10188
+ }
10189
+ }
10190
+ };
10191
+ const element = useRenderElement.useRenderElement('input', componentProps, {
10192
+ ref: [forwardedRef, inputRef],
10193
+ state,
10194
+ props: [inputProps, validation.getValidationProps(), elementProps],
10195
+ stateAttributesMapping
10196
+ });
10197
+ return element;
10198
+ });
10199
+ if (process.env.NODE_ENV !== "production") NumberFieldInput.displayName = "NumberFieldInput";
10200
+
10201
+ const NumberFieldScrubAreaContext = /*#__PURE__*/React__namespace.createContext(undefined);
10202
+ if (process.env.NODE_ENV !== "production") NumberFieldScrubAreaContext.displayName = "NumberFieldScrubAreaContext";
10203
+ function useNumberFieldScrubAreaContext() {
10204
+ const context = React__namespace.useContext(NumberFieldScrubAreaContext);
10205
+ if (context === undefined) {
10206
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: NumberFieldScrubAreaContext is missing. NumberFieldScrubArea parts must be placed within <NumberField.ScrubArea>.' : useRenderElement.formatErrorMessage(44));
10207
+ }
10208
+ return context;
10209
+ }
10210
+
10211
+ // Calculates the viewport rect for the virtual cursor.
10212
+ function getViewportRect(teleportDistance, scrubAreaEl) {
10213
+ const win = index_esm.getWindow(scrubAreaEl);
10214
+ const rect = scrubAreaEl.getBoundingClientRect();
10215
+ if (rect && teleportDistance != null) {
10216
+ return {
10217
+ x: rect.left - teleportDistance / 2,
10218
+ y: rect.top - teleportDistance / 2,
10219
+ width: rect.right + teleportDistance / 2,
10220
+ height: rect.bottom + teleportDistance / 2
10221
+ };
10222
+ }
10223
+ const vV = win.visualViewport;
10224
+ if (vV) {
10225
+ return {
10226
+ x: vV.offsetLeft,
10227
+ y: vV.offsetTop,
10228
+ width: vV.offsetLeft + vV.width,
10229
+ height: vV.offsetTop + vV.height
10230
+ };
10231
+ }
10232
+ return {
10233
+ x: 0,
10234
+ y: 0,
10235
+ width: win.document.documentElement.clientWidth,
10236
+ height: win.document.documentElement.clientHeight
10237
+ };
10238
+ }
10239
+
10240
+ // This lets us invert the scale of the cursor to match the OS scale, in which the cursor doesn't
10241
+ // scale with the content on pinch-zoom.
10242
+ function subscribeToVisualViewportResize(element, visualScaleRef) {
10243
+ const vV = index_esm.getWindow(element).visualViewport;
10244
+ if (!vV) {
10245
+ return () => {};
10246
+ }
10247
+ function handleVisualResize() {
10248
+ if (vV) {
10249
+ visualScaleRef.current = vV.scale;
10250
+ }
10251
+ }
10252
+ handleVisualResize();
10253
+ vV.addEventListener('resize', handleVisualResize);
10254
+ return () => {
10255
+ vV.removeEventListener('resize', handleVisualResize);
10256
+ };
10257
+ }
10258
+
10259
+ const NumberFieldScrubArea = /*#__PURE__*/React__namespace.forwardRef(function NumberFieldScrubArea(componentProps, forwardedRef) {
10260
+ const {
10261
+ render,
10262
+ className,
10263
+ direction = 'horizontal',
10264
+ pixelSensitivity = 2,
10265
+ teleportDistance,
10266
+ ...elementProps
10267
+ } = componentProps;
10268
+ const {
10269
+ state,
10270
+ setIsScrubbing: setRootScrubbing,
10271
+ disabled,
10272
+ readOnly,
10273
+ inputRef,
10274
+ incrementValue,
10275
+ getStepAmount,
10276
+ onValueCommitted,
10277
+ lastChangedValueRef,
10278
+ valueRef
10279
+ } = useNumberFieldRootContext();
10280
+ const scrubAreaRef = React__namespace.useRef(null);
10281
+ const isScrubbingRef = React__namespace.useRef(false);
10282
+ const didMoveRef = React__namespace.useRef(false);
10283
+ const pointerDownTargetRef = React__namespace.useRef(null);
10284
+ const scrubAreaCursorRef = React__namespace.useRef(null);
10285
+ const virtualCursorCoords = React__namespace.useRef({
10286
+ x: 0,
10287
+ y: 0
10288
+ });
10289
+ const visualScaleRef = React__namespace.useRef(1);
10290
+ const exitPointerLockTimeout = DrawerRoot.useTimeout();
10291
+ const [isTouchInput, setIsTouchInput] = React__namespace.useState(false);
10292
+ const [isPointerLockDenied, setIsPointerLockDenied] = React__namespace.useState(false);
10293
+ const [isScrubbing, setIsScrubbing] = React__namespace.useState(false);
10294
+ React__namespace.useEffect(() => {
10295
+ if (!isScrubbing || !scrubAreaCursorRef.current) {
10296
+ return undefined;
10297
+ }
10298
+ return subscribeToVisualViewportResize(scrubAreaCursorRef.current, visualScaleRef);
10299
+ }, [isScrubbing]);
10300
+ function updateCursorTransform(x, y) {
10301
+ if (scrubAreaCursorRef.current) {
10302
+ scrubAreaCursorRef.current.style.transform = `translate3d(${x}px,${y}px,0) scale(${1 / visualScaleRef.current})`;
10303
+ }
10304
+ }
10305
+ const onScrub = DrawerRoot.useStableCallback(({
10306
+ movementX,
10307
+ movementY
10308
+ }) => {
10309
+ const virtualCursor = scrubAreaCursorRef.current;
10310
+ const scrubAreaEl = scrubAreaRef.current;
10311
+ if (!virtualCursor || !scrubAreaEl) {
10312
+ return;
10313
+ }
10314
+ const rect = getViewportRect(teleportDistance, scrubAreaEl);
10315
+ const coords = virtualCursorCoords.current;
10316
+ const newCoords = {
10317
+ x: Math.round(coords.x + movementX),
10318
+ y: Math.round(coords.y + movementY)
10319
+ };
10320
+ const cursorWidth = virtualCursor.offsetWidth;
10321
+ const cursorHeight = virtualCursor.offsetHeight;
10322
+ if (newCoords.x + cursorWidth / 2 < rect.x) {
10323
+ newCoords.x = rect.width - cursorWidth / 2;
10324
+ } else if (newCoords.x + cursorWidth / 2 > rect.width) {
10325
+ newCoords.x = rect.x - cursorWidth / 2;
10326
+ }
10327
+ if (newCoords.y + cursorHeight / 2 < rect.y) {
10328
+ newCoords.y = rect.height - cursorHeight / 2;
10329
+ } else if (newCoords.y + cursorHeight / 2 > rect.height) {
10330
+ newCoords.y = rect.y - cursorHeight / 2;
10331
+ }
10332
+ virtualCursorCoords.current = newCoords;
10333
+ updateCursorTransform(newCoords.x, newCoords.y);
10334
+ });
10335
+ const onScrubbingChange = DrawerRoot.useStableCallback((scrubbingValue, {
10336
+ clientX,
10337
+ clientY
10338
+ }) => {
10339
+ ReactDOM__namespace.flushSync(() => {
10340
+ setIsScrubbing(scrubbingValue);
10341
+ setRootScrubbing(scrubbingValue);
10342
+ });
10343
+ const virtualCursor = scrubAreaCursorRef.current;
10344
+ if (!virtualCursor || !scrubbingValue) {
10345
+ return;
10346
+ }
10347
+ const initialCoords = {
10348
+ x: clientX - virtualCursor.offsetWidth / 2,
10349
+ y: clientY - virtualCursor.offsetHeight / 2
10350
+ };
10351
+ virtualCursorCoords.current = initialCoords;
10352
+ updateCursorTransform(initialCoords.x, initialCoords.y);
10353
+ });
10354
+ React__namespace.useEffect(function registerGlobalScrubbingEventListeners() {
10355
+ // Only listen while actively scrubbing; avoids unrelated pointerup events committing.
10356
+ if (!inputRef.current || disabled || readOnly || !isScrubbing) {
10357
+ return undefined;
10358
+ }
10359
+ let cumulativeDelta = 0;
10360
+ function handleScrubPointerUp(event) {
10361
+ function handler() {
10362
+ try {
10363
+ DrawerRoot.ownerDocument(scrubAreaRef.current).exitPointerLock();
10364
+ } catch {
10365
+ // Ignore errors.
10366
+ } finally {
10367
+ isScrubbingRef.current = false;
10368
+ onScrubbingChange(false, event);
10369
+ onValueCommitted(lastChangedValueRef.current ?? valueRef.current, DrawerRoot.createGenericEventDetails(DrawerRoot.scrub, event));
10370
+
10371
+ // Manually dispatch a click event if no movement happened, since
10372
+ // preventDefault on pointerdown prevents the browser click event.
10373
+ if (!didMoveRef.current && pointerDownTargetRef.current != null) {
10374
+ pointerDownTargetRef.current.dispatchEvent(new MouseEvent('click', {
10375
+ bubbles: true,
10376
+ cancelable: true
10377
+ }));
10378
+ }
10379
+ didMoveRef.current = false;
10380
+ pointerDownTargetRef.current = null;
10381
+ }
10382
+ }
10383
+ if (DrawerRoot.isFirefox) {
10384
+ // Firefox needs a small delay here when soft-clicking as the pointer
10385
+ // lock will not release otherwise.
10386
+ exitPointerLockTimeout.start(20, handler);
10387
+ } else {
10388
+ handler();
10389
+ }
10390
+ }
10391
+ function handleScrubPointerMove(event) {
10392
+ if (!isScrubbingRef.current) {
10393
+ return;
10394
+ }
10395
+
10396
+ // Prevent text selection.
10397
+ event.preventDefault();
10398
+ onScrub(event);
10399
+ const {
10400
+ movementX,
10401
+ movementY
10402
+ } = event;
10403
+ cumulativeDelta += direction === 'vertical' ? movementY : movementX;
10404
+ if (Math.abs(cumulativeDelta) >= pixelSensitivity) {
10405
+ cumulativeDelta = 0;
10406
+ didMoveRef.current = true;
10407
+ const dValue = direction === 'vertical' ? -movementY : movementX;
10408
+ const stepAmount = getStepAmount(event) ?? DEFAULT_STEP;
10409
+ const rawAmount = dValue * stepAmount;
10410
+ if (rawAmount !== 0) {
10411
+ incrementValue(Math.abs(rawAmount), {
10412
+ direction: rawAmount >= 0 ? 1 : -1,
10413
+ event,
10414
+ reason: DrawerRoot.scrub
10415
+ });
10416
+ }
10417
+ }
10418
+ }
10419
+ const win = index_esm.getWindow(inputRef.current);
10420
+ win.addEventListener('pointerup', handleScrubPointerUp, true);
10421
+ win.addEventListener('pointermove', handleScrubPointerMove, true);
10422
+ return () => {
10423
+ exitPointerLockTimeout.clear();
10424
+ win.removeEventListener('pointerup', handleScrubPointerUp, true);
10425
+ win.removeEventListener('pointermove', handleScrubPointerMove, true);
10426
+ };
10427
+ }, [disabled, readOnly, incrementValue, isScrubbing, getStepAmount, inputRef, onScrubbingChange, onScrub, direction, pixelSensitivity, lastChangedValueRef, onValueCommitted, valueRef, exitPointerLockTimeout]);
10428
+
10429
+ // Prevent scrolling using touch input when scrubbing.
10430
+ React__namespace.useEffect(function registerScrubberTouchPreventListener() {
10431
+ const element = scrubAreaRef.current;
10432
+ if (!element || disabled || readOnly) {
10433
+ return undefined;
10434
+ }
10435
+ function handleTouchStart(event) {
10436
+ if (event.touches.length === 1) {
10437
+ event.preventDefault();
10438
+ }
10439
+ }
10440
+ element.addEventListener('touchstart', handleTouchStart);
10441
+ return () => {
10442
+ element.removeEventListener('touchstart', handleTouchStart);
10443
+ };
10444
+ }, [disabled, readOnly]);
10445
+ const defaultProps = {
10446
+ role: 'presentation',
10447
+ style: {
10448
+ touchAction: 'none',
10449
+ WebkitUserSelect: 'none',
10450
+ userSelect: 'none'
10451
+ },
10452
+ async onPointerDown(event) {
10453
+ const isMainButton = !event.button || event.button === 0;
10454
+ if (event.defaultPrevented || readOnly || !isMainButton || disabled) {
10455
+ return;
10456
+ }
10457
+ const isTouch = event.pointerType === 'touch';
10458
+ setIsTouchInput(isTouch);
10459
+ if (event.pointerType === 'mouse') {
10460
+ event.preventDefault();
10461
+ inputRef.current?.focus();
10462
+ }
10463
+ isScrubbingRef.current = true;
10464
+ didMoveRef.current = false;
10465
+ pointerDownTargetRef.current = event.target;
10466
+ onScrubbingChange(true, event.nativeEvent);
10467
+
10468
+ // WebKit causes significant layout shift with the native message, so we can't use it.
10469
+ if (!isTouch && !DrawerRoot.isWebKit) {
10470
+ try {
10471
+ // Avoid non-deterministic errors in testing environments. This error sometimes
10472
+ // appears:
10473
+ // "The root document of this element is not valid for pointer lock."
10474
+ await DrawerRoot.ownerDocument(scrubAreaRef.current).body.requestPointerLock();
10475
+ setIsPointerLockDenied(false);
10476
+ } catch (error) {
10477
+ setIsPointerLockDenied(true);
10478
+ } finally {
10479
+ if (isScrubbingRef.current) {
10480
+ ReactDOM__namespace.flushSync(() => {
10481
+ onScrubbingChange(true, event.nativeEvent);
10482
+ });
10483
+ }
10484
+ }
10485
+ }
10486
+ }
10487
+ };
10488
+ const element = useRenderElement.useRenderElement('span', componentProps, {
10489
+ ref: [forwardedRef, scrubAreaRef],
10490
+ state,
10491
+ props: [defaultProps, elementProps],
10492
+ stateAttributesMapping: stateAttributesMapping$1
10493
+ });
10494
+ const contextValue = React__namespace.useMemo(() => ({
10495
+ isScrubbing,
10496
+ isTouchInput,
10497
+ isPointerLockDenied,
10498
+ scrubAreaCursorRef,
10499
+ scrubAreaRef,
10500
+ direction,
10501
+ pixelSensitivity,
10502
+ teleportDistance
10503
+ }), [isScrubbing, isTouchInput, isPointerLockDenied, direction, pixelSensitivity, teleportDistance]);
10504
+ return /*#__PURE__*/jsxRuntime.jsx(NumberFieldScrubAreaContext.Provider, {
10505
+ value: contextValue,
10506
+ children: element
10507
+ });
10508
+ });
10509
+ if (process.env.NODE_ENV !== "production") NumberFieldScrubArea.displayName = "NumberFieldScrubArea";
10510
+
10511
+ /**
10512
+ * A custom element to display instead of the native cursor while using the scrub area.
10513
+ * Renders a `<span>` element.
10514
+ *
10515
+ * This component uses the [Pointer Lock API](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API), which may prompt the browser to display a related notification. It is disabled
10516
+ * in Safari to avoid a layout shift that this notification causes there.
10517
+ *
10518
+ * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
10519
+ */
10520
+ const NumberFieldScrubAreaCursor = /*#__PURE__*/React__namespace.forwardRef(function NumberFieldScrubAreaCursor(componentProps, forwardedRef) {
10521
+ const {
10522
+ render,
10523
+ className,
10524
+ ...elementProps
10525
+ } = componentProps;
10526
+ const {
10527
+ state
10528
+ } = useNumberFieldRootContext();
10529
+ const {
10530
+ isScrubbing,
10531
+ isTouchInput,
10532
+ isPointerLockDenied,
10533
+ scrubAreaCursorRef
10534
+ } = useNumberFieldScrubAreaContext();
10535
+ const [domElement, setDomElement] = React__namespace.useState(null);
10536
+ const shouldRender = isScrubbing && !DrawerRoot.isWebKit && !isTouchInput && !isPointerLockDenied;
10537
+ const element = useRenderElement.useRenderElement('span', componentProps, {
10538
+ enabled: shouldRender,
10539
+ ref: [forwardedRef, scrubAreaCursorRef, setDomElement],
10540
+ state,
10541
+ props: [{
10542
+ role: 'presentation',
10543
+ style: {
10544
+ position: 'fixed',
10545
+ top: 0,
10546
+ left: 0,
10547
+ pointerEvents: 'none'
10548
+ }
10549
+ }, elementProps],
10550
+ stateAttributesMapping: stateAttributesMapping$1
10551
+ });
10552
+ return element && /*#__PURE__*/ReactDOM__namespace.createPortal(element, DrawerRoot.ownerDocument(domElement).body);
10553
+ });
10554
+ if (process.env.NODE_ENV !== "production") NumberFieldScrubAreaCursor.displayName = "NumberFieldScrubAreaCursor";
10555
+
10556
+ var index_parts = /*#__PURE__*/Object.freeze({
10557
+ __proto__: null,
10558
+ Decrement: NumberFieldDecrement,
10559
+ Group: NumberFieldGroup,
10560
+ Increment: NumberFieldIncrement,
10561
+ Input: NumberFieldInput,
10562
+ Root: NumberFieldRoot,
10563
+ ScrubArea: NumberFieldScrubArea,
10564
+ ScrubAreaCursor: NumberFieldScrubAreaCursor
10565
+ });
10566
+
8715
10567
  exports.Separator = Separator.Separator;
8716
- exports.Combobox = index_parts$1;
8717
- exports.Drawer = index_parts;
10568
+ exports.Combobox = index_parts$2;
10569
+ exports.Drawer = index_parts$1;
10570
+ exports.NumberField = index_parts;