@jobber/components 8.23.0 → 8.23.1

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 (134) hide show
  1. package/dist/Autocomplete/index.cjs +1 -1
  2. package/dist/Autocomplete/index.mjs +1 -1
  3. package/dist/Card/index.cjs +4 -4
  4. package/dist/Card/index.mjs +4 -4
  5. package/dist/Chip/index.cjs +1 -1
  6. package/dist/Chip/index.mjs +1 -1
  7. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +1 -1
  8. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +1 -1
  9. package/dist/Chips/InternalChipDismissible/index.cjs +1 -1
  10. package/dist/Chips/InternalChipDismissible/index.mjs +1 -1
  11. package/dist/Chips/index.cjs +1 -1
  12. package/dist/Chips/index.mjs +1 -1
  13. package/dist/ComboboxChipRemove-cjs.js +668 -379
  14. package/dist/ComboboxChipRemove-es.js +508 -219
  15. package/dist/ConfirmationModal/index.cjs +1 -1
  16. package/dist/ConfirmationModal/index.mjs +1 -1
  17. package/dist/DataDump/index.cjs +4 -4
  18. package/dist/DataDump/index.mjs +4 -4
  19. package/dist/DataList/components/DataListActions/index.cjs +1 -1
  20. package/dist/DataList/components/DataListActions/index.mjs +1 -1
  21. package/dist/DataList/components/DataListBulkActions/index.cjs +1 -1
  22. package/dist/DataList/components/DataListBulkActions/index.mjs +1 -1
  23. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +1 -1
  24. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +1 -1
  25. package/dist/DataList/components/DataListFilters/index.cjs +1 -1
  26. package/dist/DataList/components/DataListFilters/index.mjs +1 -1
  27. package/dist/DataList/components/DataListHeader/index.cjs +1 -1
  28. package/dist/DataList/components/DataListHeader/index.mjs +1 -1
  29. package/dist/DataList/components/DataListItem/index.cjs +1 -1
  30. package/dist/DataList/components/DataListItem/index.mjs +1 -1
  31. package/dist/DataList/components/DataListItemActions/index.cjs +1 -1
  32. package/dist/DataList/components/DataListItemActions/index.mjs +1 -1
  33. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +1 -1
  34. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +1 -1
  35. package/dist/DataList/components/DataListItems/index.cjs +1 -1
  36. package/dist/DataList/components/DataListItems/index.mjs +1 -1
  37. package/dist/DataList/components/DataListLayout/index.cjs +1 -1
  38. package/dist/DataList/components/DataListLayout/index.mjs +1 -1
  39. package/dist/DataList/components/DataListLayoutActions/index.cjs +1 -1
  40. package/dist/DataList/components/DataListLayoutActions/index.mjs +1 -1
  41. package/dist/DataList/index.cjs +1 -1
  42. package/dist/DataList/index.mjs +1 -1
  43. package/dist/DatePicker/index.cjs +1 -1
  44. package/dist/DatePicker/index.mjs +1 -1
  45. package/dist/Dialog/index.cjs +3 -3
  46. package/dist/Dialog/index.mjs +3 -3
  47. package/dist/DrawerDescription-cjs.js +32 -33
  48. package/dist/DrawerDescription-es.js +19 -20
  49. package/dist/DrawerTitle-cjs.js +7 -5
  50. package/dist/DrawerTitle-es.js +7 -5
  51. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +1 -1
  52. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +1 -1
  53. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +1 -1
  54. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +1 -1
  55. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +1 -1
  56. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +1 -1
  57. package/dist/FilterPicker/index.cjs +1 -1
  58. package/dist/FilterPicker/index.mjs +1 -1
  59. package/dist/FormFieldWrapper-cjs.js +2 -2
  60. package/dist/FormFieldWrapper-es.js +2 -2
  61. package/dist/FormatFile/index.cjs +1 -1
  62. package/dist/FormatFile/index.mjs +1 -1
  63. package/dist/Gallery/index.cjs +1 -1
  64. package/dist/Gallery/index.mjs +1 -1
  65. package/dist/InputDate/index.cjs +1 -1
  66. package/dist/InputDate/index.mjs +1 -1
  67. package/dist/InputNumberExperimental-cjs.js +319 -102
  68. package/dist/InputNumberExperimental-es.js +321 -104
  69. package/dist/LightBox/index.cjs +1 -1
  70. package/dist/LightBox/index.mjs +1 -1
  71. package/dist/Menu/index.cjs +4 -4
  72. package/dist/Menu/index.mjs +4 -4
  73. package/dist/MenuSubmenuTrigger-cjs.js +545 -498
  74. package/dist/MenuSubmenuTrigger-es.js +490 -443
  75. package/dist/Modal/index.cjs +1 -1
  76. package/dist/Modal/index.mjs +1 -1
  77. package/dist/NumberFieldInput-cjs.js +609 -496
  78. package/dist/NumberFieldInput-es.js +612 -498
  79. package/dist/Page/index.cjs +4 -4
  80. package/dist/Page/index.mjs +4 -4
  81. package/dist/Popover/index.cjs +1 -1
  82. package/dist/Popover/index.mjs +1 -1
  83. package/dist/ScrollAreaViewport-cjs.js +326 -601
  84. package/dist/ScrollAreaViewport-es.js +278 -548
  85. package/dist/SelectGroupLabel-cjs.js +494 -528
  86. package/dist/SelectGroupLabel-es.js +411 -445
  87. package/dist/Separator-cjs.js +1 -0
  88. package/dist/Separator-es.js +1 -0
  89. package/dist/Tooltip/index.cjs +1 -1
  90. package/dist/Tooltip/index.mjs +1 -1
  91. package/dist/docs/InputTime/InputTime.md +8 -0
  92. package/dist/docs/SelectPrimitive/SelectPrimitive.md +38 -25
  93. package/dist/floating-ui.react-cjs.js +586 -10
  94. package/dist/floating-ui.react-dom-cjs.js +92 -92
  95. package/dist/floating-ui.react-dom-es.js +1 -1
  96. package/dist/floating-ui.react-es.js +580 -4
  97. package/dist/floating-ui.utils-cjs.js +159 -0
  98. package/dist/floating-ui.utils-es.js +136 -0
  99. package/dist/index.cjs +4 -4
  100. package/dist/index.mjs +4 -4
  101. package/dist/{ToolbarRootContext-cjs.js → isElementDisabled-cjs.js} +5 -0
  102. package/dist/{ToolbarRootContext-es.js → isElementDisabled-es.js} +5 -1
  103. package/dist/primitives/BottomSheet/index.cjs +3 -3
  104. package/dist/primitives/BottomSheet/index.mjs +3 -3
  105. package/dist/primitives/ComboboxPrimitive/index.cjs +2 -2
  106. package/dist/primitives/ComboboxPrimitive/index.mjs +2 -2
  107. package/dist/primitives/InputNumberExperimental/index.cjs +1 -1
  108. package/dist/primitives/InputNumberExperimental/index.mjs +1 -1
  109. package/dist/primitives/SelectPrimitive/index.cjs +5 -5
  110. package/dist/primitives/SelectPrimitive/index.mjs +5 -5
  111. package/dist/primitives/index.cjs +4 -4
  112. package/dist/primitives/index.mjs +4 -4
  113. package/dist/resolveAriaLabelledBy-cjs.js +5 -0
  114. package/dist/resolveAriaLabelledBy-es.js +5 -1
  115. package/dist/{CSPContext-cjs.js → scrollEdges-cjs.js} +30 -2
  116. package/dist/{CSPContext-es.js → scrollEdges-es.js} +28 -3
  117. package/dist/styles.css +9 -3
  118. package/dist/unstyledPrimitives/index.cjs +1217 -639
  119. package/dist/unstyledPrimitives/index.mjs +1163 -585
  120. package/dist/useButton-cjs.js +283 -317
  121. package/dist/useButton-es.js +277 -298
  122. package/dist/useCompositeListItem-cjs.js +328 -279
  123. package/dist/useCompositeListItem-es.js +293 -246
  124. package/dist/useLabel-cjs.js +11 -1
  125. package/dist/useLabel-es.js +11 -2
  126. package/dist/useLabelableId-cjs.js +37 -91
  127. package/dist/useLabelableId-es.js +37 -90
  128. package/dist/useRenderElement-cjs.js +81 -26
  129. package/dist/useRenderElement-es.js +81 -27
  130. package/dist/{InternalBackdrop-cjs.js → useScrollLock-cjs.js} +1286 -1220
  131. package/dist/{InternalBackdrop-es.js → useScrollLock-es.js} +1250 -1207
  132. package/package.json +3 -3
  133. package/dist/index.esm-cjs.js +0 -738
  134. package/dist/index.esm-es.js +0 -712
@@ -1,26 +1,26 @@
1
1
  import * as React from 'react';
2
- import { b as useStableCallback, as as pointer, a as useIsoLayoutEffect, o as ownerDocument, r as contains, g as getTarget, h as useTimeout, u as useBaseUiId, d as createChangeEventDetails, at as swipe, W as useAnimationFrame, aq as TransitionStatusDataAttributes, E as useControlled, Z as itemPress, k as useTransitionStatus, f as useOpenChangeComplete, ab as useAnimationsFinished, af as isFirefox, au as scrub, N as createGenericEventDetails, X as isWebKit, t as transitionStatusMapping } from '../useButton-es.js';
3
- import { A as useCoreFilter, B as AriaCombobox, D as useComboboxInputValueContext, l as ComboboxArrow, i as ComboboxBackdrop, x as ComboboxClear, v as ComboboxCollection, w as ComboboxEmpty, n as ComboboxGroup, o as ComboboxGroupLabel, m as ComboboxIcon, c as ComboboxInput, d as ComboboxInputGroup, p as ComboboxItem, f as ComboboxList, k as ComboboxPopup, h as ComboboxPortal, j as ComboboxPositioner, u as ComboboxRow, g as ComboboxStatus, e as ComboboxTrigger, z as useFilteredItems, s as ComboboxChip, t as ComboboxChipRemove, r as ComboboxChips, q as ComboboxItemIndicator, a as ComboboxLabel, C as ComboboxRoot, b as ComboboxValue, y as useComboboxFilter } from '../ComboboxChipRemove-es.js';
4
- import { i as stringifyAsLabel, k as getDefaultLabelId } from '../resolveAriaLabelledBy-es.js';
2
+ import { A as useCoreFilter, B as AriaCombobox, D as useComboboxInputValueContext, e as ComboboxTrigger, d as ComboboxInputGroup, p as ComboboxItem, l as ComboboxArrow, i as ComboboxBackdrop, x as ComboboxClear, v as ComboboxCollection, w as ComboboxEmpty, n as ComboboxGroup, o as ComboboxGroupLabel, m as ComboboxIcon, c as ComboboxInput, f as ComboboxList, k as ComboboxPopup, h as ComboboxPortal, j as ComboboxPositioner, u as ComboboxRow, g as ComboboxStatus, z as useFilteredItems, s as ComboboxChip, t as ComboboxChipRemove, r as ComboboxChips, q as ComboboxItemIndicator, a as ComboboxLabel, C as ComboboxRoot, b as ComboboxValue, y as useComboboxFilter } from '../ComboboxChipRemove-es.js';
3
+ import { a as stringifyAsLabel, l as getDefaultLabelId } from '../resolveAriaLabelledBy-es.js';
5
4
  import { jsx, jsxs } from 'react/jsx-runtime';
5
+ import { af as pointer, a as useIsoLayoutEffect, b as useStableCallback, o as ownerDocument, q as contains, g as getTarget, h as useTimeout, u as useBaseUiId, d as createChangeEventDetails, ag as swipe, n as useAnimationFrame, B as addEventListener, p as activeElement, ad as TransitionStatusDataAttributes, z as useControlled, V as itemPress, k as useTransitionStatus, f as useOpenChangeComplete, a2 as useAnimationsFinished, ah as scrub, J as createGenericEventDetails, t as transitionStatusMapping } from '../useButton-es.js';
6
6
  import { S as Separator } from '../Separator-es.js';
7
7
  import { a as DialogBackdrop, d as DialogDescription, f as DialogHandle, c as DialogPopup, b as DialogViewport, e as createDialogHandle, D as DrawerDescription } from '../DrawerDescription-es.js';
8
8
  import { c as DialogClose, b as DialogTitle, a as DrawerClose, D as DrawerTitle } from '../DrawerTitle-es.js';
9
- import { i as DialogPortal, g as DialogRoot, h as DialogTrigger, t as useDrawerProviderContext, v as DrawerBackdropCssVars, w as DrawerPopupCssVars, x as DrawerProviderContext, u as useDialogRootContext, y as useDrawerRootContext, s as useTriggerRegistration, z as DrawerPopupDataAttributes, A as useDrawerSnapPoints, B as DrawerViewportContext, C as DRAWER_CONTENT_ATTRIBUTE, c as DrawerBackdrop, f as DrawerContent, d as DrawerPopup, b as DrawerPortal, a as DrawerRoot, D as DrawerTrigger, E as useScrollAreaRootContext, F as getOffset, G as ScrollAreaRootCssVars, H as scrollAreaStateAttributesMapping, I as useScrollAreaViewportContext, S as ScrollAreaRoot, e as ScrollAreaViewport } from '../ScrollAreaViewport-es.js';
9
+ import { i as DialogPortal, g as DialogRoot, h as DialogTrigger, m as useDrawerProviderContext, n as DrawerBackdropCssVars, o as DrawerPopupCssVars, q as DrawerProviderContext, u as useDialogRootContext, r as useDrawerRootContext, s as DrawerPopupDataAttributes, t as useDrawerSnapPoints, v as getSnapPointSwipeMovement, w as DrawerViewportContext, x as DRAWER_CONTENT_ATTRIBUTE, c as DrawerBackdrop, f as DrawerContent, d as DrawerPopup, b as DrawerPortal, a as DrawerRoot, D as DrawerTrigger, y as useScrollAreaRootContext, z as getOffset, A as ScrollAreaRootCssVars, B as scrollAreaStateAttributesMapping, C as useScrollAreaViewportContext, S as ScrollAreaRoot, e as ScrollAreaViewport } from '../ScrollAreaViewport-es.js';
10
10
  import { u as useRenderElement, m as mergeProps, f as formatErrorMessage, N as NOOP, E as EMPTY_OBJECT } from '../useRenderElement-es.js';
11
- import { a as isHTMLElement, f as getComputedStyle, b as isElement, o as getWindow } from '../floating-ui.utils.dom-es.js';
11
+ import { a as isHTMLElement, d as isLastTraversableNode, e as getParentNode, f as getComputedStyle, o as getWindow, b as isElement } from '../floating-ui.utils.dom-es.js';
12
12
  import { c as clamp } from '../clamp-es.js';
13
- import { C as CommonPopupDataAttributes, $ as BASE_UI_SWIPE_IGNORE_SELECTOR, p as popupStateMapping, n as useDirection, A as inertValue, r as useStore, G as triggerOpenStateMapping } from '../InternalBackdrop-es.js';
13
+ import { C as CommonPopupDataAttributes, aj as useTriggerRegistration, am as BASE_UI_SWIPE_IGNORE_SELECTOR, a0 as isInteractiveElement, p as popupStateMapping, r as useDirection, G as inertValue, X as mergeCleanups, al as gecko, J as webkit, w as useStore, W as triggerOpenStateMapping } from '../useScrollLock-es.js';
14
14
  import * as ReactDOM from 'react-dom';
15
15
  import { u as useMenuRootContext, o as useMenuPositionerContext, p as useMenuItem, R as REGULAR_ITEM, q as itemMapping, r as MenuStore, h as MenuBackdrop, l as MenuGroup, n as MenuGroupLabel, b as MenuItem, a as MenuLinkItem, j as MenuPopup, g as MenuPortal, i as MenuPositioner, k as MenuRadioGroup, c as MenuRadioItem, d as MenuRadioItemIndicator, f as MenuRoot, m as MenuSubmenuRoot, e as MenuSubmenuTrigger, M as MenuTrigger } from '../MenuSubmenuTrigger-es.js';
16
- import { e as useCompositeListItem } from '../useCompositeListItem-es.js';
17
- import { y as round } from '../index.esm-es.js';
18
- import { u as useNumberFieldRootContext, D as DEFAULT_STEP, s as stateAttributesMapping$6, d as NumberFieldDecrement, a as NumberFieldGroup, c as NumberFieldIncrement, b as NumberFieldInput, N as NumberFieldRoot } from '../NumberFieldInput-es.js';
19
- import { u as useSelectRootContext, s as selectors, l as useSelectPositionerContext, j as SelectGroup, k as SelectGroupLabel, g as SelectItem, i as SelectItemIndicator, h as SelectItemText, f as SelectList, e as SelectPopup, a as SelectPortal, d as SelectPositioner, S as SelectRoot, b as SelectTrigger, c as SelectValue } from '../SelectGroupLabel-es.js';
16
+ import { h as useCompositeListItem } from '../useCompositeListItem-es.js';
17
+ import { u as usePreviousValue, l as useSelectRootContext, s as selectors, m as useSelectPositionerContext, j as SelectGroup, k as SelectGroupLabel, g as SelectItem, i as SelectItemIndicator, h as SelectItemText, f as SelectList, e as SelectPopup, a as SelectPortal, d as SelectPositioner, S as SelectRoot, b as SelectTrigger, c as SelectValue } from '../SelectGroupLabel-es.js';
18
+ import { v as round } from '../floating-ui.utils-es.js';
19
+ import { u as useNumberFieldRootContext, s as stateAttributesMapping$6, d as NumberFieldDecrement, a as NumberFieldGroup, c as NumberFieldIncrement, b as NumberFieldInput, N as NumberFieldRoot } from '../NumberFieldInput-es.js';
20
20
  import { b as useFieldRootContext, f as fieldValidityMapping } from '../useLabelableId-es.js';
21
21
  import { u as useLabel } from '../useLabel-es.js';
22
- import '../CSPContext-es.js';
23
- import '../ToolbarRootContext-es.js';
22
+ import { g as getMaxScrollOffset, n as normalizeScrollOffset, S as SCROLL_EDGE_TOLERANCE_PX } from '../scrollEdges-es.js';
23
+ import '../isElementDisabled-es.js';
24
24
  import '../floating-ui.react-dom-es.js';
25
25
 
26
26
  function AutocompleteRoot(props) {
@@ -56,13 +56,6 @@ function AutocompleteRoot(props) {
56
56
  } else {
57
57
  resolvedInputValue = internalValue;
58
58
  }
59
- const handleValueChange = useStableCallback((nextValue, eventDetails) => {
60
- setInlineInputValue('');
61
- if (!isControlled) {
62
- setInternalValue(nextValue);
63
- }
64
- onValueChange?.(nextValue, eventDetails);
65
- });
66
59
  const collator = useCoreFilter();
67
60
  const baseFilter = React.useMemo(() => {
68
61
  if (other.filter !== undefined) {
@@ -84,7 +77,14 @@ function AutocompleteRoot(props) {
84
77
  return baseFilter(item, resolvedQuery, toString);
85
78
  };
86
79
  }, [baseFilter, mode, resolvedQuery, staticItems]);
87
- const handleItemHighlighted = useStableCallback((highlightedValue, eventDetails) => {
80
+ function handleValueChange(nextValue, eventDetails) {
81
+ setInlineInputValue('');
82
+ if (!isControlled) {
83
+ setInternalValue(nextValue);
84
+ }
85
+ onValueChange?.(nextValue, eventDetails);
86
+ }
87
+ function handleItemHighlighted(highlightedValue, eventDetails) {
88
88
  props.onItemHighlighted?.(highlightedValue, eventDetails);
89
89
  if (eventDetails.reason === pointer) {
90
90
  return;
@@ -98,7 +98,7 @@ function AutocompleteRoot(props) {
98
98
  } else {
99
99
  setInlineInputValue('');
100
100
  }
101
- });
101
+ }
102
102
  return /*#__PURE__*/jsx(AriaCombobox, {
103
103
  ...other,
104
104
  itemToStringLabel: itemToStringValue,
@@ -132,6 +132,30 @@ function AutocompleteValue(props) {
132
132
  });
133
133
  }
134
134
 
135
+ /**
136
+ * A button that opens the popup.
137
+ * Renders a `<button>` element.
138
+ *
139
+ * Documentation: [Base UI Autocomplete](https://base-ui.com/react/components/autocomplete)
140
+ */
141
+ const AutocompleteTrigger = ComboboxTrigger;
142
+
143
+ /**
144
+ * A wrapper for the input and its associated controls.
145
+ * Renders a `<div>` element.
146
+ *
147
+ * Documentation: [Base UI Autocomplete](https://base-ui.com/react/components/autocomplete)
148
+ */
149
+ const AutocompleteInputGroup = ComboboxInputGroup;
150
+
151
+ /**
152
+ * An individual item in the list.
153
+ * Renders a `<div>` element.
154
+ *
155
+ * Documentation: [Base UI Autocomplete](https://base-ui.com/react/components/autocomplete)
156
+ */
157
+ const AutocompleteItem = ComboboxItem;
158
+
135
159
  var index_parts$7 = /*#__PURE__*/Object.freeze({
136
160
  __proto__: null,
137
161
  Arrow: ComboboxArrow,
@@ -143,8 +167,8 @@ var index_parts$7 = /*#__PURE__*/Object.freeze({
143
167
  GroupLabel: ComboboxGroupLabel,
144
168
  Icon: ComboboxIcon,
145
169
  Input: ComboboxInput,
146
- InputGroup: ComboboxInputGroup,
147
- Item: ComboboxItem,
170
+ InputGroup: AutocompleteInputGroup,
171
+ Item: AutocompleteItem,
148
172
  List: ComboboxList,
149
173
  Popup: ComboboxPopup,
150
174
  Portal: ComboboxPortal,
@@ -153,7 +177,7 @@ var index_parts$7 = /*#__PURE__*/Object.freeze({
153
177
  Row: ComboboxRow,
154
178
  Separator: Separator,
155
179
  Status: ComboboxStatus,
156
- Trigger: ComboboxTrigger,
180
+ Trigger: AutocompleteTrigger,
157
181
  Value: AutocompleteValue,
158
182
  useFilter: useCoreFilter,
159
183
  useFilteredItems: useFilteredItems
@@ -198,6 +222,7 @@ const DrawerIndent = /*#__PURE__*/React.forwardRef(function DrawerIndent(compone
198
222
  const {
199
223
  render,
200
224
  className,
225
+ style,
201
226
  ...elementProps
202
227
  } = componentProps;
203
228
  const providerContext = useDrawerProviderContext();
@@ -272,6 +297,7 @@ const DrawerIndentBackground = /*#__PURE__*/React.forwardRef(function DrawerInde
272
297
  const {
273
298
  render,
274
299
  className,
300
+ style,
275
301
  ...elementProps
276
302
  } = componentProps;
277
303
  const providerContext = useDrawerProviderContext();
@@ -371,36 +397,49 @@ function createVisualStateStore() {
371
397
  };
372
398
  }
373
399
 
374
- function isScrollable(element, axis) {
400
+ function isScrollable(element, axis,
401
+ // When true, a container that overflows only once extra space is added (e.g. drawer
402
+ // keyboard scroll slack) still counts, as long as it has layout size on the axis.
403
+ allowOverflowIntent = false) {
375
404
  const style = getComputedStyle(element);
376
405
  if (axis === 'vertical') {
377
406
  const overflowY = style.overflowY;
378
- return (overflowY === 'auto' || overflowY === 'scroll') && element.scrollHeight > element.clientHeight;
407
+ if (overflowY !== 'auto' && overflowY !== 'scroll') {
408
+ return false;
409
+ }
410
+ return allowOverflowIntent ? element.clientHeight > 0 : element.scrollHeight > element.clientHeight;
379
411
  }
380
412
  const overflowX = style.overflowX;
381
- return (overflowX === 'auto' || overflowX === 'scroll') && element.scrollWidth > element.clientWidth;
413
+ if (overflowX !== 'auto' && overflowX !== 'scroll') {
414
+ return false;
415
+ }
416
+ return allowOverflowIntent ? element.clientWidth > 0 : element.scrollWidth > element.clientWidth;
382
417
  }
383
418
  function hasScrollableAncestor(target, root, axes) {
419
+ // `getParentNode` crosses shadow boundaries (and slots), so a target inside a shadow root
420
+ // still walks up to scrollable ancestors in the light DOM.
384
421
  let node = target;
385
- while (node && node !== root) {
422
+ while (isHTMLElement(node) && node !== root && !isLastTraversableNode(node)) {
386
423
  for (const axis of axes) {
387
424
  if (isScrollable(node, axis)) {
388
425
  return true;
389
426
  }
390
427
  }
391
- node = node.parentElement;
428
+ node = getParentNode(node);
392
429
  }
393
430
  return false;
394
431
  }
395
- function findScrollableTouchTarget(target, root, axis = 'vertical') {
432
+ function findScrollableTouchTarget(target, root, axis = 'vertical', allowOverflowIntent = false) {
433
+ // `getParentNode` crosses shadow boundaries (and slots), so a target inside a shadow root
434
+ // still reaches a scrollable ancestor in the light DOM.
396
435
  let node = isHTMLElement(target) ? target : null;
397
- while (node && node !== root) {
398
- if (isScrollable(node, axis)) {
436
+ while (isHTMLElement(node) && node !== root && !isLastTraversableNode(node)) {
437
+ if (isScrollable(node, axis, allowOverflowIntent)) {
399
438
  return node;
400
439
  }
401
- node = node.parentElement;
440
+ node = getParentNode(node);
402
441
  }
403
- return isScrollable(root, axis) ? root : null;
442
+ return isScrollable(root, axis, allowOverflowIntent) ? root : null;
404
443
  }
405
444
 
406
445
  function getElementAtPoint(doc, x, y) {
@@ -429,7 +468,7 @@ function getDisplacement(direction, deltaX, deltaY) {
429
468
  }
430
469
  }
431
470
  function getElementTransform(element) {
432
- const computedStyle = window.getComputedStyle(element);
471
+ const computedStyle = getWindow(element).getComputedStyle(element);
433
472
  const transform = computedStyle.transform;
434
473
  let translateX = 0;
435
474
  let translateY = 0;
@@ -458,6 +497,9 @@ function getElementTransform(element) {
458
497
  function getValidTimeStamp(timeStamp) {
459
498
  return Number.isFinite(timeStamp) && timeStamp > 0 ? timeStamp : null;
460
499
  }
500
+ function getDragTransform(dragOffset, scale) {
501
+ return `translate3d(${dragOffset.x}px,${dragOffset.y}px,0) scale(${scale})`;
502
+ }
461
503
  function hasPrimaryMouseButton(buttons) {
462
504
  return buttons % 2 === 1;
463
505
  }
@@ -487,6 +529,7 @@ function useSwipeDismiss(options) {
487
529
  swipeThreshold: swipeThresholdProp,
488
530
  onDismiss,
489
531
  onProgress,
532
+ onCancel,
490
533
  onSwipeStart,
491
534
  onRelease,
492
535
  onSwipingChange,
@@ -513,18 +556,7 @@ function useSwipeDismiss(options) {
513
556
  }, [hasHorizontal, hasVertical]);
514
557
  const [currentSwipeDirection, setCurrentSwipeDirection] = React.useState(undefined);
515
558
  const [isSwiping, setIsSwiping] = React.useState(false);
516
- const [isRealSwipe, setIsRealSwipe] = React.useState(false);
517
559
  const [dragDismissed, setDragDismissed] = React.useState(false);
518
- const [dragOffset, setDragOffset] = React.useState({
519
- x: 0,
520
- y: 0
521
- });
522
- const [initialTransform, setInitialTransform] = React.useState({
523
- x: 0,
524
- y: 0,
525
- scale: 1
526
- });
527
- const [lockedDirection, setLockedDirection] = React.useState(null);
528
560
  const dragStartPosRef = React.useRef({
529
561
  x: 0,
530
562
  y: 0
@@ -546,6 +578,7 @@ function useSwipeDismiss(options) {
546
578
  x: 0,
547
579
  y: 0
548
580
  });
581
+ const lockedDirectionRef = React.useRef(null);
549
582
  const isFirstPointerMoveRef = React.useRef(false);
550
583
  const pendingSwipeRef = React.useRef(false);
551
584
  const pendingSwipeStartPosRef = React.useRef(null);
@@ -565,6 +598,7 @@ function useSwipeDismiss(options) {
565
598
  });
566
599
  const lastProgressDetailsRef = React.useRef(null);
567
600
  const isSwipingRef = React.useRef(false);
601
+ const dragStyleSnapshotRef = React.useRef(null);
568
602
  const setSwiping = useStableCallback(nextSwiping => {
569
603
  if (isSwipingRef.current === nextSwiping) {
570
604
  return;
@@ -610,6 +644,35 @@ function useSwipeDismiss(options) {
610
644
  }
611
645
  onProgress?.(nextProgress, details);
612
646
  });
647
+ const syncDragStyles = useStableCallback(swiping => {
648
+ const element = elementRef.current;
649
+ if (!trackDrag || !element) {
650
+ if (!swiping) {
651
+ dragStyleSnapshotRef.current = null;
652
+ }
653
+ return;
654
+ }
655
+ const style = element.style;
656
+ const dragStyleSnapshot = dragStyleSnapshotRef.current;
657
+ if (swiping) {
658
+ if (!dragStyleSnapshot) {
659
+ dragStyleSnapshotRef.current = [style.transition, style.transform];
660
+ }
661
+ style.transition = 'none';
662
+ } else if (dragStyleSnapshot) {
663
+ [style.transition, style.transform] = dragStyleSnapshot;
664
+ dragStyleSnapshotRef.current = null;
665
+ }
666
+ const dragOffset = dragOffsetRef.current;
667
+ const initialTransform = initialTransformRef.current;
668
+ const deltaX = dragOffset.x - initialTransform.x;
669
+ const deltaY = dragOffset.y - initialTransform.y;
670
+ if (swiping) {
671
+ style.transform = getDragTransform(dragOffset, initialTransform.scale);
672
+ }
673
+ style.setProperty(movementCssVars.x, `${deltaX}px`);
674
+ style.setProperty(movementCssVars.y, `${deltaY}px`);
675
+ });
613
676
  function recordDragSample(offset, timeStamp) {
614
677
  if (timeStamp === null) {
615
678
  return;
@@ -631,18 +694,7 @@ function useSwipeDismiss(options) {
631
694
  const reset = React.useCallback(() => {
632
695
  setCurrentSwipeDirection(undefined);
633
696
  setSwiping(false);
634
- setIsRealSwipe(false);
635
697
  setDragDismissed(false);
636
- setDragOffset({
637
- x: 0,
638
- y: 0
639
- });
640
- setInitialTransform({
641
- x: 0,
642
- y: 0,
643
- scale: 1
644
- });
645
- setLockedDirection(null);
646
698
  updateSwipeProgress(0);
647
699
  swipeThresholdRef.current = swipeThresholdDefault;
648
700
  dragStartPosRef.current = {
@@ -665,6 +717,7 @@ function useSwipeDismiss(options) {
665
717
  x: 0,
666
718
  y: 0
667
719
  };
720
+ lockedDirectionRef.current = null;
668
721
  isFirstPointerMoveRef.current = false;
669
722
  lastMovePosRef.current = null;
670
723
  pendingSwipeRef.current = false;
@@ -682,7 +735,8 @@ function useSwipeDismiss(options) {
682
735
  y: 0
683
736
  };
684
737
  lastProgressDetailsRef.current = null;
685
- }, [setSwiping, swipeThresholdDefault, updateSwipeProgress]);
738
+ syncDragStyles(false);
739
+ }, [setSwiping, swipeThresholdDefault, syncDragStyles, updateSwipeProgress]);
686
740
  React.useEffect(() => {
687
741
  if (typeof swipeThresholdProp !== 'function') {
688
742
  swipeThresholdRef.current = swipeThresholdDefault;
@@ -764,11 +818,6 @@ function useSwipeDismiss(options) {
764
818
  x: transform.x,
765
819
  y: transform.y
766
820
  };
767
- setInitialTransform(transform);
768
- setDragOffset({
769
- x: transform.x,
770
- y: transform.y
771
- });
772
821
  recordDragSample({
773
822
  x: transform.x,
774
823
  y: transform.y
@@ -779,10 +828,10 @@ function useSwipeDismiss(options) {
779
828
  }
780
829
  onSwipeStart?.(event.nativeEvent);
781
830
  setSwiping(true);
782
- setIsRealSwipe(false);
783
- setLockedDirection(null);
831
+ lockedDirectionRef.current = null;
784
832
  isFirstPointerMoveRef.current = true;
785
833
  updateSwipeProgress(0);
834
+ syncDragStyles(true);
786
835
  return true;
787
836
  }
788
837
  function resetPendingSwipeState() {
@@ -800,19 +849,15 @@ function useSwipeDismiss(options) {
800
849
  return;
801
850
  }
802
851
  setSwiping(false);
803
- setIsRealSwipe(false);
804
- setLockedDirection(null);
805
- const resolvedInitialTransform = trackDrag ? initialTransform : initialTransformRef.current;
852
+ lockedDirectionRef.current = null;
853
+ const resolvedInitialTransform = initialTransformRef.current;
806
854
  dragOffsetRef.current = {
807
855
  x: resolvedInitialTransform.x,
808
856
  y: resolvedInitialTransform.y
809
857
  };
810
- setDragOffset({
811
- x: resolvedInitialTransform.x,
812
- y: resolvedInitialTransform.y
813
- });
814
858
  setCurrentSwipeDirection(undefined);
815
859
  sawPrimaryButtonsOnMoveRef.current = false;
860
+ syncDragStyles(false);
816
861
  const element = elementRef.current;
817
862
  if (element) {
818
863
  safelyChangePointerCapture(element, event.pointerId, 'releasePointerCapture');
@@ -822,6 +867,7 @@ function useSwipeDismiss(options) {
822
867
  deltaY: 0,
823
868
  direction: undefined
824
869
  });
870
+ onCancel?.(event.nativeEvent);
825
871
  }
826
872
  function applyDirectionalDamping(deltaX, deltaY) {
827
873
  const exponent = value => value >= 0 ? value ** 0.5 : -(Math.abs(value) ** 0.5);
@@ -942,17 +988,12 @@ function useSwipeDismiss(options) {
942
988
  const deltaY = clientY - dragStartPosRef.current.y;
943
989
  const cancelDeltaY = clientY - swipeCancelBaselineRef.current.y;
944
990
  const cancelDeltaX = clientX - swipeCancelBaselineRef.current.x;
945
- if (!isRealSwipe) {
991
+ let lockedDirection = lockedDirectionRef.current;
992
+ if (lockedDirection === null && hasHorizontal && hasVertical) {
946
993
  const movementDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
947
994
  if (movementDistance >= MIN_DRAG_THRESHOLD) {
948
- setIsRealSwipe(true);
949
- if (lockedDirection === null) {
950
- if (hasHorizontal && hasVertical) {
951
- const absX = Math.abs(deltaX);
952
- const absY = Math.abs(deltaY);
953
- setLockedDirection(absX > absY ? 'horizontal' : 'vertical');
954
- }
955
- }
995
+ lockedDirection = Math.abs(deltaX) > Math.abs(deltaY) ? 'horizontal' : 'vertical';
996
+ lockedDirectionRef.current = lockedDirection;
956
997
  }
957
998
  }
958
999
  let candidate;
@@ -1017,12 +1058,7 @@ function useSwipeDismiss(options) {
1017
1058
  x: newOffsetX,
1018
1059
  y: newOffsetY
1019
1060
  };
1020
- if (trackDrag) {
1021
- setDragOffset({
1022
- x: newOffsetX,
1023
- y: newOffsetY
1024
- });
1025
- }
1061
+ syncDragStyles(true);
1026
1062
  recordDragSample({
1027
1063
  x: newOffsetX,
1028
1064
  y: newOffsetY
@@ -1064,91 +1100,6 @@ function useSwipeDismiss(options) {
1064
1100
  direction: swipeDirectionDetails
1065
1101
  });
1066
1102
  }
1067
- const handleMove = useStableCallback(event => {
1068
- const currentPos = getPrimaryPointerPosition(event);
1069
- if (!currentPos) {
1070
- return;
1071
- }
1072
- if (!('touches' in event)) {
1073
- const hasPrimaryButton = hasPrimaryMouseButton(event.buttons);
1074
- if (hasPrimaryButton) {
1075
- sawPrimaryButtonsOnMoveRef.current = true;
1076
- }
1077
-
1078
- // Cancel the swipe if a non-primary button takes over the interaction.
1079
- // This handles cases where a right-click interrupts dragging.
1080
- const lostPrimaryButtonDuringSwipe = event.buttons === 0 && sawPrimaryButtonsOnMoveRef.current;
1081
- if (event.buttons !== 0 && !hasPrimaryButton || lostPrimaryButtonDuringSwipe) {
1082
- cancelSwipeInteraction(event);
1083
- return;
1084
- }
1085
- }
1086
- if (!isSwiping && pendingSwipeRef.current) {
1087
- if (!isTouchLikeEvent(event) && (event.defaultPrevented || event.nativeEvent.defaultPrevented)) {
1088
- resetPendingSwipeState();
1089
- return;
1090
- }
1091
- const allowedToStart = canStart ? canStart(currentPos, {
1092
- nativeEvent: event.nativeEvent,
1093
- direction: primaryDirection
1094
- }) : true;
1095
- if (allowedToStart) {
1096
- const pendingStartPos = pendingSwipeStartPosRef.current;
1097
- let ignoreScrollableOnStart = false;
1098
- if (isTouchLikeEvent(event)) {
1099
- const element = elementRef.current;
1100
- if (pendingStartPos && element) {
1101
- const target = getTargetAtPoint(currentPos, event.nativeEvent);
1102
- const doc = ownerDocument(element);
1103
- const body = doc.body;
1104
- const scrollTarget = body ? findGestureScrollableTouchTarget(target, body) : null;
1105
- if (scrollTarget && (contains(element, scrollTarget) || contains(scrollTarget, element))) {
1106
- const deltaX = currentPos.x - pendingStartPos.x;
1107
- const deltaY = currentPos.y - pendingStartPos.y;
1108
- const canSwipeFromEdge = canSwipeFromScrollEdgeOnPendingMove(scrollTarget, deltaX, deltaY);
1109
- if (canSwipeFromEdge === false) {
1110
- return;
1111
- }
1112
- if (canSwipeFromEdge === true) {
1113
- ignoreScrollableOnStart = true;
1114
- }
1115
- }
1116
- }
1117
- }
1118
- const started = startSwipeAtPosition(event, currentPos, {
1119
- ignoreScrollableTarget: ignoreScrollableOnStart,
1120
- ignoreScrollableAncestors: ignoreScrollableOnStart
1121
- });
1122
- if (started) {
1123
- if (pendingStartPos && ignoreScrollableOnStart) {
1124
- // Preserve displacement between touchstart and the move that activates swipe from
1125
- // a scroll-edge so quick flicks can dismiss.
1126
- clearPendingSwipeStartState();
1127
- dragStartPosRef.current = pendingStartPos;
1128
- swipeCancelBaselineRef.current = pendingStartPos;
1129
- lastMovePosRef.current = pendingStartPos;
1130
- isFirstPointerMoveRef.current = false;
1131
- } else {
1132
- // Start from the current in-bounds position without dropping follow-up move
1133
- // displacement; this avoids jumps when entering from outside the element while
1134
- // keeping swipe tracking responsive on the next move.
1135
- clearPendingSwipeStartState();
1136
- swipeFromScrollableRef.current = false;
1137
- }
1138
- }
1139
- }
1140
- }
1141
- const previousPos = lastMovePosRef.current;
1142
- const movement = previousPos === null ? {
1143
- x: 0,
1144
- y: 0
1145
- } : {
1146
- x: currentPos.x - previousPos.x,
1147
- y: currentPos.y - previousPos.y
1148
- };
1149
- lastMovePosRef.current = currentPos;
1150
- handleMoveCore(event, currentPos, movement);
1151
- });
1152
1103
  const handleEnd = useStableCallback(event => {
1153
1104
  if (!enabled) {
1154
1105
  return;
@@ -1160,7 +1111,7 @@ function useSwipeDismiss(options) {
1160
1111
  const progressDetails = {
1161
1112
  deltaX: releaseDeltaX,
1162
1113
  deltaY: releaseDeltaY,
1163
- direction: currentSwipeDirection ?? intendedSwipeDirectionRef.current
1114
+ direction: intendedSwipeDirectionRef.current
1164
1115
  };
1165
1116
  if (!isSwipingRef.current) {
1166
1117
  resetPendingSwipeState();
@@ -1168,8 +1119,7 @@ function useSwipeDismiss(options) {
1168
1119
  return;
1169
1120
  }
1170
1121
  setSwiping(false);
1171
- setIsRealSwipe(false);
1172
- setLockedDirection(null);
1122
+ lockedDirectionRef.current = null;
1173
1123
  resetPendingSwipeState();
1174
1124
  sawPrimaryButtonsOnMoveRef.current = false;
1175
1125
  const element = elementRef.current;
@@ -1210,7 +1160,7 @@ function useSwipeDismiss(options) {
1210
1160
  }
1211
1161
  const releaseDecision = onRelease?.({
1212
1162
  event: event.nativeEvent,
1213
- direction: currentSwipeDirection ?? intendedSwipeDirectionRef.current,
1163
+ direction: intendedSwipeDirectionRef.current,
1214
1164
  deltaX,
1215
1165
  deltaY,
1216
1166
  velocityX,
@@ -1224,11 +1174,8 @@ function useSwipeDismiss(options) {
1224
1174
  x: resolvedInitialTransform.x,
1225
1175
  y: resolvedInitialTransform.y
1226
1176
  };
1227
- setDragOffset({
1228
- x: resolvedInitialTransform.x,
1229
- y: resolvedInitialTransform.y
1230
- });
1231
1177
  setCurrentSwipeDirection(undefined);
1178
+ syncDragStyles(false);
1232
1179
  updateSwipeProgress(0, progressDetails);
1233
1180
  return;
1234
1181
  }
@@ -1236,7 +1183,7 @@ function useSwipeDismiss(options) {
1236
1183
  let dismissDirection;
1237
1184
  if (hasReleaseDecision) {
1238
1185
  shouldClose = releaseDecision;
1239
- dismissDirection = currentSwipeDirection ?? intendedSwipeDirectionRef.current ?? primaryDirection;
1186
+ dismissDirection = intendedSwipeDirectionRef.current ?? primaryDirection;
1240
1187
  } else {
1241
1188
  for (const direction of directions) {
1242
1189
  switch (direction) {
@@ -1273,6 +1220,7 @@ function useSwipeDismiss(options) {
1273
1220
  if (shouldClose && dismissDirection) {
1274
1221
  setCurrentSwipeDirection(dismissDirection);
1275
1222
  setDragDismissed(true);
1223
+ syncDragStyles(false);
1276
1224
  onDismiss?.(event.nativeEvent, {
1277
1225
  direction: dismissDirection
1278
1226
  });
@@ -1281,34 +1229,150 @@ function useSwipeDismiss(options) {
1281
1229
  x: resolvedInitialTransform.x,
1282
1230
  y: resolvedInitialTransform.y
1283
1231
  };
1284
- setDragOffset({
1285
- x: resolvedInitialTransform.x,
1286
- y: resolvedInitialTransform.y
1287
- });
1288
1232
  setCurrentSwipeDirection(undefined);
1233
+ syncDragStyles(false);
1289
1234
  updateSwipeProgress(0, progressDetails);
1290
1235
  }
1291
1236
  });
1237
+ const handleMove = useStableCallback(event => {
1238
+ const currentPos = getPrimaryPointerPosition(event);
1239
+ if (!currentPos) {
1240
+ return;
1241
+ }
1242
+ let endAfterMove = false;
1243
+ if (!('touches' in event)) {
1244
+ const hasPrimaryButton = hasPrimaryMouseButton(event.buttons);
1245
+ if (hasPrimaryButton) {
1246
+ sawPrimaryButtonsOnMoveRef.current = true;
1247
+ }
1248
+
1249
+ // Cancel the swipe if a non-primary button takes over the interaction.
1250
+ // This handles cases where a right-click interrupts dragging.
1251
+ if (event.buttons !== 0 && !hasPrimaryButton) {
1252
+ cancelSwipeInteraction(event);
1253
+ return;
1254
+ }
1255
+
1256
+ // A `buttons: 0` pointermove means the primary button was already released, so the gesture is
1257
+ // over even if no pointerup reached us. On fast trackpad flicks this trailing move is
1258
+ // dispatched just before pointerup; treat it as the release (mirroring touchend) instead of
1259
+ // cancelling and snapping the element back.
1260
+ if (event.buttons === 0 && sawPrimaryButtonsOnMoveRef.current) {
1261
+ if (!isSwipingRef.current) {
1262
+ // The gesture never activated — discard it.
1263
+ handleEnd(event);
1264
+ return;
1265
+ }
1266
+ // This release move can itself carry the threshold-crossing displacement (and the peak
1267
+ // release velocity), so let it flow through `handleMoveCore` below to update the drag
1268
+ // offset / velocity sample, then commit the release afterwards.
1269
+ endAfterMove = true;
1270
+ }
1271
+ }
1272
+ if (!isSwiping && pendingSwipeRef.current) {
1273
+ if (!isTouchLikeEvent(event) && (event.defaultPrevented || event.nativeEvent.defaultPrevented)) {
1274
+ resetPendingSwipeState();
1275
+ return;
1276
+ }
1277
+ const allowedToStart = canStart ? canStart(currentPos, {
1278
+ nativeEvent: event.nativeEvent,
1279
+ direction: primaryDirection
1280
+ }) : true;
1281
+ if (allowedToStart) {
1282
+ const pendingStartPos = pendingSwipeStartPosRef.current;
1283
+ let ignoreScrollableOnStart = false;
1284
+ if (isTouchLikeEvent(event)) {
1285
+ const element = elementRef.current;
1286
+ if (pendingStartPos && element) {
1287
+ const target = getTargetAtPoint(currentPos, event.nativeEvent);
1288
+ const doc = ownerDocument(element);
1289
+ const body = doc.body;
1290
+ const scrollTarget = body ? findGestureScrollableTouchTarget(target, body) : null;
1291
+ if (scrollTarget && (contains(element, scrollTarget) || contains(scrollTarget, element))) {
1292
+ const deltaX = currentPos.x - pendingStartPos.x;
1293
+ const deltaY = currentPos.y - pendingStartPos.y;
1294
+ const canSwipeFromEdge = canSwipeFromScrollEdgeOnPendingMove(scrollTarget, deltaX, deltaY);
1295
+ if (canSwipeFromEdge === false) {
1296
+ return;
1297
+ }
1298
+ if (canSwipeFromEdge === true) {
1299
+ ignoreScrollableOnStart = true;
1300
+ }
1301
+ }
1302
+ }
1303
+ }
1304
+ const started = startSwipeAtPosition(event, currentPos, {
1305
+ ignoreScrollableTarget: ignoreScrollableOnStart,
1306
+ ignoreScrollableAncestors: ignoreScrollableOnStart
1307
+ });
1308
+ if (started) {
1309
+ if (pendingStartPos && ignoreScrollableOnStart) {
1310
+ // Preserve displacement between touchstart and the move that activates swipe from
1311
+ // a scroll-edge so quick flicks can dismiss.
1312
+ clearPendingSwipeStartState();
1313
+ dragStartPosRef.current = pendingStartPos;
1314
+ swipeCancelBaselineRef.current = pendingStartPos;
1315
+ lastMovePosRef.current = pendingStartPos;
1316
+ isFirstPointerMoveRef.current = false;
1317
+ } else {
1318
+ // Start from the current in-bounds position without dropping follow-up move
1319
+ // displacement; this avoids jumps when entering from outside the element while
1320
+ // keeping swipe tracking responsive on the next move.
1321
+ clearPendingSwipeStartState();
1322
+ swipeFromScrollableRef.current = false;
1323
+ }
1324
+ }
1325
+ }
1326
+ }
1327
+ const previousPos = lastMovePosRef.current;
1328
+ const movement = previousPos === null ? {
1329
+ x: 0,
1330
+ y: 0
1331
+ } : {
1332
+ x: currentPos.x - previousPos.x,
1333
+ y: currentPos.y - previousPos.y
1334
+ };
1335
+ lastMovePosRef.current = currentPos;
1336
+ handleMoveCore(event, currentPos, movement);
1337
+
1338
+ // `endAfterMove` is only set in the non-touch branch above; the `'touches'` guard re-narrows the
1339
+ // event type for `handleEnd` after the shared move handling has run.
1340
+ if (endAfterMove && !('touches' in event)) {
1341
+ handleEnd(event);
1342
+ }
1343
+ });
1344
+
1345
+ // Feeds a native touchmove into the swipe pipeline. Used by consumers that claim the gesture
1346
+ // in a capture-phase listener and stop it from reaching React's delegated touch handlers.
1347
+ const moveNative = useStableCallback((nativeEvent, currentTarget) => {
1348
+ handleMove({
1349
+ touches: nativeEvent.touches,
1350
+ currentTarget,
1351
+ nativeEvent,
1352
+ defaultPrevented: nativeEvent.defaultPrevented,
1353
+ timeStamp: nativeEvent.timeStamp
1354
+ });
1355
+ });
1292
1356
  const getDragStyles = React.useCallback(() => {
1293
- const resolvedDragOffset = trackDrag ? dragOffset : dragOffsetRef.current;
1294
- const resolvedInitialTransform = trackDrag ? initialTransform : initialTransformRef.current;
1295
- if (!isSwiping && resolvedDragOffset.x === resolvedInitialTransform.x && resolvedDragOffset.y === resolvedInitialTransform.y && !dragDismissed) {
1357
+ const dragOffset = dragOffsetRef.current;
1358
+ const initialTransform = initialTransformRef.current;
1359
+ const deltaX = dragOffset.x - initialTransform.x;
1360
+ const deltaY = dragOffset.y - initialTransform.y;
1361
+ if (!isSwiping && deltaX === 0 && deltaY === 0 && !dragDismissed) {
1296
1362
  return {
1297
1363
  [movementCssVars.x]: '0px',
1298
1364
  [movementCssVars.y]: '0px'
1299
1365
  };
1300
1366
  }
1301
- const deltaX = resolvedDragOffset.x - resolvedInitialTransform.x;
1302
- const deltaY = resolvedDragOffset.y - resolvedInitialTransform.y;
1303
1367
  return {
1304
1368
  transition: isSwiping ? 'none' : undefined,
1305
1369
  // While swiping, freeze the element at its current visual transform so it doesn't snap to the
1306
1370
  // end position.
1307
- transform: isSwiping ? `translateX(${resolvedDragOffset.x}px) translateY(${resolvedDragOffset.y}px) scale(${resolvedInitialTransform.scale})` : undefined,
1371
+ transform: isSwiping ? getDragTransform(dragOffset, initialTransform.scale) : undefined,
1308
1372
  [movementCssVars.x]: `${deltaX}px`,
1309
1373
  [movementCssVars.y]: `${deltaY}px`
1310
1374
  };
1311
- }, [dragDismissed, dragOffset, initialTransform, isSwiping, movementCssVars, trackDrag]);
1375
+ }, [dragDismissed, isSwiping, movementCssVars]);
1312
1376
  const getPointerProps = React.useCallback(() => {
1313
1377
  if (!enabled) {
1314
1378
  return {};
@@ -1337,6 +1401,7 @@ function useSwipeDismiss(options) {
1337
1401
  dragDismissed,
1338
1402
  getPointerProps,
1339
1403
  getTouchProps,
1404
+ moveNative,
1340
1405
  getDragStyles,
1341
1406
  reset
1342
1407
  };
@@ -1417,8 +1482,9 @@ function resolveTouchAction(direction) {
1417
1482
  */
1418
1483
  const DrawerSwipeArea = /*#__PURE__*/React.forwardRef(function DrawerSwipeArea(componentProps, forwardedRef) {
1419
1484
  const {
1420
- className,
1421
1485
  render,
1486
+ className,
1487
+ style,
1422
1488
  disabled = false,
1423
1489
  swipeDirection: swipeDirectionProp,
1424
1490
  ...elementProps
@@ -1446,13 +1512,13 @@ const DrawerSwipeArea = /*#__PURE__*/React.forwardRef(function DrawerSwipeArea(c
1446
1512
  const swipeAreaId = useBaseUiId(componentProps.id);
1447
1513
  const registerTrigger = useTriggerRegistration(swipeAreaId, store);
1448
1514
  const open = store.useState('open');
1449
- const resolvedSwipeDirection = swipeDirectionProp ?? oppositeSwipeDirection[swipeDirection];
1450
- const dismissDirection = oppositeSwipeDirection[resolvedSwipeDirection];
1451
- const enabled = !disabled && (!open || swipeActive);
1452
1515
  const resetDragDelta = useStableCallback(() => {
1453
1516
  dragDeltaRef.current.x = 0;
1454
1517
  dragDeltaRef.current.y = 0;
1455
1518
  });
1519
+ const resolvedSwipeDirection = swipeDirectionProp ?? oppositeSwipeDirection[swipeDirection];
1520
+ const dismissDirection = oppositeSwipeDirection[resolvedSwipeDirection];
1521
+ const enabled = !disabled && (!open || swipeActive);
1456
1522
  function disableDismissForSwipe() {
1457
1523
  releaseDismissTimeout.clear();
1458
1524
  store.context.outsidePressEnabledRef.current = false;
@@ -1592,6 +1658,18 @@ const DrawerSwipeArea = /*#__PURE__*/React.forwardRef(function DrawerSwipeArea(c
1592
1658
  }
1593
1659
  store.setOpen(false, createChangeEventDetails(swipe, event, swipeAreaRef.current ?? undefined));
1594
1660
  }
1661
+ function resetSwipeInteractionState() {
1662
+ swipeStartEventRef.current = null;
1663
+ openedBySwipeRef.current = false;
1664
+ closedOffsetRef.current = null;
1665
+ setSwipeActive(false);
1666
+ }
1667
+ function finishSwipeInteraction() {
1668
+ resetSwipeInteractionState();
1669
+ enableDismissAfterRelease();
1670
+ resetDragDelta();
1671
+ clearSwipeStyles();
1672
+ }
1595
1673
  const swipe$1 = useSwipeDismiss({
1596
1674
  enabled,
1597
1675
  directions: [resolvedSwipeDirection],
@@ -1650,15 +1728,10 @@ const DrawerSwipeArea = /*#__PURE__*/React.forwardRef(function DrawerSwipeArea(c
1650
1728
  } else if (openedBySwipeRef.current) {
1651
1729
  closeDrawer(event);
1652
1730
  }
1653
- swipeStartEventRef.current = null;
1654
- openedBySwipeRef.current = false;
1655
- setSwipeActive(false);
1656
- closedOffsetRef.current = null;
1657
- enableDismissAfterRelease();
1658
- resetDragDelta();
1659
- clearSwipeStyles();
1731
+ finishSwipeInteraction();
1660
1732
  return false;
1661
- }
1733
+ },
1734
+ onCancel: finishSwipeInteraction
1662
1735
  });
1663
1736
  const swipePointerProps = swipe$1.getPointerProps();
1664
1737
  const swipeTouchProps = swipe$1.getTouchProps();
@@ -1668,10 +1741,7 @@ const DrawerSwipeArea = /*#__PURE__*/React.forwardRef(function DrawerSwipeArea(c
1668
1741
  resetSwipe();
1669
1742
  resetDragDelta();
1670
1743
  clearSwipeStyles();
1671
- setSwipeActive(false);
1672
- openedBySwipeRef.current = false;
1673
- swipeStartEventRef.current = null;
1674
- closedOffsetRef.current = null;
1744
+ resetSwipeInteractionState();
1675
1745
  }
1676
1746
  }, [clearSwipeStyles, enabled, resetDragDelta, resetSwipe]);
1677
1747
  React.useEffect(() => {
@@ -1701,6 +1771,7 @@ const DrawerSwipeArea = /*#__PURE__*/React.forwardRef(function DrawerSwipeArea(c
1701
1771
  return;
1702
1772
  }
1703
1773
  swipePointerProps.onPointerDown?.(event);
1774
+
1704
1775
  // Prevent native text selection/drag gestures from competing with swipe-open dragging.
1705
1776
  if (event.cancelable) {
1706
1777
  event.preventDefault();
@@ -1731,6 +1802,12 @@ const DrawerSwipeArea = /*#__PURE__*/React.forwardRef(function DrawerSwipeArea(c
1731
1802
  });
1732
1803
  if (process.env.NODE_ENV !== "production") DrawerSwipeArea.displayName = "DrawerSwipeArea";
1733
1804
 
1805
+ const DrawerVirtualKeyboardContext = /*#__PURE__*/React.createContext(undefined);
1806
+ if (process.env.NODE_ENV !== "production") DrawerVirtualKeyboardContext.displayName = "DrawerVirtualKeyboardContext";
1807
+ function useDrawerVirtualKeyboardContext() {
1808
+ return React.useContext(DrawerVirtualKeyboardContext);
1809
+ }
1810
+
1734
1811
  const MIN_SWIPE_THRESHOLD = 10;
1735
1812
  const FAST_SWIPE_VELOCITY = 0.5;
1736
1813
  const SNAP_VELOCITY_THRESHOLD = 0.5;
@@ -1751,14 +1828,17 @@ const DRAWER_CONTENT_SELECTOR = `[${DRAWER_CONTENT_ATTRIBUTE}]`;
1751
1828
  */
1752
1829
  const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(props, forwardedRef) {
1753
1830
  const {
1754
- className,
1755
1831
  render,
1832
+ className,
1833
+ style,
1756
1834
  children,
1757
1835
  ...elementProps
1758
1836
  } = props;
1759
1837
  const {
1760
1838
  store
1761
1839
  } = useDialogRootContext();
1840
+ const popupRef = store.context.popupRef;
1841
+ const backdropRef = store.context.backdropRef;
1762
1842
  const {
1763
1843
  swipeDirection,
1764
1844
  notifyParentSwipingChange,
@@ -1767,17 +1847,6 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
1767
1847
  snapToSequentialPoints
1768
1848
  } = useDrawerRootContext();
1769
1849
  const providerContext = useDrawerProviderContext();
1770
- const visualStateStore = providerContext?.visualStateStore;
1771
- const open = store.useState('open');
1772
- const mounted = store.useState('mounted');
1773
- const nested = store.useState('nested');
1774
- const nestedOpenDialogCount = store.useState('nestedOpenDialogCount');
1775
- const viewportElement = store.useState('viewportElement');
1776
- const popupElementState = store.useState('popupElement');
1777
- const nestedDrawerOpen = nestedOpenDialogCount > 0;
1778
- const scrollAxis = swipeDirection === 'left' || swipeDirection === 'right' ? 'horizontal' : 'vertical';
1779
- const isVerticalScrollAxis = scrollAxis === 'vertical';
1780
- const crossScrollAxis = isVerticalScrollAxis ? 'horizontal' : 'vertical';
1781
1850
  const {
1782
1851
  snapPoints,
1783
1852
  resolvedSnapPoints,
@@ -1786,15 +1855,28 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
1786
1855
  setActiveSnapPoint,
1787
1856
  popupHeight
1788
1857
  } = useDrawerSnapPoints();
1858
+ const open = store.useState('open');
1859
+ const mounted = store.useState('mounted');
1860
+ const nested = store.useState('nested');
1861
+ const nestedOpenDrawerCount = store.useState('nestedOpenDrawerCount');
1862
+ const viewportElement = store.useState('viewportElement');
1863
+ const popupElementState = store.useState('popupElement');
1864
+ const visualStateStore = providerContext?.visualStateStore;
1865
+ const nestedDrawerOpen = nestedOpenDrawerCount > 0;
1866
+ const scrollAxis = swipeDirection === 'left' || swipeDirection === 'right' ? 'horizontal' : 'vertical';
1867
+ const isVerticalScrollAxis = scrollAxis === 'vertical';
1868
+ const crossScrollAxis = isVerticalScrollAxis ? 'horizontal' : 'vertical';
1789
1869
  const [swipeRelease, setSwipeRelease] = React.useState(null);
1790
1870
  const pendingSwipeCloseSnapPointRef = React.useRef(undefined);
1791
1871
  const resetSwipeRef = React.useRef(null);
1792
1872
  const controlledDismissFrame = useAnimationFrame();
1873
+ const swipingRef = React.useRef(false);
1793
1874
  const nestedSwipeActiveRef = React.useRef(false);
1794
1875
  const lastPointerTypeRef = React.useRef('');
1795
1876
  const ignoreNextTouchStartFromPenRef = React.useRef(false);
1796
1877
  const ignoreTouchSwipeRef = React.useRef(false);
1797
1878
  const touchScrollStateRef = React.useRef(null);
1879
+ const virtualKeyboard = useDrawerVirtualKeyboardContext();
1798
1880
  const snapPointRange = React.useMemo(() => {
1799
1881
  if (!snapPoints || snapPoints.length < 2) {
1800
1882
  return null;
@@ -1838,13 +1920,20 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
1838
1920
  return [swipeDirection];
1839
1921
  }, [snapPoints, swipeDirection]);
1840
1922
  const setSwipeDismissed = useStableCallback(dismissed => {
1841
- setSwipeDismissedElements(store.context.popupRef.current, store.context.backdropRef.current, dismissed);
1923
+ setSwipeDismissedElements(popupRef.current, backdropRef.current, dismissed);
1842
1924
  });
1843
1925
  const clearSwipeRelease = useStableCallback(() => {
1844
1926
  setSwipeDismissed(false);
1845
- store.context.popupRef.current?.removeAttribute(TransitionStatusDataAttributes.endingStyle);
1927
+ popupRef.current?.removeAttribute(TransitionStatusDataAttributes.endingStyle);
1846
1928
  setSwipeRelease(null);
1847
1929
  });
1930
+ const finishNestedSwipe = useStableCallback(() => {
1931
+ if (!nestedSwipeActiveRef.current) {
1932
+ return;
1933
+ }
1934
+ nestedSwipeActiveRef.current = false;
1935
+ notifyParentSwipingChange?.(false);
1936
+ });
1848
1937
  const applySwipeProgress = useStableCallback(({
1849
1938
  resolvedProgress,
1850
1939
  shouldTrackProgress,
@@ -1852,15 +1941,18 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
1852
1941
  }) => {
1853
1942
  const isActive = open && !nested && shouldTrackProgress;
1854
1943
  const swipeProgress = isActive ? resolvedProgress : 0;
1944
+ const nestedSwipeProgress = open && shouldTrackProgress ? resolvedProgress : 0;
1855
1945
  if (notifyParent && notifyParentSwipeProgressChange) {
1856
- const nestedSwipeProgress = open && shouldTrackProgress ? resolvedProgress : 0;
1857
1946
  notifyParentSwipeProgressChange(nestedSwipeProgress);
1947
+ if (nestedSwipeProgress <= 0) {
1948
+ finishNestedSwipe();
1949
+ }
1858
1950
  }
1859
1951
  visualStateStore?.set({
1860
1952
  swipeProgress,
1861
1953
  frontmostHeight: swipeProgress > 0 ? frontmostHeight : 0
1862
1954
  });
1863
- const backdropElement = store.context.backdropRef.current;
1955
+ const backdropElement = backdropRef.current;
1864
1956
  if (!backdropElement) {
1865
1957
  return;
1866
1958
  }
@@ -1955,7 +2047,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
1955
2047
  if ('touches' in event || 'pointerType' in event && event.pointerType === 'touch') {
1956
2048
  return;
1957
2049
  }
1958
- const popupElement = store.context.popupRef.current;
2050
+ const popupElement = popupRef.current;
1959
2051
  if (!popupElement) {
1960
2052
  return;
1961
2053
  }
@@ -1972,10 +2064,10 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
1972
2064
  selection.removeAllRanges();
1973
2065
  },
1974
2066
  onSwipingChange(swiping) {
2067
+ swipingRef.current = swiping;
1975
2068
  setBackdropSwipingAttribute(store.context.backdropRef.current, swiping);
1976
- if (!swiping) {
1977
- nestedSwipeActiveRef.current = false;
1978
- notifyParentSwipingChange?.(false);
2069
+ if (!swiping && !notifyParentSwipeProgressChange) {
2070
+ finishNestedSwipe();
1979
2071
  }
1980
2072
  },
1981
2073
  swipeThreshold({
@@ -2006,9 +2098,16 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2006
2098
  },
2007
2099
  onProgress(progress, details) {
2008
2100
  updateNestedSwipeActive(details);
2101
+ const hasSnapPoints = Boolean(snapPoints && snapPoints.length > 0);
2102
+ if (swipingRef.current && swipeDirection === 'down' && hasSnapPoints && details && Number.isFinite(details.deltaY)) {
2103
+ const popupElement = store.context.popupRef.current;
2104
+ if (popupElement) {
2105
+ popupElement.style.removeProperty('transform');
2106
+ popupElement.style.setProperty(DrawerPopupCssVars.swipeMovementY, `${getSnapPointSwipeMovement(activeSnapPointOffset ?? 0, details.deltaY)}px`);
2107
+ }
2108
+ }
2009
2109
  const currentDirection = details?.direction ?? swipe$1.swipeDirection;
2010
2110
  const isDismissSwipe = currentDirection === undefined || currentDirection === swipeDirection;
2011
- const hasSnapPoints = Boolean(snapPoints && snapPoints.length > 0);
2012
2111
  const isVerticalSwipe = swipeDirection === 'down' || swipeDirection === 'up';
2013
2112
  const shouldTrackProgress = hasSnapPoints && isVerticalSwipe || !hasSnapPoints || swipeDirection === 'left' || swipeDirection === 'right' || isDismissSwipe;
2014
2113
  let resolvedProgress = progress;
@@ -2057,7 +2156,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2057
2156
  if (!popupElement) {
2058
2157
  return;
2059
2158
  }
2060
- notifyParentSwipingChange?.(false);
2159
+ finishNestedSwipe();
2061
2160
  setSwipeDismissed(true);
2062
2161
  popupElement.style.removeProperty('transition');
2063
2162
  popupElement.setAttribute(TransitionStatusDataAttributes.endingStyle, '');
@@ -2264,7 +2363,10 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2264
2363
  });
2265
2364
  const swipePointerProps = swipe$1.getPointerProps();
2266
2365
  const swipeTouchProps = swipe$1.getTouchProps();
2267
- const resetSwipe = swipe$1.reset;
2366
+ const {
2367
+ moveNative: moveSwipeNative,
2368
+ reset: resetSwipe
2369
+ } = swipe$1;
2268
2370
  resetSwipeRef.current = resetSwipe;
2269
2371
  React.useEffect(() => {
2270
2372
  const rootElement = viewportElement ?? popupElementState;
@@ -2275,6 +2377,11 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2275
2377
  const doc = ownerDocument(resolvedRootElement);
2276
2378
  const win = getWindow(doc);
2277
2379
  function handleNativeTouchMove(event) {
2380
+ // The virtual keyboard provider observes the move to tell a tap apart from a drag.
2381
+ // It must run even when the swipe gesture below claims the event with
2382
+ // `stopPropagation()`, which would otherwise prevent React's delegated handlers
2383
+ // (and the provider) from ever seeing the move.
2384
+ virtualKeyboard?.onTouchMove(event);
2278
2385
  if (ignoreTouchSwipeRef.current) {
2279
2386
  return;
2280
2387
  }
@@ -2311,6 +2418,10 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2311
2418
  if (event.cancelable) {
2312
2419
  event.preventDefault();
2313
2420
  }
2421
+ // Claim the gesture before React's delegated touch handlers see it; dispatching the
2422
+ // move through React re-rasterizes the popup content on every frame.
2423
+ event.stopPropagation();
2424
+ moveSwipeNative(event, resolvedRootElement);
2314
2425
  updateTouchScrollPosition(touchState, touch);
2315
2426
  return;
2316
2427
  }
@@ -2321,6 +2432,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2321
2432
  if (event.cancelable) {
2322
2433
  event.preventDefault();
2323
2434
  }
2435
+ event.stopPropagation();
2324
2436
  updateTouchScrollPosition(touchState, touch);
2325
2437
  return;
2326
2438
  }
@@ -2340,18 +2452,17 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2340
2452
  event.preventDefault();
2341
2453
  }
2342
2454
  }
2455
+ if (touchState.allowSwipe === true) {
2456
+ event.stopPropagation();
2457
+ moveSwipeNative(event, resolvedRootElement);
2458
+ }
2343
2459
  updateTouchScrollPosition(touchState, touch);
2344
2460
  }
2345
- doc.addEventListener('touchmove', handleNativeTouchMove, {
2461
+ return addEventListener(doc, 'touchmove', handleNativeTouchMove, {
2346
2462
  passive: false,
2347
2463
  capture: true
2348
2464
  });
2349
- return () => {
2350
- doc.removeEventListener('touchmove', handleNativeTouchMove, {
2351
- capture: true
2352
- });
2353
- };
2354
- }, [mounted, nestedDrawerOpen, open, popupElementState, isVerticalScrollAxis, scrollAxis, swipeDirection, viewportElement]);
2465
+ }, [mounted, nestedDrawerOpen, open, popupElementState, isVerticalScrollAxis, scrollAxis, swipeDirection, moveSwipeNative, viewportElement, virtualKeyboard]);
2355
2466
  React.useEffect(() => {
2356
2467
  if (!snapPointRange || swipe$1.swiping) {
2357
2468
  return;
@@ -2381,23 +2492,30 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2381
2492
  }
2382
2493
  }, [clearSwipeRelease, open, resetSwipe]);
2383
2494
  React.useEffect(() => {
2495
+ const backdropElement = backdropRef.current;
2384
2496
  return () => {
2385
2497
  visualStateStore?.set({
2386
2498
  swipeProgress: 0,
2387
2499
  frontmostHeight: 0
2388
2500
  });
2389
- setBackdropSwipingAttribute(store.context.backdropRef.current, false);
2390
- notifyParentSwipingChange?.(false);
2501
+ setBackdropSwipingAttribute(backdropElement, false);
2502
+ // `data-swiping` is set on whichever backdrop is current when a swipe starts, which can
2503
+ // differ from the captured element if the backdrop mounted late or changed identity.
2504
+ // Reading the live ref here is intentional so the current backdrop is cleared too.
2505
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2506
+ const currentBackdrop = backdropRef.current;
2507
+ if (currentBackdrop !== backdropElement) {
2508
+ setBackdropSwipingAttribute(currentBackdrop, false);
2509
+ }
2510
+ finishNestedSwipe();
2391
2511
  };
2392
- }, [notifyParentSwipingChange, store, visualStateStore]);
2512
+ }, [backdropRef, finishNestedSwipe, visualStateStore]);
2393
2513
  const swipeProviderValue = React.useMemo(() => ({
2394
2514
  swiping: swipe$1.swiping,
2395
2515
  getDragStyles: swipe$1.getDragStyles,
2396
2516
  swipeStrength: swipeRelease ?? null,
2397
- setSwipeDismissed(dismissed) {
2398
- setSwipeDismissedElements(store.context.popupRef.current, store.context.backdropRef.current, dismissed);
2399
- }
2400
- }), [store, swipe$1.getDragStyles, swipe$1.swiping, swipeRelease]);
2517
+ setSwipeDismissed
2518
+ }), [setSwipeDismissed, swipe$1.getDragStyles, swipe$1.swiping, swipeRelease]);
2401
2519
  function resetTouchTrackingState() {
2402
2520
  ignoreTouchSwipeRef.current = false;
2403
2521
  touchScrollStateRef.current = null;
@@ -2407,6 +2525,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2407
2525
  return /*#__PURE__*/jsx(DialogViewport, {
2408
2526
  ref: forwardedRef,
2409
2527
  className: className,
2528
+ style: style,
2410
2529
  render: render,
2411
2530
  ...mergeProps(elementProps, {
2412
2531
  onPointerDown(event) {
@@ -2473,16 +2592,23 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2473
2592
  }
2474
2593
  const doc = ownerDocument(event.currentTarget);
2475
2594
  const elementAtPoint = getElementAtPoint(doc, touch.clientX, touch.clientY);
2595
+ const rootElement = viewportElement ?? popupElementState;
2596
+ const eventTarget = getTarget(event.nativeEvent);
2597
+ const target = isElement(eventTarget) ? eventTarget : null;
2598
+ if (rootElement && target && !contains(rootElement, target)) {
2599
+ ignoreTouchSwipeRef.current = true;
2600
+ touchScrollStateRef.current = null;
2601
+ return;
2602
+ }
2603
+ virtualKeyboard?.onTouchStart(event);
2476
2604
  ignoreTouchSwipeRef.current = isSwipeIgnoredTarget(elementAtPoint);
2477
2605
  if (ignoreTouchSwipeRef.current) {
2478
2606
  touchScrollStateRef.current = null;
2479
2607
  return;
2480
2608
  }
2481
- const rootElement = viewportElement ?? popupElementState;
2482
- const target = isElement(event.target) ? event.target : null;
2483
2609
  let scrollTarget = null;
2484
2610
  let hasCrossAxisScrollableContent = false;
2485
- if (rootElement && target && contains(rootElement, target)) {
2611
+ if (rootElement && target) {
2486
2612
  scrollTarget = findScrollableTouchTarget(target, rootElement, scrollAxis);
2487
2613
  hasCrossAxisScrollableContent = findScrollableTouchTarget(target, rootElement, crossScrollAxis) != null;
2488
2614
  }
@@ -2503,27 +2629,13 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2503
2629
  };
2504
2630
  swipeTouchProps.onTouchStart?.(event);
2505
2631
  },
2506
- onTouchMove(event) {
2507
- if (ignoreTouchSwipeRef.current) {
2508
- return;
2509
- }
2510
- if (isReactTouchEventOnRangeInput(event)) {
2511
- return;
2512
- }
2513
- const touchState = touchScrollStateRef.current;
2514
- if (touchState?.preserveNativeCrossAxisScroll) {
2515
- return;
2516
- }
2517
- if (touchState?.allowSwipe === false || touchState?.scrollTarget != null && !touchState.allowSwipe) {
2518
- return;
2519
- }
2520
- swipeTouchProps.onTouchMove?.(event);
2521
- },
2522
2632
  onTouchEnd(event) {
2633
+ virtualKeyboard?.onTouchEnd(event);
2523
2634
  resetTouchTrackingState();
2524
2635
  swipeTouchProps.onTouchEnd?.(event);
2525
2636
  },
2526
2637
  onTouchCancel(event) {
2638
+ virtualKeyboard?.onTouchCancel();
2527
2639
  resetTouchTrackingState();
2528
2640
  swipeTouchProps.onTouchCancel?.(event);
2529
2641
  },
@@ -2582,114 +2694,582 @@ function hasExpandedSelectionWithinTarget(selection, target) {
2582
2694
  return selection.containsNode(target, true) || contains(target, anchorElement) || contains(target, focusElement);
2583
2695
  }
2584
2696
  function shouldIgnoreSwipeForTextSelection(doc, rootElement) {
2585
- const activeElement = doc.activeElement;
2586
- const activeElementWithinRoot = Boolean(activeElement && contains(rootElement, activeElement));
2587
- if (activeElementWithinRoot && isTextSelectionControl(activeElement)) {
2697
+ const activeEl = activeElement(doc);
2698
+ const activeElementWithinRoot = Boolean(activeEl && contains(rootElement, activeEl));
2699
+ if (activeElementWithinRoot && isTextSelectionControl(activeEl)) {
2588
2700
  const {
2589
2701
  selectionStart,
2590
2702
  selectionEnd
2591
- } = activeElement;
2703
+ } = activeEl;
2592
2704
  if (selectionStart != null && selectionEnd != null && selectionStart < selectionEnd) {
2593
2705
  return true;
2594
2706
  }
2595
- }
2596
- const selection = doc.getSelection?.();
2597
- if (!selection || selection.isCollapsed) {
2598
- return false;
2599
- }
2600
- return hasExpandedSelectionWithinTarget(selection, rootElement);
2601
- }
2602
- function isEventOnRangeInput(event, win) {
2603
- const composedPath = event.composedPath();
2604
- if (composedPath) {
2605
- return composedPath.some(pathTarget => isRangeInput(pathTarget, win));
2606
- }
2607
- return isRangeInput(event.target, win);
2608
- }
2609
- function isReactTouchEventOnRangeInput(event) {
2610
- return isEventOnRangeInput(event.nativeEvent, getWindow(event.currentTarget));
2611
- }
2612
- function updateTouchScrollPosition(touchState, touch) {
2613
- touchState.lastX = touch.clientX;
2614
- touchState.lastY = touch.clientY;
2707
+ }
2708
+ const selection = doc.getSelection?.();
2709
+ if (!selection || selection.isCollapsed) {
2710
+ return false;
2711
+ }
2712
+ return hasExpandedSelectionWithinTarget(selection, rootElement);
2713
+ }
2714
+ function isEventOnRangeInput(event, win) {
2715
+ const composedPath = event.composedPath();
2716
+ if (composedPath) {
2717
+ return composedPath.some(pathTarget => isRangeInput(pathTarget, win));
2718
+ }
2719
+ return isRangeInput(getTarget(event), win);
2720
+ }
2721
+ function isReactTouchEventOnRangeInput(event) {
2722
+ return isEventOnRangeInput(event.nativeEvent, getWindow(event.currentTarget));
2723
+ }
2724
+ function updateTouchScrollPosition(touchState, touch) {
2725
+ touchState.lastX = touch.clientX;
2726
+ touchState.lastY = touch.clientY;
2727
+ }
2728
+ function preserveNativeCrossAxisScrollOnMove(touchState, touch, isVerticalScrollAxis) {
2729
+ if (touchState.preserveNativeCrossAxisScroll) {
2730
+ return true;
2731
+ }
2732
+ if (touchState.allowSwipe === true || !touchState.hasCrossAxisScrollableContent) {
2733
+ return false;
2734
+ }
2735
+ const drawerAxisGestureDelta = isVerticalScrollAxis ? touch.clientY - touchState.startY : touch.clientX - touchState.startX;
2736
+ const crossAxisGestureDelta = isVerticalScrollAxis ? touch.clientX - touchState.startX : touch.clientY - touchState.startY;
2737
+ const absDrawerAxisGestureDelta = Math.abs(drawerAxisGestureDelta);
2738
+ const absCrossAxisGestureDelta = Math.abs(crossAxisGestureDelta);
2739
+ if (absCrossAxisGestureDelta < 6 || absCrossAxisGestureDelta <= absDrawerAxisGestureDelta + 2) {
2740
+ return false;
2741
+ }
2742
+ touchState.preserveNativeCrossAxisScroll = true;
2743
+ return true;
2744
+ }
2745
+ function hasScrollableContentOnAxis(scrollTarget, axis) {
2746
+ return axis === 'vertical' ? scrollTarget.scrollHeight > scrollTarget.clientHeight : scrollTarget.scrollWidth > scrollTarget.clientWidth;
2747
+ }
2748
+ function getScrollMetrics(scrollTarget, axis) {
2749
+ if (axis === 'vertical') {
2750
+ const max = Math.max(0, scrollTarget.scrollHeight - scrollTarget.clientHeight);
2751
+ return {
2752
+ offset: scrollTarget.scrollTop,
2753
+ max
2754
+ };
2755
+ }
2756
+ const max = Math.max(0, scrollTarget.scrollWidth - scrollTarget.clientWidth);
2757
+ return {
2758
+ offset: scrollTarget.scrollLeft,
2759
+ max
2760
+ };
2761
+ }
2762
+ function isAtSwipeStartEdge(scrollTarget, axis, direction) {
2763
+ const {
2764
+ offset,
2765
+ max
2766
+ } = getScrollMetrics(scrollTarget, axis);
2767
+ const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
2768
+ if (dismissFromStartEdge === null) {
2769
+ return false;
2770
+ }
2771
+ return dismissFromStartEdge ? offset <= 0 : offset >= max;
2772
+ }
2773
+ function canSwipeFromScrollEdgeOnMove(scrollTarget, axis, direction, delta) {
2774
+ const {
2775
+ offset,
2776
+ max
2777
+ } = getScrollMetrics(scrollTarget, axis);
2778
+ const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
2779
+ if (dismissFromStartEdge === null) {
2780
+ return false;
2781
+ }
2782
+ const movingTowardDismiss = dismissFromStartEdge ? delta > 0 : delta < 0;
2783
+ if (!movingTowardDismiss) {
2784
+ return false;
2785
+ }
2786
+ return dismissFromStartEdge ? offset <= 0 : offset >= max;
2787
+ }
2788
+ function shouldDismissFromStartEdge(direction, axis) {
2789
+ if (axis === 'vertical') {
2790
+ if (direction === 'down') {
2791
+ return true;
2792
+ }
2793
+ if (direction === 'up') {
2794
+ return false;
2795
+ }
2796
+ return null;
2797
+ }
2798
+ if (direction === 'right') {
2799
+ return true;
2800
+ }
2801
+ if (direction === 'left') {
2802
+ return false;
2803
+ }
2804
+ return null;
2805
+ }
2806
+
2807
+ let DrawerViewportCssVars = /*#__PURE__*/function (DrawerViewportCssVars) {
2808
+ /**
2809
+ * The software keyboard inset, measured from the bottom edge of the layout viewport.
2810
+ * Present only when the drawer is wrapped in `Drawer.VirtualKeyboardProvider`.
2811
+ * @type {CSS length}
2812
+ */
2813
+ DrawerViewportCssVars["keyboardInset"] = "--drawer-keyboard-inset";
2814
+ return DrawerViewportCssVars;
2815
+ }({});
2816
+
2817
+ const KEYBOARD_RESIZE_THRESHOLD = 60;
2818
+ const KEYBOARD_VISIBILITY_MARGIN = 16;
2819
+ // Extra breathing room (px) added below the focused field, on top of its measured
2820
+ // keyboard overlap, so the field can be scrolled clear of the keyboard instead of
2821
+ // ending up flush against it. Only applied when there is actual overlap.
2822
+ const KEYBOARD_SCROLL_SLACK = 48;
2823
+ const INPUT_TAP_MOVE_THRESHOLD = 10;
2824
+ const INPUT_TAP_HIT_SLOP = 16;
2825
+ const KEYBOARD_INPUT_TYPES = new Set(['email', 'number', 'password', 'search', 'tel', 'text', 'url']);
2826
+
2827
+ // Snapshot of a scroll container's relevant styles taken before keyboard slack is
2828
+ // applied. The string fields are the exact inline values to restore on cleanup;
2829
+ // the parsed numbers are the computed baselines that slack is added on top of.
2830
+
2831
+ // Returned by the point-based resolver when the lift point lands on another
2832
+ // interactive/label element. It signals that the tap was intentionally rejected, so the
2833
+ // caller must NOT fall back to the touchstart target (`touchend.target` stays at the
2834
+ // touchstart node on mobile) — doing so would steal a tap meant for that element.
2835
+ const KEYBOARD_TAP_BLOCKED = Symbol('KeyboardTapBlocked');
2836
+
2837
+ /**
2838
+ * Provides keyboard-aware focus and scroll handling for bottom-sheet drawers with form fields.
2839
+ *
2840
+ * Documentation: [Base UI Drawer](https://base-ui.com/react/components/drawer)
2841
+ */
2842
+ function DrawerVirtualKeyboardProvider(props) {
2843
+ const {
2844
+ children
2845
+ } = props;
2846
+ const {
2847
+ store
2848
+ } = useDialogRootContext();
2849
+ const open = store.useState('open');
2850
+ const mounted = store.useState('mounted');
2851
+ const nestedOpenDialogCount = store.useState('nestedOpenDialogCount');
2852
+ const viewportElement = store.useState('viewportElement');
2853
+
2854
+ // The provider requires a `<Drawer.Viewport>` to act as the measurement and containment
2855
+ // root and to host the keyboard inset variable; `<Drawer.Popup>` already warns when the
2856
+ // viewport is missing, so there is no need to fall back to the popup element here.
2857
+ const rootElement = viewportElement;
2858
+ const nestedDrawerOpen = nestedOpenDialogCount > 0;
2859
+ const pendingKeyboardFocusMovedRef = React.useRef(false);
2860
+ const keyboardTouchStartRef = React.useRef(null);
2861
+ const focusedKeyboardTargetRef = React.useRef(null);
2862
+ const keyboardScrollAdjustmentRef = React.useRef(null);
2863
+ const keyboardFocusFrame = useAnimationFrame();
2864
+ const restoreKeyboardScrollAdjustment = useStableCallback(() => {
2865
+ const adjustment = keyboardScrollAdjustmentRef.current;
2866
+ if (!adjustment) {
2867
+ return;
2868
+ }
2869
+ adjustment.element.style.overflowAnchor = adjustment.overflowAnchor;
2870
+ adjustment.element.style.paddingBottom = adjustment.paddingBottom;
2871
+ adjustment.element.style.scrollPaddingBottom = adjustment.scrollPaddingBottom;
2872
+ keyboardScrollAdjustmentRef.current = null;
2873
+ });
2874
+ const setKeyboardScrollSlack = useStableCallback((element, slack) => {
2875
+ const roundedSlack = Math.max(0, Math.ceil(slack));
2876
+ let adjustment = keyboardScrollAdjustmentRef.current;
2877
+ if (adjustment && !adjustment.element.isConnected) {
2878
+ restoreKeyboardScrollAdjustment();
2879
+ adjustment = null;
2880
+ }
2881
+ if (roundedSlack === 0) {
2882
+ restoreKeyboardScrollAdjustment();
2883
+ return;
2884
+ }
2885
+ if (adjustment && adjustment.element !== element) {
2886
+ restoreKeyboardScrollAdjustment();
2887
+ adjustment = null;
2888
+ }
2889
+ if (!adjustment) {
2890
+ const styles = getComputedStyle(element);
2891
+ adjustment = {
2892
+ element,
2893
+ overflowAnchor: element.style.overflowAnchor,
2894
+ paddingBottom: element.style.paddingBottom,
2895
+ scrollPaddingBottom: element.style.scrollPaddingBottom,
2896
+ computedPaddingBottom: Number.parseFloat(styles.paddingBottom) || 0,
2897
+ computedScrollPaddingBottom: Number.parseFloat(styles.scrollPaddingBottom) || 0
2898
+ };
2899
+ keyboardScrollAdjustmentRef.current = adjustment;
2900
+ }
2901
+ element.style.overflowAnchor = 'none';
2902
+ element.style.paddingBottom = `${adjustment.computedPaddingBottom + roundedSlack}px`;
2903
+ element.style.scrollPaddingBottom = `${adjustment.computedScrollPaddingBottom + KEYBOARD_VISIBILITY_MARGIN}px`;
2904
+ });
2905
+ const animateKeyboardScroll = useStableCallback((element, scrollTop) => {
2906
+ const win = getWindow(element);
2907
+ const behavior = win.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ? 'auto' : 'smooth';
2908
+ element.scrollTo({
2909
+ top: scrollTop,
2910
+ behavior
2911
+ });
2912
+ });
2913
+ const resetTouchTrackingState = useStableCallback(() => {
2914
+ pendingKeyboardFocusMovedRef.current = false;
2915
+ keyboardTouchStartRef.current = null;
2916
+ });
2917
+ React.useEffect(() => {
2918
+ if (!mounted || !open) {
2919
+ focusedKeyboardTargetRef.current = null;
2920
+ restoreKeyboardScrollAdjustment();
2921
+ keyboardFocusFrame.cancel();
2922
+ return undefined;
2923
+ }
2924
+ if (!rootElement) {
2925
+ restoreKeyboardScrollAdjustment();
2926
+ return undefined;
2927
+ }
2928
+ const doc = ownerDocument(rootElement);
2929
+ const win = getWindow(rootElement);
2930
+ const visualViewport = win.visualViewport;
2931
+ const setDrawerKeyboardInset = inset => {
2932
+ rootElement.style.setProperty(DrawerViewportCssVars.keyboardInset, `${Math.max(0, Math.ceil(inset))}px`);
2933
+ };
2934
+ const resetDrawerKeyboardInset = () => {
2935
+ setDrawerKeyboardInset(0);
2936
+ };
2937
+ const clearFocusedKeyboardTarget = () => {
2938
+ focusedKeyboardTargetRef.current = null;
2939
+ resetDrawerKeyboardInset();
2940
+ restoreKeyboardScrollAdjustment();
2941
+ keyboardFocusFrame.cancel();
2942
+ };
2943
+ const alignFocusedKeyboardTarget = () => {
2944
+ const target = focusedKeyboardTargetRef.current;
2945
+ // If the focused field is removed from the DOM without firing `focusout` (e.g. it is
2946
+ // conditionally rendered away), any applied scroll slack is restored here on the next
2947
+ // focus/viewport event or when the drawer closes. This self-corrects rather than
2948
+ // tracking each field's lifecycle.
2949
+ if (nestedDrawerOpen || !target || !contains(rootElement, target)) {
2950
+ resetDrawerKeyboardInset();
2951
+ restoreKeyboardScrollAdjustment();
2952
+ return;
2953
+ }
2954
+ const keyboardViewport = getKeyboardVisualViewport(win);
2955
+ if (!keyboardViewport) {
2956
+ resetDrawerKeyboardInset();
2957
+ restoreKeyboardScrollAdjustment();
2958
+ return;
2959
+ }
2960
+ setDrawerKeyboardInset(getDrawerKeyboardInset(win, keyboardViewport));
2961
+ const scrollTarget = findKeyboardScrollTarget(target, rootElement);
2962
+ if (!scrollTarget) {
2963
+ restoreKeyboardScrollAdjustment();
2964
+ return;
2965
+ }
2966
+ if (!scrollTarget.isConnected || !contains(rootElement, scrollTarget)) {
2967
+ resetDrawerKeyboardInset();
2968
+ restoreKeyboardScrollAdjustment();
2969
+ return;
2970
+ }
2971
+ const scrollTargetRect = scrollTarget.getBoundingClientRect();
2972
+ const clippedBottom = Math.min(scrollTargetRect.bottom, keyboardViewport.bottom);
2973
+ const overlap = Math.max(0, scrollTargetRect.bottom - keyboardViewport.bottom);
2974
+ setKeyboardScrollSlack(scrollTarget, overlap > 0 ? overlap + KEYBOARD_SCROLL_SLACK : 0);
2975
+ const maxScrollTop = Math.max(0, scrollTarget.scrollHeight - scrollTarget.clientHeight);
2976
+ if (maxScrollTop <= 0) {
2977
+ return;
2978
+ }
2979
+ const clippedTop = Math.max(scrollTargetRect.top, keyboardViewport.top);
2980
+ const visibleTop = clippedTop + KEYBOARD_VISIBILITY_MARGIN;
2981
+ const visibleBottom = clippedBottom - KEYBOARD_VISIBILITY_MARGIN;
2982
+ if (visibleBottom <= visibleTop) {
2983
+ return;
2984
+ }
2985
+ const targetRect = target.getBoundingClientRect();
2986
+ const targetCenter = (targetRect.top + targetRect.bottom) / 2;
2987
+ const visibleCenter = (visibleTop + visibleBottom) / 2;
2988
+ const nextScrollTop = scrollTarget.scrollTop + targetCenter - visibleCenter;
2989
+ animateKeyboardScroll(scrollTarget, clamp(nextScrollTop, 0, maxScrollTop));
2990
+ };
2991
+ const scheduleKeyboardFocusAlignment = () => {
2992
+ keyboardFocusFrame.request(alignFocusedKeyboardTarget);
2993
+ };
2994
+ const captureFocusedKeyboardTarget = eventTarget => {
2995
+ if (nestedDrawerOpen) {
2996
+ return false;
2997
+ }
2998
+
2999
+ // Resolve through the same path as taps so contentEditable hosts (and labelled
3000
+ // controls) are normalized identically for the focus and touch paths.
3001
+ const target = resolveKeyboardInputTarget(eventTarget);
3002
+ if (!target || !contains(rootElement, target)) {
3003
+ return false;
3004
+ }
3005
+ focusedKeyboardTargetRef.current = target;
3006
+ return true;
3007
+ };
3008
+ const handleFocusIn = event => {
3009
+ if (captureFocusedKeyboardTarget(getTarget(event))) {
3010
+ scheduleKeyboardFocusAlignment();
3011
+ }
3012
+ };
3013
+ const handleFocusOut = event => {
3014
+ if (captureFocusedKeyboardTarget(event.relatedTarget)) {
3015
+ scheduleKeyboardFocusAlignment();
3016
+ return;
3017
+ }
3018
+ clearFocusedKeyboardTarget();
3019
+ };
3020
+ const handleViewportUpdate = () => {
3021
+ if (focusedKeyboardTargetRef.current || captureFocusedKeyboardTarget(activeElement(doc))) {
3022
+ scheduleKeyboardFocusAlignment();
3023
+ }
3024
+ };
3025
+ const cleanupListeners = [];
3026
+ if (visualViewport) {
3027
+ cleanupListeners.push(addEventListener(visualViewport, 'resize', handleViewportUpdate), addEventListener(visualViewport, 'scroll', handleViewportUpdate));
3028
+ }
3029
+ cleanupListeners.push(addEventListener(doc, 'focusin', handleFocusIn, true), addEventListener(doc, 'focusout', handleFocusOut, true));
3030
+ if (captureFocusedKeyboardTarget(activeElement(doc))) {
3031
+ scheduleKeyboardFocusAlignment();
3032
+ }
3033
+ return () => {
3034
+ cleanupListeners.forEach(cleanup => cleanup());
3035
+ clearFocusedKeyboardTarget();
3036
+ rootElement.style.removeProperty(DrawerViewportCssVars.keyboardInset);
3037
+ };
3038
+ }, [animateKeyboardScroll, keyboardFocusFrame, mounted, nestedDrawerOpen, open, restoreKeyboardScrollAdjustment, rootElement, setKeyboardScrollSlack]);
3039
+ const onTouchStart = useStableCallback(event => {
3040
+ if (!open || !mounted || nestedDrawerOpen) {
3041
+ resetTouchTrackingState();
3042
+ return;
3043
+ }
3044
+ const touch = event.touches[0];
3045
+ if (!touch) {
3046
+ return;
3047
+ }
3048
+ pendingKeyboardFocusMovedRef.current = false;
3049
+ keyboardTouchStartRef.current = {
3050
+ x: touch.clientX,
3051
+ y: touch.clientY
3052
+ };
3053
+ });
3054
+ const onTouchMove = useStableCallback(event => {
3055
+ const touch = event.touches[0];
3056
+ const touchStart = keyboardTouchStartRef.current;
3057
+ if (!touch || !touchStart || pendingKeyboardFocusMovedRef.current) {
3058
+ return;
3059
+ }
3060
+
3061
+ // Treat the gesture as a scroll/swipe (not a tap-to-focus) once the finger
3062
+ // moves past the threshold, so we don't open the keyboard on a drag.
3063
+ if (Math.abs(touch.clientX - touchStart.x) > INPUT_TAP_MOVE_THRESHOLD || Math.abs(touch.clientY - touchStart.y) > INPUT_TAP_MOVE_THRESHOLD) {
3064
+ pendingKeyboardFocusMovedRef.current = true;
3065
+ }
3066
+ });
3067
+ const onTouchEnd = useStableCallback(event => {
3068
+ if (!open || !mounted || nestedDrawerOpen || !rootElement || !keyboardTouchStartRef.current || pendingKeyboardFocusMovedRef.current) {
3069
+ resetTouchTrackingState();
3070
+ return;
3071
+ }
3072
+ const touch = event.changedTouches[0] ?? event.touches[0];
3073
+ const doc = ownerDocument(event.currentTarget);
3074
+ const nativeEventTarget = getTarget(event.nativeEvent);
3075
+ const pointTarget = touch ? resolveKeyboardTouchTargetFromPoint(doc, touch.clientX, touch.clientY) : null;
3076
+
3077
+ // The lift point landed on another interactive/label element; let its native tap
3078
+ // through instead of stealing it for the touchstart input.
3079
+ if (pointTarget === KEYBOARD_TAP_BLOCKED) {
3080
+ resetTouchTrackingState();
3081
+ return;
3082
+ }
3083
+ const keyboardTarget = touch && (pointTarget ?? resolveKeyboardTouchTarget(nativeEventTarget));
3084
+ if (keyboardTarget && (!contains(rootElement, keyboardTarget.focusTarget) || !contains(rootElement, keyboardTarget.clickTarget))) {
3085
+ resetTouchTrackingState();
3086
+ return;
3087
+ }
3088
+ if (keyboardTarget) {
3089
+ const {
3090
+ clickTarget: keyboardClickTarget,
3091
+ focusTarget: keyboardFocusTarget
3092
+ } = keyboardTarget;
3093
+ const win = getWindow(keyboardFocusTarget);
3094
+
3095
+ // While pinch-zoomed, keyboard alignment is suspended; let native behavior
3096
+ // handle focus and caret placement instead of blurring and re-focusing.
3097
+ if (win.visualViewport && win.visualViewport.scale !== 1) {
3098
+ resetTouchTrackingState();
3099
+ return;
3100
+ }
3101
+
3102
+ // Already focused with the keyboard up: let the native tap through so it can
3103
+ // reposition the caret, rather than blurring and re-focusing the same input.
3104
+ if (activeElement(ownerDocument(keyboardFocusTarget)) === keyboardFocusTarget && isKeyboardVisualViewportOpen(win)) {
3105
+ resetTouchTrackingState();
3106
+ return;
3107
+ }
3108
+
3109
+ // iOS only opens the software keyboard when focus happens synchronously
3110
+ // inside the touch gesture.
3111
+ event.preventDefault();
3112
+ focusKeyboardInputWithoutPageScroll(keyboardFocusTarget);
3113
+ // Preventing the touchend default also suppresses the compatibility mouse
3114
+ // events, including `click`; redispatch an untrusted replacement on the
3115
+ // original tap target so click handlers still run with the tap coordinates.
3116
+ dispatchKeyboardClick(keyboardClickTarget, touch);
3117
+ resetTouchTrackingState();
3118
+ return;
3119
+ }
3120
+ resetTouchTrackingState();
3121
+ });
3122
+ const contextValue = React.useMemo(() => ({
3123
+ onTouchStart,
3124
+ onTouchMove,
3125
+ onTouchEnd,
3126
+ onTouchCancel: resetTouchTrackingState
3127
+ }), [onTouchEnd, onTouchMove, onTouchStart, resetTouchTrackingState]);
3128
+ return /*#__PURE__*/jsx(DrawerVirtualKeyboardContext.Provider, {
3129
+ value: contextValue,
3130
+ children: children
3131
+ });
2615
3132
  }
2616
- function preserveNativeCrossAxisScrollOnMove(touchState, touch, isVerticalScrollAxis) {
2617
- if (touchState.preserveNativeCrossAxisScroll) {
3133
+ function isKeyboardInputElement(element) {
3134
+ if (element.isContentEditable) {
2618
3135
  return true;
2619
3136
  }
2620
- if (touchState.allowSwipe === true || !touchState.hasCrossAxisScrollableContent) {
2621
- return false;
2622
- }
2623
- const drawerAxisGestureDelta = isVerticalScrollAxis ? touch.clientY - touchState.startY : touch.clientX - touchState.startX;
2624
- const crossAxisGestureDelta = isVerticalScrollAxis ? touch.clientX - touchState.startX : touch.clientY - touchState.startY;
2625
- const absDrawerAxisGestureDelta = Math.abs(drawerAxisGestureDelta);
2626
- const absCrossAxisGestureDelta = Math.abs(crossAxisGestureDelta);
2627
- if (absCrossAxisGestureDelta < 6 || absCrossAxisGestureDelta <= absDrawerAxisGestureDelta + 2) {
2628
- return false;
3137
+ const win = getWindow(element);
3138
+ if (element instanceof win.HTMLTextAreaElement || element instanceof win.HTMLInputElement && KEYBOARD_INPUT_TYPES.has(element.type)) {
3139
+ // Disabled controls can't focus or open the keyboard, so tap-to-focus must skip them —
3140
+ // otherwise the dispatched click fires handlers a native tap on a disabled control never would.
3141
+ return !element.matches(':disabled');
2629
3142
  }
2630
- touchState.preserveNativeCrossAxisScroll = true;
2631
- return true;
3143
+ return false;
2632
3144
  }
2633
- function hasScrollableContentOnAxis(scrollTarget, axis) {
2634
- return axis === 'vertical' ? scrollTarget.scrollHeight > scrollTarget.clientHeight : scrollTarget.scrollWidth > scrollTarget.clientWidth;
3145
+ function resolveKeyboardInputTarget(target) {
3146
+ if (!isHTMLElement(target)) {
3147
+ return null;
3148
+ }
3149
+ if (isKeyboardInputElement(target)) {
3150
+ return target.isContentEditable ? getContentEditableHost(target) : target;
3151
+ }
3152
+ const label = target.closest('label');
3153
+ const control = label?.control ?? null;
3154
+ return isHTMLElement(control) && isKeyboardInputElement(control) ? control : null;
2635
3155
  }
2636
- function getScrollMetrics(scrollTarget, axis) {
2637
- if (axis === 'vertical') {
2638
- const max = Math.max(0, scrollTarget.scrollHeight - scrollTarget.clientHeight);
2639
- return {
2640
- offset: scrollTarget.scrollTop,
2641
- max
2642
- };
3156
+ function resolveKeyboardTouchTarget(target) {
3157
+ const focusTarget = resolveKeyboardInputTarget(target);
3158
+ if (!focusTarget) {
3159
+ return null;
2643
3160
  }
2644
- const max = Math.max(0, scrollTarget.scrollWidth - scrollTarget.clientWidth);
2645
3161
  return {
2646
- offset: scrollTarget.scrollLeft,
2647
- max
3162
+ focusTarget,
3163
+ clickTarget: isHTMLElement(target) ? target : focusTarget
2648
3164
  };
2649
3165
  }
2650
- function isAtSwipeStartEdge(scrollTarget, axis, direction) {
2651
- const {
2652
- offset,
2653
- max
2654
- } = getScrollMetrics(scrollTarget, axis);
2655
- const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
2656
- if (dismissFromStartEdge === null) {
2657
- return false;
3166
+
3167
+ // Inherited-editable descendants (no `contenteditable` attribute of their own) are not
3168
+ // focusable, so focusing them is a no-op; resolve taps on them to the editing host.
3169
+ function getContentEditableHost(element) {
3170
+ let host = element;
3171
+ while (host.parentElement?.isContentEditable) {
3172
+ host = host.parentElement;
2658
3173
  }
2659
- return dismissFromStartEdge ? offset <= 0 : offset >= max;
3174
+ return host;
2660
3175
  }
2661
- function canSwipeFromScrollEdgeOnMove(scrollTarget, axis, direction, delta) {
2662
- const {
2663
- offset,
2664
- max
2665
- } = getScrollMetrics(scrollTarget, axis);
2666
- const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
2667
- if (dismissFromStartEdge === null) {
2668
- return false;
3176
+ function resolveKeyboardTouchTargetFromPoint(doc, clientX, clientY) {
3177
+ const exactTarget = getElementAtPoint(doc, clientX, clientY);
3178
+ const exactKeyboardTarget = resolveKeyboardInputTarget(exactTarget);
3179
+ if (exactKeyboardTarget) {
3180
+ return {
3181
+ focusTarget: exactKeyboardTarget,
3182
+ clickTarget: isHTMLElement(exactTarget) ? exactTarget : exactKeyboardTarget
3183
+ };
2669
3184
  }
2670
- const movingTowardDismiss = dismissFromStartEdge ? delta > 0 : delta < 0;
2671
- if (!movingTowardDismiss) {
2672
- return false;
3185
+
3186
+ // Probing nearby points compensates for iOS retargeting taps while the page reacts
3187
+ // to the keyboard, but it must not steal a tap that lands on another interactive
3188
+ // element — that would suppress its click and focus a neighboring field instead.
3189
+ // `closest('label')` covers labels of non-keyboard controls (e.g. checkboxes).
3190
+ // Returning the blocked sentinel (rather than `null`) stops the caller from falling
3191
+ // back to the touchstart target, which would re-steal the very tap rejected here.
3192
+ if (isInteractiveElement(exactTarget) || exactTarget?.closest('label') != null) {
3193
+ return KEYBOARD_TAP_BLOCKED;
2673
3194
  }
2674
- return dismissFromStartEdge ? offset <= 0 : offset >= max;
2675
- }
2676
- function shouldDismissFromStartEdge(direction, axis) {
2677
- if (axis === 'vertical') {
2678
- if (direction === 'down') {
2679
- return true;
3195
+ for (const [offsetX, offsetY] of [[0, INPUT_TAP_HIT_SLOP], [0, -INPUT_TAP_HIT_SLOP], [INPUT_TAP_HIT_SLOP, 0], [-INPUT_TAP_HIT_SLOP, 0]]) {
3196
+ const keyboardTarget = resolveKeyboardInputTarget(getElementAtPoint(doc, clientX + offsetX, clientY + offsetY));
3197
+ if (keyboardTarget) {
3198
+ return {
3199
+ focusTarget: keyboardTarget,
3200
+ clickTarget: keyboardTarget
3201
+ };
2680
3202
  }
2681
- if (direction === 'up') {
2682
- return false;
3203
+ }
3204
+ return null;
3205
+ }
3206
+ function dispatchKeyboardClick(target, touch) {
3207
+ const win = getWindow(target);
3208
+ const ClickEvent = win.PointerEvent ?? win.MouseEvent;
3209
+ target.dispatchEvent(new ClickEvent('click', {
3210
+ bubbles: true,
3211
+ cancelable: true,
3212
+ clientX: touch.clientX,
3213
+ clientY: touch.clientY,
3214
+ detail: 1,
3215
+ view: win
3216
+ }));
3217
+ }
3218
+ function focusKeyboardInputWithoutPageScroll(target) {
3219
+ const wasFocused = activeElement(ownerDocument(target)) === target;
3220
+ const previousOpacity = target.style.opacity;
3221
+ const previousTransform = target.style.transform;
3222
+ const previousTransition = target.style.transition;
3223
+
3224
+ // iOS Safari can still scroll the page for transformed sheets even with preventScroll.
3225
+ // Move the input off-screen only for the synchronous focus call.
3226
+ target.style.transition = 'none';
3227
+ target.style.opacity = '0';
3228
+ target.style.transform = 'translateY(-2000px)';
3229
+ try {
3230
+ if (wasFocused) {
3231
+ target.blur();
2683
3232
  }
2684
- return null;
3233
+ target.focus({
3234
+ preventScroll: true
3235
+ });
3236
+ } finally {
3237
+ target.style.opacity = previousOpacity;
3238
+ target.style.transform = previousTransform;
3239
+ target.style.transition = previousTransition;
2685
3240
  }
2686
- if (direction === 'right') {
2687
- return true;
3241
+ }
3242
+ function findKeyboardScrollTarget(target, root) {
3243
+ // Start at the parent: scrolling the focused field's own content (an overflowing
3244
+ // textarea is scrollable itself) can never move its box out from under the keyboard.
3245
+ // `getParentNode` crosses shadow boundaries so an input inside a shadow root still reaches
3246
+ // the drawer body scroller. Prefer an already-scrollable ancestor, then fall back to one
3247
+ // that only becomes scrollable once keyboard slack is added (overflow intent without
3248
+ // current overflow).
3249
+ const scrollStart = getParentNode(target);
3250
+ return findScrollableTouchTarget(scrollStart, root, 'vertical') ?? findScrollableTouchTarget(scrollStart, root, 'vertical', true);
3251
+ }
3252
+ function getKeyboardVisualViewport(win) {
3253
+ const visualViewport = win.visualViewport;
3254
+ if (!visualViewport || visualViewport.scale !== 1) {
3255
+ return null;
2688
3256
  }
2689
- if (direction === 'left') {
2690
- return false;
3257
+ const reducedHeight = win.innerHeight - visualViewport.height;
3258
+ // Treat small viewport changes as browser chrome movement, not the software keyboard.
3259
+ if (reducedHeight <= KEYBOARD_RESIZE_THRESHOLD) {
3260
+ return null;
2691
3261
  }
2692
- return null;
3262
+ const top = Math.max(0, visualViewport.offsetTop);
3263
+ return {
3264
+ top,
3265
+ bottom: Math.min(win.innerHeight, top + visualViewport.height)
3266
+ };
3267
+ }
3268
+ function getDrawerKeyboardInset(win, keyboardViewport) {
3269
+ return Math.max(0, win.innerHeight - keyboardViewport.bottom);
3270
+ }
3271
+ function isKeyboardVisualViewportOpen(win) {
3272
+ return !win.visualViewport || getKeyboardVisualViewport(win) != null;
2693
3273
  }
2694
3274
 
2695
3275
  var index_parts$5 = /*#__PURE__*/Object.freeze({
@@ -2709,6 +3289,7 @@ var index_parts$5 = /*#__PURE__*/Object.freeze({
2709
3289
  Title: DrawerTitle,
2710
3290
  Trigger: DrawerTrigger,
2711
3291
  Viewport: DrawerViewport,
3292
+ VirtualKeyboardProvider: DrawerVirtualKeyboardProvider,
2712
3293
  createHandle: createDialogHandle
2713
3294
  });
2714
3295
 
@@ -2720,8 +3301,9 @@ var index_parts$5 = /*#__PURE__*/Object.freeze({
2720
3301
  */
2721
3302
  const MenuArrow = /*#__PURE__*/React.forwardRef(function MenuArrow(componentProps, forwardedRef) {
2722
3303
  const {
2723
- className,
2724
3304
  render,
3305
+ className,
3306
+ style,
2725
3307
  ...elementProps
2726
3308
  } = componentProps;
2727
3309
  const {
@@ -2782,6 +3364,7 @@ const MenuCheckboxItem = /*#__PURE__*/React.forwardRef(function MenuCheckboxItem
2782
3364
  checked: checkedProp,
2783
3365
  defaultChecked,
2784
3366
  onCheckedChange,
3367
+ style,
2785
3368
  ...elementProps
2786
3369
  } = componentProps;
2787
3370
  const listItem = useCompositeListItem({
@@ -2810,7 +3393,7 @@ const MenuCheckboxItem = /*#__PURE__*/React.forwardRef(function MenuCheckboxItem
2810
3393
  id,
2811
3394
  store,
2812
3395
  nativeButton,
2813
- nodeId: menuPositionerContext?.nodeId,
3396
+ nodeId: menuPositionerContext?.context.nodeId,
2814
3397
  itemMetadata: REGULAR_ITEM
2815
3398
  });
2816
3399
  const state = React.useMemo(() => ({
@@ -2818,17 +3401,16 @@ const MenuCheckboxItem = /*#__PURE__*/React.forwardRef(function MenuCheckboxItem
2818
3401
  highlighted,
2819
3402
  checked
2820
3403
  }), [disabled, highlighted, checked]);
2821
- const handleClick = useStableCallback(event => {
2822
- const details = {
2823
- ...createChangeEventDetails(itemPress, event.nativeEvent),
2824
- preventUnmountOnClose: () => {}
2825
- };
3404
+ function handleClick(event) {
3405
+ const details = createChangeEventDetails(itemPress, event.nativeEvent, undefined, {
3406
+ preventUnmountOnClose() {}
3407
+ });
2826
3408
  onCheckedChange?.(!checked, details);
2827
3409
  if (details.isCanceled) {
2828
3410
  return;
2829
3411
  }
2830
3412
  setChecked(currentlyChecked => !currentlyChecked);
2831
- });
3413
+ }
2832
3414
  const element = useRenderElement('div', componentProps, {
2833
3415
  state,
2834
3416
  stateAttributesMapping: itemMapping,
@@ -2856,6 +3438,7 @@ const MenuCheckboxItemIndicator = /*#__PURE__*/React.forwardRef(function MenuChe
2856
3438
  const {
2857
3439
  render,
2858
3440
  className,
3441
+ style,
2859
3442
  keepMounted = false,
2860
3443
  ...elementProps
2861
3444
  } = componentProps;
@@ -2894,25 +3477,6 @@ const MenuCheckboxItemIndicator = /*#__PURE__*/React.forwardRef(function MenuChe
2894
3477
  });
2895
3478
  if (process.env.NODE_ENV !== "production") MenuCheckboxItemIndicator.displayName = "MenuCheckboxItemIndicator";
2896
3479
 
2897
- /**
2898
- * Returns a previous value of its argument.
2899
- * @param value Current value.
2900
- * @returns Previous value, or null if there is no previous value.
2901
- */
2902
- function usePreviousValue(value) {
2903
- const [state, setState] = React.useState({
2904
- current: value,
2905
- previous: null
2906
- });
2907
- if (value !== state.current) {
2908
- setState({
2909
- current: value,
2910
- previous: state.current
2911
- });
2912
- }
2913
- return state.previous;
2914
- }
2915
-
2916
3480
  function getCssDimensions(element) {
2917
3481
  const css = getComputedStyle(element);
2918
3482
  // In testing environments, the `width` and `height` properties are empty
@@ -2933,8 +3497,6 @@ function getCssDimensions(element) {
2933
3497
  };
2934
3498
  }
2935
3499
 
2936
- const DEFAULT_ENABLED = () => true;
2937
-
2938
3500
  /**
2939
3501
  * Allows the element to automatically resize based on its content while supporting animations.
2940
3502
  */
@@ -2944,7 +3506,6 @@ function usePopupAutoResize(parameters) {
2944
3506
  positionerElement,
2945
3507
  content,
2946
3508
  mounted,
2947
- enabled = DEFAULT_ENABLED,
2948
3509
  onMeasureLayout: onMeasureLayoutParam,
2949
3510
  onMeasureLayoutComplete: onMeasureLayoutCompleteParam,
2950
3511
  side,
@@ -2953,7 +3514,6 @@ function usePopupAutoResize(parameters) {
2953
3514
  const runOnceAnimationsFinish = useAnimationsFinished(popupElement, true, false);
2954
3515
  const animationFrame = useAnimationFrame();
2955
3516
  const committedDimensionsRef = React.useRef(null);
2956
- const liveDimensionsRef = React.useRef(null);
2957
3517
  const isInitialRenderRef = React.useRef(true);
2958
3518
  const restoreAnchoringStylesRef = React.useRef(NOOP);
2959
3519
  const onMeasureLayout = useStableCallback(onMeasureLayoutParam);
@@ -2977,27 +3537,16 @@ function usePopupAutoResize(parameters) {
2977
3537
  }, [side, direction]);
2978
3538
  useIsoLayoutEffect(() => {
2979
3539
  // Reset the state when the popup is closed.
2980
- if (!mounted || !enabled() || typeof ResizeObserver !== 'function') {
3540
+ if (!mounted) {
2981
3541
  restoreAnchoringStylesRef.current = NOOP;
2982
3542
  isInitialRenderRef.current = true;
2983
3543
  committedDimensionsRef.current = null;
2984
- liveDimensionsRef.current = null;
2985
3544
  return undefined;
2986
3545
  }
2987
3546
  if (!popupElement || !positionerElement) {
2988
3547
  return undefined;
2989
3548
  }
2990
3549
  restoreAnchoringStylesRef.current = applyElementStyles(popupElement, anchoringStyles);
2991
- const observer = new ResizeObserver(entries => {
2992
- const entry = entries[0];
2993
- if (entry) {
2994
- liveDimensionsRef.current = {
2995
- width: Math.ceil(entry.borderBoxSize[0].inlineSize),
2996
- height: Math.ceil(entry.borderBoxSize[0].blockSize)
2997
- };
2998
- }
2999
- });
3000
- observer.observe(popupElement);
3001
3550
 
3002
3551
  // Measure the rendered size to enable transitions:
3003
3552
  setPopupCssSize(popupElement, 'auto');
@@ -3029,32 +3578,18 @@ function usePopupAutoResize(parameters) {
3029
3578
  onMeasureLayoutComplete?.(null, dimensions);
3030
3579
  isInitialRenderRef.current = false;
3031
3580
  return () => {
3032
- observer.disconnect();
3033
3581
  restoreAnchoringStylesRef.current();
3034
3582
  restoreAnchoringStylesRef.current = NOOP;
3035
3583
  };
3036
3584
  }
3037
3585
 
3038
3586
  // Subsequent renders while open (when `content` changes).
3039
- setPopupCssSize(popupElement, 'auto');
3040
3587
  setPositionerCssSize(positionerElement, 'max-content');
3041
- const previousDimensions = committedDimensionsRef.current ?? liveDimensionsRef.current;
3588
+ const previousDimensions = committedDimensionsRef.current;
3042
3589
  const newDimensions = getCssDimensions(popupElement);
3043
3590
 
3044
- // Commit immediately so future content changes have a stable previous size, even if
3045
- // ResizeObserver runs after this point.
3591
+ // Commit immediately so future content changes have a stable previous size.
3046
3592
  committedDimensionsRef.current = newDimensions;
3047
- if (!previousDimensions) {
3048
- setPositionerCssSize(positionerElement, newDimensions);
3049
- restoreMeasurementOverridesIncludingScale();
3050
- onMeasureLayoutComplete?.(null, newDimensions);
3051
- return () => {
3052
- observer.disconnect();
3053
- animationFrame.cancel();
3054
- restoreAnchoringStylesRef.current();
3055
- restoreAnchoringStylesRef.current = NOOP;
3056
- };
3057
- }
3058
3593
  setPopupCssSize(popupElement, previousDimensions);
3059
3594
  restoreMeasurementOverridesIncludingScale();
3060
3595
  onMeasureLayoutComplete?.(previousDimensions, newDimensions);
@@ -3068,13 +3603,12 @@ function usePopupAutoResize(parameters) {
3068
3603
  }, abortController.signal);
3069
3604
  });
3070
3605
  return () => {
3071
- observer.disconnect();
3072
3606
  abortController.abort();
3073
3607
  animationFrame.cancel();
3074
3608
  restoreAnchoringStylesRef.current();
3075
3609
  restoreAnchoringStylesRef.current = NOOP;
3076
3610
  };
3077
- }, [content, popupElement, positionerElement, runOnceAnimationsFinish, animationFrame, enabled, mounted, onMeasureLayout, onMeasureLayoutComplete, anchoringStyles]);
3611
+ }, [content, popupElement, positionerElement, runOnceAnimationsFinish, animationFrame, mounted, onMeasureLayout, onMeasureLayoutComplete, anchoringStyles]);
3078
3612
  }
3079
3613
  function overrideElementStyle(element, property, value) {
3080
3614
  const originalValue = element.style.getPropertyValue(property);
@@ -3157,6 +3691,11 @@ function usePopupViewport(parameters) {
3157
3691
  }
3158
3692
  });
3159
3693
  const lastHandledTriggerRef = React.useRef(null);
3694
+ useIsoLayoutEffect(() => {
3695
+ if (!open || !mounted) {
3696
+ lastHandledTriggerRef.current = null;
3697
+ }
3698
+ }, [open, mounted]);
3160
3699
  useIsoLayoutEffect(() => {
3161
3700
  // When a trigger changes, set the captured children HTML to state,
3162
3701
  // so we can render both new and old content.
@@ -3195,7 +3734,7 @@ function usePopupViewport(parameters) {
3195
3734
  if (!source) {
3196
3735
  return;
3197
3736
  }
3198
- const wrapper = document.createElement('div');
3737
+ const wrapper = ownerDocument(source).createElement('div');
3199
3738
  for (const child of Array.from(source.childNodes)) {
3200
3739
  wrapper.appendChild(child.cloneNode(true));
3201
3740
  }
@@ -3367,8 +3906,8 @@ const stateAttributesMapping$2 = {
3367
3906
 
3368
3907
  /**
3369
3908
  * A viewport for displaying content transitions.
3370
- * This component is only required if one popup can be opened by multiple triggers, its content change based on the trigger
3371
- * and switching between them is animated.
3909
+ * This component is only required if one popup can be opened by multiple triggers, its content
3910
+ * changes based on the trigger, and switching between them is animated.
3372
3911
  * Renders a `<div>` element.
3373
3912
  *
3374
3913
  * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu)
@@ -3377,6 +3916,7 @@ const MenuViewport = /*#__PURE__*/React.forwardRef(function MenuViewport(compone
3377
3916
  const {
3378
3917
  render,
3379
3918
  className,
3919
+ style,
3380
3920
  children,
3381
3921
  ...elementProps
3382
3922
  } = componentProps;
@@ -3447,7 +3987,7 @@ class MenuHandle {
3447
3987
  * Indicates whether the menu is currently open.
3448
3988
  */
3449
3989
  get isOpen() {
3450
- return this.store.state.open;
3990
+ return this.store.select('open');
3451
3991
  }
3452
3992
  }
3453
3993
 
@@ -3520,6 +4060,7 @@ const ScrollAreaScrollbar = /*#__PURE__*/React.forwardRef(function ScrollAreaScr
3520
4060
  className,
3521
4061
  orientation = 'vertical',
3522
4062
  keepMounted = false,
4063
+ style,
3523
4064
  ...elementProps
3524
4065
  } = componentProps;
3525
4066
  const {
@@ -3535,6 +4076,7 @@ const ScrollAreaScrollbar = /*#__PURE__*/React.forwardRef(function ScrollAreaScr
3535
4076
  thumbXRef,
3536
4077
  handlePointerDown,
3537
4078
  handlePointerUp,
4079
+ handleScroll,
3538
4080
  rootId,
3539
4081
  thumbSize,
3540
4082
  hasMeasuredScrollbar
@@ -3556,7 +4098,12 @@ const ScrollAreaScrollbar = /*#__PURE__*/React.forwardRef(function ScrollAreaScr
3556
4098
  };
3557
4099
  const direction = useDirection();
3558
4100
  const hideTrackUntilMeasured = !hasMeasuredScrollbar && !keepMounted;
4101
+ const isHidden = orientation === 'vertical' ? hiddenState.y : hiddenState.x;
4102
+ const shouldRender = keepMounted || !isHidden;
3559
4103
  React.useEffect(() => {
4104
+ if (!shouldRender) {
4105
+ return undefined;
4106
+ }
3560
4107
  const viewportEl = viewportRef.current;
3561
4108
  const scrollbarEl = orientation === 'vertical' ? scrollbarYRef.current : scrollbarXRef.current;
3562
4109
  if (!scrollbarEl) {
@@ -3566,34 +4113,34 @@ const ScrollAreaScrollbar = /*#__PURE__*/React.forwardRef(function ScrollAreaScr
3566
4113
  if (!viewportEl || !scrollbarEl || event.ctrlKey) {
3567
4114
  return;
3568
4115
  }
3569
- event.preventDefault();
3570
- if (orientation === 'vertical') {
3571
- if (viewportEl.scrollTop === 0 && event.deltaY < 0) {
3572
- return;
3573
- }
3574
- } else if (viewportEl.scrollLeft === 0 && event.deltaX < 0) {
4116
+ const horizontal = orientation === 'horizontal';
4117
+ const scrollProperty = horizontal ? 'scrollLeft' : 'scrollTop';
4118
+ const delta = horizontal ? event.deltaX : event.deltaY;
4119
+ if (delta === 0) {
3575
4120
  return;
3576
4121
  }
3577
- if (orientation === 'vertical') {
3578
- if (viewportEl.scrollTop === viewportEl.scrollHeight - viewportEl.clientHeight && event.deltaY > 0) {
3579
- return;
3580
- }
3581
- } else if (viewportEl.scrollLeft === viewportEl.scrollWidth - viewportEl.clientWidth && event.deltaX > 0) {
4122
+ const maxScroll = horizontal ? viewportEl.scrollWidth - viewportEl.clientWidth : viewportEl.scrollHeight - viewportEl.clientHeight;
4123
+ // RTL horizontal scrolling uses a negative `scrollLeft` range, from 0 to `-maxScroll`.
4124
+ const minScroll = horizontal && direction === 'rtl' ? -maxScroll : 0;
4125
+ const maxScrollValue = horizontal && direction === 'rtl' ? 0 : maxScroll;
4126
+ const scrollValue = viewportEl[scrollProperty];
4127
+
4128
+ // At an edge (or with no overflow), let the wheel event chain to the
4129
+ // parent/page instead of swallowing it via `preventDefault`.
4130
+ if (scrollValue <= minScroll && delta < 0 || scrollValue >= maxScrollValue && delta > 0) {
3582
4131
  return;
3583
4132
  }
3584
- if (orientation === 'vertical') {
3585
- viewportEl.scrollTop += event.deltaY;
3586
- } else {
3587
- viewportEl.scrollLeft += event.deltaX;
3588
- }
4133
+ event.preventDefault();
4134
+ viewportEl[scrollProperty] = Math.min(maxScrollValue, Math.max(minScroll, scrollValue + delta));
4135
+ handleScroll({
4136
+ x: viewportEl.scrollLeft,
4137
+ y: viewportEl.scrollTop
4138
+ });
3589
4139
  }
3590
- scrollbarEl.addEventListener('wheel', handleWheel, {
4140
+ return addEventListener(scrollbarEl, 'wheel', handleWheel, {
3591
4141
  passive: false
3592
4142
  });
3593
- return () => {
3594
- scrollbarEl.removeEventListener('wheel', handleWheel);
3595
- };
3596
- }, [orientation, scrollbarXRef, scrollbarYRef, viewportRef]);
4143
+ }, [direction, handleScroll, orientation, scrollbarXRef, scrollbarYRef, shouldRender, viewportRef]);
3597
4144
  const props = {
3598
4145
  ...(rootId && {
3599
4146
  'data-id': `${rootId}-scrollbar`
@@ -3602,9 +4149,12 @@ const ScrollAreaScrollbar = /*#__PURE__*/React.forwardRef(function ScrollAreaScr
3602
4149
  if (event.button !== 0) {
3603
4150
  return;
3604
4151
  }
4152
+ const target = getTarget(event.nativeEvent);
4153
+ const thumb = orientation === 'vertical' ? thumbYRef.current : thumbXRef.current;
3605
4154
 
3606
- // Ignore clicks on thumb
3607
- if (event.currentTarget !== event.target) {
4155
+ // Ignore clicks on thumb, including cases where React retargets the
4156
+ // synthetic event to the track host across a shadow boundary.
4157
+ if (thumb && contains(thumb, target)) {
3608
4158
  return;
3609
4159
  }
3610
4160
  if (!viewportRef.current) {
@@ -3649,9 +4199,16 @@ const ScrollAreaScrollbar = /*#__PURE__*/React.forwardRef(function ScrollAreaScr
3649
4199
  }
3650
4200
  viewportRef.current.scrollLeft = newScrollLeft;
3651
4201
  }
4202
+ handleScroll({
4203
+ x: viewportRef.current.scrollLeft,
4204
+ y: viewportRef.current.scrollTop
4205
+ });
3652
4206
  handlePointerDown(event);
3653
4207
  },
3654
4208
  onPointerUp: handlePointerUp,
4209
+ // Mirror `onPointerUp` so a browser-cancelled gesture on the track (no thumb
4210
+ // child captures the pointer) still clears the drag state.
4211
+ onPointerCancel: handlePointerUp,
3655
4212
  style: {
3656
4213
  position: 'absolute',
3657
4214
  touchAction: 'none',
@@ -3681,8 +4238,6 @@ const ScrollAreaScrollbar = /*#__PURE__*/React.forwardRef(function ScrollAreaScr
3681
4238
  const contextValue = React.useMemo(() => ({
3682
4239
  orientation
3683
4240
  }), [orientation]);
3684
- const isHidden = orientation === 'vertical' ? hiddenState.y : hiddenState.x;
3685
- const shouldRender = keepMounted || !isHidden;
3686
4241
  if (!shouldRender) {
3687
4242
  return null;
3688
4243
  }
@@ -3703,34 +4258,42 @@ const ScrollAreaContent = /*#__PURE__*/React.forwardRef(function ScrollAreaConte
3703
4258
  const {
3704
4259
  render,
3705
4260
  className,
4261
+ style,
3706
4262
  ...elementProps
3707
4263
  } = componentProps;
3708
- const contentWrapperRef = React.useRef(null);
3709
4264
  const {
3710
4265
  computeThumbPosition
3711
4266
  } = useScrollAreaViewportContext();
3712
4267
  const {
4268
+ hasMeasuredScrollbar,
3713
4269
  viewportState
3714
4270
  } = useScrollAreaRootContext();
4271
+ const contentWrapperRef = React.useRef(null);
4272
+ const computeOnInitialResizeRef = React.useRef(hasMeasuredScrollbar);
3715
4273
  useIsoLayoutEffect(() => {
3716
4274
  if (typeof ResizeObserver === 'undefined') {
3717
4275
  return undefined;
3718
4276
  }
3719
4277
  let hasInitialized = false;
3720
- const ro = new ResizeObserver(() => {
3721
- // ResizeObserver fires once upon observing, so we skip the initial call
3722
- // to avoid double-calculating the thumb position on mount.
4278
+ const resizeObserver = new ResizeObserver(() => {
3723
4279
  if (!hasInitialized) {
3724
4280
  hasInitialized = true;
3725
- return;
4281
+
4282
+ // ResizeObserver fires once upon observing. Skip that initial call to avoid
4283
+ // double-calculating the thumb position on mount, unless the content mounted
4284
+ // after the viewport's initial measurement (in which case this fire is what
4285
+ // brings the overflow state in sync).
4286
+ if (!computeOnInitialResizeRef.current) {
4287
+ return;
4288
+ }
3726
4289
  }
3727
4290
  computeThumbPosition();
3728
4291
  });
3729
4292
  if (contentWrapperRef.current) {
3730
- ro.observe(contentWrapperRef.current);
4293
+ resizeObserver.observe(contentWrapperRef.current);
3731
4294
  }
3732
4295
  return () => {
3733
- ro.disconnect();
4296
+ resizeObserver.disconnect();
3734
4297
  };
3735
4298
  }, [computeThumbPosition]);
3736
4299
  const element = useRenderElement('div', componentProps, {
@@ -3758,6 +4321,7 @@ const ScrollAreaThumb = /*#__PURE__*/React.forwardRef(function ScrollAreaThumb(c
3758
4321
  const {
3759
4322
  render,
3760
4323
  className,
4324
+ style,
3761
4325
  ...elementProps
3762
4326
  } = componentProps;
3763
4327
  const {
@@ -3768,29 +4332,34 @@ const ScrollAreaThumb = /*#__PURE__*/React.forwardRef(function ScrollAreaThumb(c
3768
4332
  handlePointerUp,
3769
4333
  setScrollingX,
3770
4334
  setScrollingY,
4335
+ scrollingX,
4336
+ scrollingY,
3771
4337
  hasMeasuredScrollbar
3772
4338
  } = useScrollAreaRootContext();
3773
4339
  const {
3774
4340
  orientation
3775
4341
  } = useScrollAreaScrollbarContext();
3776
4342
  const state = {
4343
+ scrolling: orientation === 'horizontal' ? scrollingX : scrollingY,
3777
4344
  orientation
3778
4345
  };
4346
+ function endDrag(event) {
4347
+ if (orientation === 'vertical') {
4348
+ setScrollingY(false);
4349
+ }
4350
+ if (orientation === 'horizontal') {
4351
+ setScrollingX(false);
4352
+ }
4353
+ handlePointerUp(event);
4354
+ }
3779
4355
  const element = useRenderElement('div', componentProps, {
3780
4356
  ref: [forwardedRef, orientation === 'vertical' ? thumbYRef : thumbXRef],
3781
4357
  state,
3782
4358
  props: [{
3783
4359
  onPointerDown: handlePointerDown,
3784
4360
  onPointerMove: handlePointerMove,
3785
- onPointerUp(event) {
3786
- if (orientation === 'vertical') {
3787
- setScrollingY(false);
3788
- }
3789
- if (orientation === 'horizontal') {
3790
- setScrollingX(false);
3791
- }
3792
- handlePointerUp(event);
3793
- },
4361
+ onPointerUp: endDrag,
4362
+ onPointerCancel: endDrag,
3794
4363
  style: {
3795
4364
  visibility: hasMeasuredScrollbar ? undefined : 'hidden',
3796
4365
  ...(orientation === 'vertical' && {
@@ -3816,6 +4385,7 @@ const ScrollAreaCorner = /*#__PURE__*/React.forwardRef(function ScrollAreaCorner
3816
4385
  const {
3817
4386
  render,
3818
4387
  className,
4388
+ style,
3819
4389
  ...elementProps
3820
4390
  } = componentProps;
3821
4391
  const {
@@ -3894,32 +4464,32 @@ function useNumberFieldScrubAreaContext() {
3894
4464
  return context;
3895
4465
  }
3896
4466
 
3897
- // Calculates the viewport rect for the virtual cursor.
4467
+ // Calculates the bounds the virtual cursor wraps within, as absolute edge coordinates.
3898
4468
  function getViewportRect(teleportDistance, scrubAreaEl) {
3899
4469
  const win = getWindow(scrubAreaEl);
3900
- const rect = scrubAreaEl.getBoundingClientRect();
3901
- if (rect && teleportDistance != null) {
4470
+ if (teleportDistance != null) {
4471
+ const rect = scrubAreaEl.getBoundingClientRect();
3902
4472
  return {
3903
- x: rect.left - teleportDistance / 2,
3904
- y: rect.top - teleportDistance / 2,
3905
- width: rect.right + teleportDistance / 2,
3906
- height: rect.bottom + teleportDistance / 2
4473
+ left: rect.left - teleportDistance / 2,
4474
+ top: rect.top - teleportDistance / 2,
4475
+ right: rect.right + teleportDistance / 2,
4476
+ bottom: rect.bottom + teleportDistance / 2
3907
4477
  };
3908
4478
  }
3909
4479
  const vV = win.visualViewport;
3910
4480
  if (vV) {
3911
4481
  return {
3912
- x: vV.offsetLeft,
3913
- y: vV.offsetTop,
3914
- width: vV.offsetLeft + vV.width,
3915
- height: vV.offsetTop + vV.height
4482
+ left: vV.offsetLeft,
4483
+ top: vV.offsetTop,
4484
+ right: vV.offsetLeft + vV.width,
4485
+ bottom: vV.offsetTop + vV.height
3916
4486
  };
3917
4487
  }
3918
4488
  return {
3919
- x: 0,
3920
- y: 0,
3921
- width: win.document.documentElement.clientWidth,
3922
- height: win.document.documentElement.clientHeight
4489
+ left: 0,
4490
+ top: 0,
4491
+ right: win.document.documentElement.clientWidth,
4492
+ bottom: win.document.documentElement.clientHeight
3923
4493
  };
3924
4494
  }
3925
4495
 
@@ -3936,12 +4506,21 @@ function subscribeToVisualViewportResize(element, visualScaleRef) {
3936
4506
  }
3937
4507
  }
3938
4508
  handleVisualResize();
3939
- vV.addEventListener('resize', handleVisualResize);
3940
- return () => {
3941
- vV.removeEventListener('resize', handleVisualResize);
3942
- };
4509
+ return addEventListener(vV, 'resize', handleVisualResize);
3943
4510
  }
3944
4511
 
4512
+ const SCRUB_AREA_STYLE = {
4513
+ touchAction: 'none',
4514
+ WebkitUserSelect: 'none',
4515
+ userSelect: 'none'
4516
+ };
4517
+
4518
+ /**
4519
+ * An interactive area where the user can click and drag to change the field value.
4520
+ * Renders a `<span>` element.
4521
+ *
4522
+ * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
4523
+ */
3945
4524
  const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldScrubArea(componentProps, forwardedRef) {
3946
4525
  const {
3947
4526
  render,
@@ -3949,6 +4528,7 @@ const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldS
3949
4528
  direction = 'horizontal',
3950
4529
  pixelSensitivity = 2,
3951
4530
  teleportDistance,
4531
+ style,
3952
4532
  ...elementProps
3953
4533
  } = componentProps;
3954
4534
  const {
@@ -3958,6 +4538,7 @@ const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldS
3958
4538
  readOnly,
3959
4539
  inputRef,
3960
4540
  incrementValue,
4541
+ allowInputSyncRef,
3961
4542
  getStepAmount,
3962
4543
  onValueCommitted,
3963
4544
  lastChangedValueRef,
@@ -4005,15 +4586,15 @@ const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldS
4005
4586
  };
4006
4587
  const cursorWidth = virtualCursor.offsetWidth;
4007
4588
  const cursorHeight = virtualCursor.offsetHeight;
4008
- if (newCoords.x + cursorWidth / 2 < rect.x) {
4009
- newCoords.x = rect.width - cursorWidth / 2;
4010
- } else if (newCoords.x + cursorWidth / 2 > rect.width) {
4011
- newCoords.x = rect.x - cursorWidth / 2;
4589
+ if (newCoords.x + cursorWidth / 2 < rect.left) {
4590
+ newCoords.x = rect.right - cursorWidth / 2;
4591
+ } else if (newCoords.x + cursorWidth / 2 > rect.right) {
4592
+ newCoords.x = rect.left - cursorWidth / 2;
4012
4593
  }
4013
- if (newCoords.y + cursorHeight / 2 < rect.y) {
4014
- newCoords.y = rect.height - cursorHeight / 2;
4015
- } else if (newCoords.y + cursorHeight / 2 > rect.height) {
4016
- newCoords.y = rect.y - cursorHeight / 2;
4594
+ if (newCoords.y + cursorHeight / 2 < rect.top) {
4595
+ newCoords.y = rect.bottom - cursorHeight / 2;
4596
+ } else if (newCoords.y + cursorHeight / 2 > rect.bottom) {
4597
+ newCoords.y = rect.top - cursorHeight / 2;
4017
4598
  }
4018
4599
  virtualCursorCoords.current = newCoords;
4019
4600
  updateCursorTransform(newCoords.x, newCoords.y);
@@ -4056,8 +4637,10 @@ const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldS
4056
4637
 
4057
4638
  // Manually dispatch a click event if no movement happened, since
4058
4639
  // preventDefault on pointerdown prevents the browser click event.
4059
- if (!didMoveRef.current && pointerDownTargetRef.current != null) {
4060
- pointerDownTargetRef.current.dispatchEvent(new MouseEvent('click', {
4640
+ const pointerDownTarget = pointerDownTargetRef.current;
4641
+ const input = inputRef.current;
4642
+ if (!didMoveRef.current && pointerDownTarget != null && input) {
4643
+ pointerDownTarget.dispatchEvent(new (getWindow(input).MouseEvent)('click', {
4061
4644
  bubbles: true,
4062
4645
  cancelable: true
4063
4646
  }));
@@ -4066,7 +4649,7 @@ const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldS
4066
4649
  pointerDownTargetRef.current = null;
4067
4650
  }
4068
4651
  }
4069
- if (isFirefox) {
4652
+ if (gecko) {
4070
4653
  // Firefox needs a small delay here when soft-clicking as the pointer
4071
4654
  // lock will not release otherwise.
4072
4655
  exitPointerLockTimeout.start(20, handler);
@@ -4091,9 +4674,10 @@ const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldS
4091
4674
  cumulativeDelta = 0;
4092
4675
  didMoveRef.current = true;
4093
4676
  const dValue = direction === 'vertical' ? -movementY : movementX;
4094
- const stepAmount = getStepAmount(event) ?? DEFAULT_STEP;
4677
+ const stepAmount = getStepAmount(event);
4095
4678
  const rawAmount = dValue * stepAmount;
4096
4679
  if (rawAmount !== 0) {
4680
+ allowInputSyncRef.current = true;
4097
4681
  incrementValue(Math.abs(rawAmount), {
4098
4682
  direction: rawAmount >= 0 ? 1 : -1,
4099
4683
  event,
@@ -4103,14 +4687,26 @@ const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldS
4103
4687
  }
4104
4688
  }
4105
4689
  const win = getWindow(inputRef.current);
4106
- win.addEventListener('pointerup', handleScrubPointerUp, true);
4107
- win.addEventListener('pointermove', handleScrubPointerMove, true);
4690
+ const unsubscribe = mergeCleanups(addEventListener(win, 'pointerup', handleScrubPointerUp, true), addEventListener(win, 'pointermove', handleScrubPointerMove, true));
4108
4691
  return () => {
4109
4692
  exitPointerLockTimeout.clear();
4110
- win.removeEventListener('pointerup', handleScrubPointerUp, true);
4111
- win.removeEventListener('pointermove', handleScrubPointerMove, true);
4693
+ unsubscribe();
4112
4694
  };
4113
- }, [disabled, readOnly, incrementValue, isScrubbing, getStepAmount, inputRef, onScrubbingChange, onScrub, direction, pixelSensitivity, lastChangedValueRef, onValueCommitted, valueRef, exitPointerLockTimeout]);
4695
+ }, [disabled, readOnly, allowInputSyncRef, incrementValue, isScrubbing, getStepAmount, inputRef, onScrubbingChange, onScrub, direction, pixelSensitivity, lastChangedValueRef, onValueCommitted, valueRef, exitPointerLockTimeout]);
4696
+
4697
+ // If the scrub area unmounts mid-scrub, release pointer lock and clear the root's scrubbing
4698
+ // state so it doesn't stay locked or stuck. (No commit: there's no pointer release here.)
4699
+ React.useEffect(() => () => {
4700
+ if (isScrubbingRef.current) {
4701
+ isScrubbingRef.current = false;
4702
+ setRootScrubbing(false);
4703
+ try {
4704
+ ownerDocument(scrubAreaRef.current).exitPointerLock();
4705
+ } catch {
4706
+ // Ignore errors.
4707
+ }
4708
+ }
4709
+ }, [setRootScrubbing]);
4114
4710
 
4115
4711
  // Prevent scrolling using touch input when scrubbing.
4116
4712
  React.useEffect(function registerScrubberTouchPreventListener() {
@@ -4123,18 +4719,11 @@ const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldS
4123
4719
  event.preventDefault();
4124
4720
  }
4125
4721
  }
4126
- element.addEventListener('touchstart', handleTouchStart);
4127
- return () => {
4128
- element.removeEventListener('touchstart', handleTouchStart);
4129
- };
4722
+ return addEventListener(element, 'touchstart', handleTouchStart);
4130
4723
  }, [disabled, readOnly]);
4131
4724
  const defaultProps = {
4132
4725
  role: 'presentation',
4133
- style: {
4134
- touchAction: 'none',
4135
- WebkitUserSelect: 'none',
4136
- userSelect: 'none'
4137
- },
4726
+ style: SCRUB_AREA_STYLE,
4138
4727
  async onPointerDown(event) {
4139
4728
  const isMainButton = !event.button || event.button === 0;
4140
4729
  if (event.defaultPrevented || readOnly || !isMainButton || disabled) {
@@ -4148,11 +4737,11 @@ const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldS
4148
4737
  }
4149
4738
  isScrubbingRef.current = true;
4150
4739
  didMoveRef.current = false;
4151
- pointerDownTargetRef.current = event.target;
4740
+ pointerDownTargetRef.current = getTarget(event.nativeEvent);
4152
4741
  onScrubbingChange(true, event.nativeEvent);
4153
4742
 
4154
4743
  // WebKit causes significant layout shift with the native message, so we can't use it.
4155
- if (!isTouch && !isWebKit) {
4744
+ if (!isTouch && !webkit) {
4156
4745
  try {
4157
4746
  // Avoid non-deterministic errors in testing environments. This error sometimes
4158
4747
  // appears:
@@ -4162,10 +4751,11 @@ const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldS
4162
4751
  } catch (error) {
4163
4752
  setIsPointerLockDenied(true);
4164
4753
  } finally {
4754
+ // `onScrubbingChange` already wraps its state updates in `flushSync`, so re-emit the
4755
+ // scrubbing state directly (no extra nested `flushSync`) to reflect the resolved
4756
+ // pointer-lock result on the cursor.
4165
4757
  if (isScrubbingRef.current) {
4166
- ReactDOM.flushSync(() => {
4167
- onScrubbingChange(true, event.nativeEvent);
4168
- });
4758
+ onScrubbingChange(true, event.nativeEvent);
4169
4759
  }
4170
4760
  }
4171
4761
  }
@@ -4181,12 +4771,8 @@ const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldS
4181
4771
  isScrubbing,
4182
4772
  isTouchInput,
4183
4773
  isPointerLockDenied,
4184
- scrubAreaCursorRef,
4185
- scrubAreaRef,
4186
- direction,
4187
- pixelSensitivity,
4188
- teleportDistance
4189
- }), [isScrubbing, isTouchInput, isPointerLockDenied, direction, pixelSensitivity, teleportDistance]);
4774
+ scrubAreaCursorRef
4775
+ }), [isScrubbing, isTouchInput, isPointerLockDenied]);
4190
4776
  return /*#__PURE__*/jsx(NumberFieldScrubAreaContext.Provider, {
4191
4777
  value: contextValue,
4192
4778
  children: element
@@ -4194,6 +4780,13 @@ const NumberFieldScrubArea = /*#__PURE__*/React.forwardRef(function NumberFieldS
4194
4780
  });
4195
4781
  if (process.env.NODE_ENV !== "production") NumberFieldScrubArea.displayName = "NumberFieldScrubArea";
4196
4782
 
4783
+ const CURSOR_STYLE = {
4784
+ position: 'fixed',
4785
+ top: 0,
4786
+ left: 0,
4787
+ pointerEvents: 'none'
4788
+ };
4789
+
4197
4790
  /**
4198
4791
  * A custom element to display instead of the native cursor while using the scrub area.
4199
4792
  * Renders a `<span>` element.
@@ -4207,6 +4800,7 @@ const NumberFieldScrubAreaCursor = /*#__PURE__*/React.forwardRef(function Number
4207
4800
  const {
4208
4801
  render,
4209
4802
  className,
4803
+ style,
4210
4804
  ...elementProps
4211
4805
  } = componentProps;
4212
4806
  const {
@@ -4219,19 +4813,14 @@ const NumberFieldScrubAreaCursor = /*#__PURE__*/React.forwardRef(function Number
4219
4813
  scrubAreaCursorRef
4220
4814
  } = useNumberFieldScrubAreaContext();
4221
4815
  const [domElement, setDomElement] = React.useState(null);
4222
- const shouldRender = isScrubbing && !isWebKit && !isTouchInput && !isPointerLockDenied;
4816
+ const shouldRender = isScrubbing && !webkit && !isTouchInput && !isPointerLockDenied;
4223
4817
  const element = useRenderElement('span', componentProps, {
4224
4818
  enabled: shouldRender,
4225
4819
  ref: [forwardedRef, scrubAreaCursorRef, setDomElement],
4226
4820
  state,
4227
4821
  props: [{
4228
4822
  role: 'presentation',
4229
- style: {
4230
- position: 'fixed',
4231
- top: 0,
4232
- left: 0,
4233
- pointerEvents: 'none'
4234
- }
4823
+ style: CURSOR_STYLE
4235
4824
  }, elementProps],
4236
4825
  stateAttributesMapping: stateAttributesMapping$6
4237
4826
  });
@@ -4260,6 +4849,7 @@ const SelectLabel = /*#__PURE__*/React.forwardRef(function SelectLabel(component
4260
4849
  const {
4261
4850
  render,
4262
4851
  className,
4852
+ style,
4263
4853
  ...elementProps
4264
4854
  } = componentProps;
4265
4855
  // Keep label id derived from the root and ignore runtime `id` overrides from untyped consumers.
@@ -4296,8 +4886,9 @@ if (process.env.NODE_ENV !== "production") SelectLabel.displayName = "SelectLabe
4296
4886
  */
4297
4887
  const SelectIcon = /*#__PURE__*/React.forwardRef(function SelectIcon(componentProps, forwardedRef) {
4298
4888
  const {
4299
- className,
4300
4889
  render,
4890
+ className,
4891
+ style,
4301
4892
  ...elementProps
4302
4893
  } = componentProps;
4303
4894
  const {
@@ -4326,15 +4917,16 @@ const stateAttributesMapping$1 = {
4326
4917
  };
4327
4918
 
4328
4919
  /**
4329
- * An overlay displayed beneath the menu popup.
4920
+ * An overlay displayed beneath the select popup.
4330
4921
  * Renders a `<div>` element.
4331
4922
  *
4332
4923
  * Documentation: [Base UI Select](https://base-ui.com/react/components/select)
4333
4924
  */
4334
4925
  const SelectBackdrop = /*#__PURE__*/React.forwardRef(function SelectBackdrop(componentProps, forwardedRef) {
4335
4926
  const {
4336
- className,
4337
4927
  render,
4928
+ className,
4929
+ style,
4338
4930
  ...elementProps
4339
4931
  } = componentProps;
4340
4932
  const {
@@ -4377,8 +4969,9 @@ const stateAttributesMapping = {
4377
4969
  */
4378
4970
  const SelectArrow = /*#__PURE__*/React.forwardRef(function SelectArrow(componentProps, forwardedRef) {
4379
4971
  const {
4380
- className,
4381
4972
  render,
4973
+ className,
4974
+ style,
4382
4975
  ...elementProps
4383
4976
  } = componentProps;
4384
4977
  const {
@@ -4392,7 +4985,7 @@ const SelectArrow = /*#__PURE__*/React.forwardRef(function SelectArrow(component
4392
4985
  arrowUncentered,
4393
4986
  alignItemWithTriggerActive
4394
4987
  } = useSelectPositionerContext();
4395
- const open = useStore(store, selectors.open, true);
4988
+ const open = useStore(store, selectors.open);
4396
4989
  const state = {
4397
4990
  open,
4398
4991
  side,
@@ -4422,10 +5015,12 @@ const SelectScrollArrow = /*#__PURE__*/React.forwardRef(function SelectScrollArr
4422
5015
  const {
4423
5016
  render,
4424
5017
  className,
5018
+ style,
4425
5019
  direction,
4426
5020
  keepMounted = false,
4427
5021
  ...elementProps
4428
5022
  } = componentProps;
5023
+ const isUp = direction === 'up';
4429
5024
  const {
4430
5025
  store,
4431
5026
  popupRef,
@@ -4438,15 +5033,16 @@ const SelectScrollArrow = /*#__PURE__*/React.forwardRef(function SelectScrollArr
4438
5033
  scrollDownArrowRef,
4439
5034
  scrollUpArrowRef
4440
5035
  } = useSelectPositionerContext();
4441
- const visibleSelector = direction === 'up' ? selectors.scrollUpArrowVisible : selectors.scrollDownArrowVisible;
5036
+ const visibleSelector = isUp ? selectors.scrollUpArrowVisible : selectors.scrollDownArrowVisible;
4442
5037
  const stateVisible = useStore(store, visibleSelector);
4443
5038
  const openMethod = useStore(store, selectors.openMethod);
4444
5039
 
4445
5040
  // Scroll arrows are disabled for touch modality as they are a hover-only element.
4446
5041
  const visible = stateVisible && openMethod !== 'touch';
4447
5042
  const timeout = useTimeout();
4448
- const scrollArrowRef = direction === 'up' ? scrollUpArrowRef : scrollDownArrowRef;
5043
+ const scrollArrowRef = isUp ? scrollUpArrowRef : scrollDownArrowRef;
4449
5044
  const {
5045
+ mounted,
4450
5046
  transitionStatus,
4451
5047
  setMounted
4452
5048
  } = useTransitionStatus(visible);
@@ -4479,7 +5075,7 @@ const SelectScrollArrow = /*#__PURE__*/React.forwardRef(function SelectScrollArr
4479
5075
  };
4480
5076
  const defaultProps = {
4481
5077
  'aria-hidden': true,
4482
- children: direction === 'up' ? '▲' : '▼',
5078
+ children: isUp ? '▲' : '▼',
4483
5079
  style: {
4484
5080
  position: 'absolute'
4485
5081
  },
@@ -4495,72 +5091,25 @@ const SelectScrollArrow = /*#__PURE__*/React.forwardRef(function SelectScrollArr
4495
5091
  }
4496
5092
  store.set('activeIndex', null);
4497
5093
  handleScrollArrowVisibility();
4498
- const isScrolledToTop = scroller.scrollTop === 0;
4499
- const isScrolledToBottom = Math.round(scroller.scrollTop + scroller.clientHeight) >= scroller.scrollHeight;
4500
- const list = listRef.current;
5094
+ const maxScrollTop = getMaxScrollOffset(scroller.scrollHeight, scroller.clientHeight);
5095
+ const scrollTop = normalizeScrollOffset(scroller.scrollTop, maxScrollTop);
5096
+ const isScrolledToEdge = scrollTop === (isUp ? 0 : maxScrollTop);
5097
+ const items = listRef.current;
5098
+ if (scrollTop !== scroller.scrollTop) {
5099
+ scroller.scrollTop = scrollTop;
5100
+ }
4501
5101
 
4502
5102
  // Fallback when there are no items registered yet.
4503
- if (list.length === 0) {
4504
- if (direction === 'up') {
4505
- store.set('scrollUpArrowVisible', !isScrolledToTop);
4506
- } else {
4507
- store.set('scrollDownArrowVisible', !isScrolledToBottom);
4508
- }
5103
+ if (items.length === 0) {
5104
+ store.set(isUp ? 'scrollUpArrowVisible' : 'scrollDownArrowVisible', !isScrolledToEdge);
4509
5105
  }
4510
- if (direction === 'up' && isScrolledToTop || direction === 'down' && isScrolledToBottom) {
5106
+ if (isScrolledToEdge) {
4511
5107
  timeout.clear();
4512
5108
  return;
4513
5109
  }
4514
- if ((store.state.listElement || popupRef.current) && listRef.current && listRef.current.length > 0) {
4515
- const items = listRef.current;
5110
+ if (items.length > 0) {
4516
5111
  const scrollArrowHeight = scrollArrowRef.current?.offsetHeight || 0;
4517
- if (direction === 'up') {
4518
- let firstVisibleIndex = 0;
4519
- const scrollTop = scroller.scrollTop + scrollArrowHeight;
4520
- for (let i = 0; i < items.length; i += 1) {
4521
- const item = items[i];
4522
- if (item) {
4523
- const itemTop = item.offsetTop;
4524
- if (itemTop >= scrollTop) {
4525
- firstVisibleIndex = i;
4526
- break;
4527
- }
4528
- }
4529
- }
4530
- const targetIndex = Math.max(0, firstVisibleIndex - 1);
4531
- if (targetIndex < firstVisibleIndex) {
4532
- const targetItem = items[targetIndex];
4533
- if (targetItem) {
4534
- scroller.scrollTop = Math.max(0, targetItem.offsetTop - scrollArrowHeight);
4535
- }
4536
- } else {
4537
- // Already at the first item; ensure we reach the absolute top to account for group labels.
4538
- scroller.scrollTop = 0;
4539
- }
4540
- } else {
4541
- let lastVisibleIndex = items.length - 1;
4542
- const scrollBottom = scroller.scrollTop + scroller.clientHeight - scrollArrowHeight;
4543
- for (let i = 0; i < items.length; i += 1) {
4544
- const item = items[i];
4545
- if (item) {
4546
- const itemBottom = item.offsetTop + item.offsetHeight;
4547
- if (itemBottom > scrollBottom) {
4548
- lastVisibleIndex = Math.max(0, i - 1);
4549
- break;
4550
- }
4551
- }
4552
- }
4553
- const targetIndex = Math.min(items.length - 1, lastVisibleIndex + 1);
4554
- if (targetIndex > lastVisibleIndex) {
4555
- const targetItem = items[targetIndex];
4556
- if (targetItem) {
4557
- scroller.scrollTop = targetItem.offsetTop + targetItem.offsetHeight - scroller.clientHeight + scrollArrowHeight;
4558
- }
4559
- } else {
4560
- // Already at the last item; ensure we reach the true bottom.
4561
- scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight;
4562
- }
4563
- }
5112
+ scroller.scrollTop = getTargetScrollTop(items, isUp, scrollTop, scroller.clientHeight, scrollArrowHeight, maxScrollTop);
4564
5113
  }
4565
5114
  timeout.start(40, scrollNextItem);
4566
5115
  }
@@ -4573,15 +5122,44 @@ const SelectScrollArrow = /*#__PURE__*/React.forwardRef(function SelectScrollArr
4573
5122
  const element = useRenderElement('div', componentProps, {
4574
5123
  ref: [forwardedRef, scrollArrowRef],
4575
5124
  state,
4576
- props: [defaultProps, elementProps]
5125
+ props: [defaultProps, elementProps],
5126
+ stateAttributesMapping: transitionStatusMapping
4577
5127
  });
4578
- const shouldRender = visible || keepMounted;
5128
+ const shouldRender = mounted || keepMounted;
4579
5129
  if (!shouldRender) {
4580
5130
  return null;
4581
5131
  }
4582
5132
  return element;
4583
5133
  });
4584
5134
  if (process.env.NODE_ENV !== "production") SelectScrollArrow.displayName = "SelectScrollArrow";
5135
+ function getTargetScrollTop(items, isUp, scrollTop, clientHeight, scrollArrowHeight, maxScrollTop) {
5136
+ if (isUp) {
5137
+ let firstVisibleIndex = 0;
5138
+ const visibleTop = scrollTop + scrollArrowHeight - SCROLL_EDGE_TOLERANCE_PX;
5139
+ for (let i = 0; i < items.length; i += 1) {
5140
+ const item = items[i];
5141
+ if (item && item.offsetTop >= visibleTop) {
5142
+ firstVisibleIndex = i;
5143
+ break;
5144
+ }
5145
+ }
5146
+ const targetIndex = Math.max(0, firstVisibleIndex - 1);
5147
+ const targetItem = items[targetIndex];
5148
+ return targetIndex < firstVisibleIndex && targetItem ? normalizeScrollOffset(targetItem.offsetTop - scrollArrowHeight, maxScrollTop) : 0;
5149
+ }
5150
+ let lastVisibleIndex = items.length - 1;
5151
+ const visibleBottom = scrollTop + clientHeight - scrollArrowHeight + SCROLL_EDGE_TOLERANCE_PX;
5152
+ for (let i = 0; i < items.length; i += 1) {
5153
+ const item = items[i];
5154
+ if (item && item.offsetTop + item.offsetHeight > visibleBottom) {
5155
+ lastVisibleIndex = Math.max(0, i - 1);
5156
+ break;
5157
+ }
5158
+ }
5159
+ const targetIndex = Math.min(items.length - 1, lastVisibleIndex + 1);
5160
+ const targetItem = items[targetIndex];
5161
+ return targetIndex > lastVisibleIndex && targetItem ? normalizeScrollOffset(targetItem.offsetTop + targetItem.offsetHeight - clientHeight + scrollArrowHeight, maxScrollTop) : maxScrollTop;
5162
+ }
4585
5163
 
4586
5164
  /**
4587
5165
  * An element that scrolls the select popup down when hovered. Does not render when using touch input.