@jobber/components 9.5.3 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/Combobox.d.ts +1 -0
  2. package/Combobox.js +17 -0
  3. package/dist/Autocomplete/index.cjs +0 -2
  4. package/dist/Autocomplete/index.mjs +0 -2
  5. package/dist/Autocomplete-cjs.js +4 -5
  6. package/dist/Autocomplete-es.js +1 -2
  7. package/dist/Card/index.cjs +2 -2
  8. package/dist/Card/index.mjs +2 -2
  9. package/dist/Chip/index.cjs +0 -2
  10. package/dist/Chip/index.mjs +0 -2
  11. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +0 -2
  12. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +0 -2
  13. package/dist/Chips/InternalChipDismissible/index.cjs +0 -2
  14. package/dist/Chips/InternalChipDismissible/index.mjs +0 -2
  15. package/dist/Chips/index.cjs +0 -2
  16. package/dist/Chips/index.mjs +0 -2
  17. package/dist/Combobox/Combobox.contentSlots.d.ts +5 -0
  18. package/dist/Combobox/Combobox.context.d.ts +34 -0
  19. package/dist/Combobox/Combobox.d.ts +69 -0
  20. package/dist/Combobox/Combobox.footer.d.ts +7 -0
  21. package/dist/Combobox/Combobox.items.d.ts +25 -0
  22. package/dist/Combobox/Combobox.responsive.d.ts +54 -0
  23. package/dist/Combobox/Combobox.selectAll.d.ts +4 -0
  24. package/dist/Combobox/Combobox.slotFactory.d.ts +7 -0
  25. package/dist/Combobox/Combobox.testUtils.d.ts +12 -0
  26. package/dist/Combobox/Combobox.types.d.ts +166 -0
  27. package/dist/Combobox/index.cjs +35 -0
  28. package/dist/Combobox/index.d.ts +2 -0
  29. package/dist/Combobox/index.mjs +29 -0
  30. package/dist/Combobox-cjs.js +675 -0
  31. package/dist/Combobox-es.js +673 -0
  32. package/dist/ComboboxPrimitive-cjs.js +88 -47
  33. package/dist/ComboboxPrimitive-es.js +39 -23
  34. package/dist/{ComboboxChipRemove-cjs.js → ComboboxSeparator-cjs.js} +501 -455
  35. package/dist/{ComboboxChipRemove-es.js → ComboboxSeparator-es.js} +500 -456
  36. package/dist/ConfirmationModal/index.cjs +0 -2
  37. package/dist/ConfirmationModal/index.mjs +0 -2
  38. package/dist/ContentBlock/index.cjs +1 -0
  39. package/dist/ContentBlock/index.mjs +1 -0
  40. package/dist/ContentBlock-cjs.js +2 -18
  41. package/dist/ContentBlock-es.js +1 -17
  42. package/dist/DataDump/index.cjs +2 -2
  43. package/dist/DataDump/index.mjs +2 -2
  44. package/dist/DataList/components/DataListActions/index.cjs +0 -2
  45. package/dist/DataList/components/DataListActions/index.mjs +0 -2
  46. package/dist/DataList/components/DataListBulkActions/index.cjs +0 -2
  47. package/dist/DataList/components/DataListBulkActions/index.mjs +0 -2
  48. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +0 -2
  49. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +0 -2
  50. package/dist/DataList/components/DataListFilters/index.cjs +0 -2
  51. package/dist/DataList/components/DataListFilters/index.mjs +0 -2
  52. package/dist/DataList/components/DataListHeader/index.cjs +0 -2
  53. package/dist/DataList/components/DataListHeader/index.mjs +0 -2
  54. package/dist/DataList/components/DataListItem/index.cjs +0 -2
  55. package/dist/DataList/components/DataListItem/index.mjs +0 -2
  56. package/dist/DataList/components/DataListItemActions/index.cjs +0 -2
  57. package/dist/DataList/components/DataListItemActions/index.mjs +0 -2
  58. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +0 -2
  59. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +0 -2
  60. package/dist/DataList/components/DataListItems/index.cjs +0 -2
  61. package/dist/DataList/components/DataListItems/index.mjs +0 -2
  62. package/dist/DataList/components/DataListLayout/index.cjs +0 -2
  63. package/dist/DataList/components/DataListLayout/index.mjs +0 -2
  64. package/dist/DataList/components/DataListLayoutActions/index.cjs +0 -2
  65. package/dist/DataList/components/DataListLayoutActions/index.mjs +0 -2
  66. package/dist/DataList/index.cjs +0 -2
  67. package/dist/DataList/index.mjs +0 -2
  68. package/dist/DatePicker/index.cjs +0 -2
  69. package/dist/DatePicker/index.mjs +0 -2
  70. package/dist/DatePicker-cjs.js +4 -5
  71. package/dist/DatePicker-es.js +1 -2
  72. package/dist/Dialog/index.cjs +2 -2
  73. package/dist/Dialog/index.mjs +2 -2
  74. package/dist/DrawerDescription-cjs.js +268 -99
  75. package/dist/DrawerDescription-es.js +270 -103
  76. package/dist/DrawerTitle-cjs.js +2 -6
  77. package/dist/DrawerTitle-es.js +3 -7
  78. package/dist/FieldDescription-cjs.js +184 -181
  79. package/dist/FieldDescription-es.js +187 -184
  80. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +0 -2
  81. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +0 -2
  82. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +0 -2
  83. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +0 -2
  84. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +0 -2
  85. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +0 -2
  86. package/dist/FilterPicker/index.cjs +0 -2
  87. package/dist/FilterPicker/index.mjs +0 -2
  88. package/dist/FilterPickerContent-cjs.js +4 -5
  89. package/dist/FilterPickerContent-es.js +1 -2
  90. package/dist/FormatFile/index.cjs +0 -2
  91. package/dist/FormatFile/index.mjs +0 -2
  92. package/dist/Gallery/index.cjs +0 -2
  93. package/dist/Gallery/index.mjs +0 -2
  94. package/dist/InputDate/index.cjs +0 -2
  95. package/dist/InputDate/index.mjs +0 -2
  96. package/dist/InputNumber/index.cjs +2 -2
  97. package/dist/InputNumber/index.mjs +2 -2
  98. package/dist/InputNumber/types.d.ts +5 -0
  99. package/dist/InputNumber-cjs.js +2 -2
  100. package/dist/InputNumber-es.js +2 -2
  101. package/dist/LightBox/index.cjs +0 -2
  102. package/dist/LightBox/index.mjs +0 -2
  103. package/dist/Menu/index.cjs +2 -2
  104. package/dist/Menu/index.mjs +2 -2
  105. package/dist/Menu-cjs.js +4 -5
  106. package/dist/Menu-es.js +1 -2
  107. package/dist/MenuSubmenuTrigger-cjs.js +206 -164
  108. package/dist/MenuSubmenuTrigger-es.js +194 -152
  109. package/dist/Modal/index.cjs +0 -2
  110. package/dist/Modal/index.mjs +1 -3
  111. package/dist/NumberFieldInput-cjs.js +187 -308
  112. package/dist/NumberFieldInput-es.js +187 -308
  113. package/dist/Page/index.cjs +2 -2
  114. package/dist/Page/index.mjs +2 -2
  115. package/dist/Popover/index.cjs +0 -2
  116. package/dist/Popover/index.mjs +0 -2
  117. package/dist/Popover-cjs.js +7 -8
  118. package/dist/Popover-es.js +1 -2
  119. package/dist/ScrollAreaViewport-cjs.js +747 -1093
  120. package/dist/ScrollAreaViewport-es.js +734 -1081
  121. package/dist/Select/index.cjs +2 -2
  122. package/dist/Select/index.mjs +2 -2
  123. package/dist/SelectGroupLabel-cjs.js +207 -306
  124. package/dist/SelectGroupLabel-es.js +203 -302
  125. package/dist/SelectPrimitive-cjs.js +11 -17
  126. package/dist/SelectPrimitive-es.js +1 -7
  127. package/dist/Tooltip/index.cjs +0 -2
  128. package/dist/Tooltip/index.mjs +0 -2
  129. package/dist/Tooltip-cjs.js +6 -7
  130. package/dist/Tooltip-es.js +1 -2
  131. package/dist/buttonRenderAdapter-cjs.js +2 -1
  132. package/dist/buttonRenderAdapter-es.js +2 -1
  133. package/dist/dialogReturnFocus-cjs.js +1 -1
  134. package/dist/dialogReturnFocus-es.js +2 -2
  135. package/dist/docs/InputNumber/InputNumber.md +2 -2
  136. package/dist/docs/SelectPrimitive/SelectPrimitive.md +1 -1
  137. package/dist/floating-ui.react-cjs.js +2557 -185
  138. package/dist/floating-ui.react-es.js +2512 -149
  139. package/dist/getMappedBreakpointWidth-cjs.js +21 -0
  140. package/dist/getMappedBreakpointWidth-es.js +18 -0
  141. package/dist/index.cjs +15 -10
  142. package/dist/index.d.mts +1 -0
  143. package/dist/index.d.ts +1 -0
  144. package/dist/index.mjs +14 -10
  145. package/dist/mergeClassName-cjs.js +12 -0
  146. package/dist/mergeClassName-es.js +10 -0
  147. package/dist/primitives/BottomSheet/index.cjs +1 -1
  148. package/dist/primitives/BottomSheet/index.mjs +1 -1
  149. package/dist/primitives/ComboboxPrimitive/ComboboxPrimitive.d.ts +2 -2
  150. package/dist/primitives/ComboboxPrimitive/index.cjs +4 -5
  151. package/dist/primitives/ComboboxPrimitive/index.mjs +4 -5
  152. package/dist/primitives/SelectPrimitive/index.cjs +1 -2
  153. package/dist/primitives/SelectPrimitive/index.mjs +1 -2
  154. package/dist/primitives/index.cjs +3 -3
  155. package/dist/primitives/index.mjs +3 -3
  156. package/dist/scrollEdges-cjs.js +1 -11
  157. package/dist/scrollEdges-es.js +1 -11
  158. package/dist/scrollable-cjs.js +58 -0
  159. package/dist/scrollable-es.js +54 -0
  160. package/dist/styles.css +759 -42
  161. package/dist/unstyledPrimitives/index.cjs +720 -478
  162. package/dist/unstyledPrimitives/index.mjs +663 -421
  163. package/dist/useCompositeListItem-cjs.js +2108 -361
  164. package/dist/useCompositeListItem-es.js +2076 -331
  165. package/dist/useLabel-cjs.js +2 -3
  166. package/dist/useLabel-es.js +2 -3
  167. package/dist/useRenderElement-cjs.js +9 -7
  168. package/dist/useRenderElement-es.js +9 -7
  169. package/dist/useScrollLock-cjs.js +819 -344
  170. package/dist/useScrollLock-es.js +732 -296
  171. package/dist/useScrollToActive-cjs.js +4 -5
  172. package/dist/useScrollToActive-es.js +1 -2
  173. package/dist/utils/meta/meta.json +43 -1
  174. package/package.json +8 -3
  175. package/dist/floating-ui.react-dom-cjs.js +0 -2103
  176. package/dist/floating-ui.react-dom-es.js +0 -2072
  177. package/dist/floating-ui.utils-cjs.js +0 -342
  178. package/dist/floating-ui.utils-es.js +0 -300
@@ -7,7 +7,6 @@ var jsxRuntime = require('react/jsx-runtime');
7
7
  var useCompositeListItem = require('./useCompositeListItem-cjs.js');
8
8
  var SelectGroupLabel = require('./SelectGroupLabel-cjs.js');
9
9
  var scrollEdges = require('./scrollEdges-cjs.js');
10
- var floatingUi_utils = require('./floating-ui.utils-cjs.js');
11
10
  var ScrollAreaViewport = require('./ScrollAreaViewport-cjs.js');
12
11
  var ReactDOM = require('react-dom');
13
12
 
@@ -86,14 +85,14 @@ function useFocus(context, props = {}) {
86
85
  if (!enabled) {
87
86
  return undefined;
88
87
  }
89
- const win = floatingUi_utils.getWindow(domReference);
88
+ const win = useScrollLock.getWindow(domReference);
90
89
 
91
90
  // If the reference was focused and the user left the tab/window, and the
92
91
  // floating element was not open, the focus should be blocked when they
93
92
  // return to the tab/window.
94
93
  function onBlur() {
95
94
  const currentDomReference = store.select('domReferenceElement');
96
- if (!store.select('open') && floatingUi_utils.isHTMLElement(currentDomReference) && currentDomReference === useScrollLock.activeElement(useScrollLock.ownerDocument(currentDomReference))) {
95
+ if (!store.select('open') && useScrollLock.isHTMLElement(currentDomReference) && currentDomReference === useScrollLock.activeElement(useScrollLock.ownerDocument(currentDomReference))) {
97
96
  blockFocusRef.current = true;
98
97
  }
99
98
  }
@@ -112,7 +111,7 @@ function useFocus(context, props = {}) {
112
111
  function onOpenChangeLocal(details) {
113
112
  if (details.reason === useScrollLock.triggerPress || details.reason === useScrollLock.escapeKey) {
114
113
  const referenceElement = store.select('domReferenceElement');
115
- if (floatingUi_utils.isElement(referenceElement)) {
114
+ if (useScrollLock.isElement(referenceElement)) {
116
115
  blockedReferenceRef.current = referenceElement;
117
116
  blockFocusRef.current = true;
118
117
  }
@@ -141,7 +140,7 @@ function useFocus(context, props = {}) {
141
140
  resetBlockedFocus();
142
141
  }
143
142
  const target = useScrollLock.getTarget(event.nativeEvent);
144
- if (floatingUi_utils.isElement(target)) {
143
+ if (useScrollLock.isElement(target)) {
145
144
  // Safari fails to match `:focus-visible` if focus was initially
146
145
  // outside the document.
147
146
  if (isMacSafari && !event.relatedTarget) {
@@ -176,7 +175,7 @@ function useFocus(context, props = {}) {
176
175
 
177
176
  // Hit the non-modal focus management portal guard. Focus will be
178
177
  // moved into the floating element immediately after.
179
- const movedToFocusGuard = floatingUi_utils.isElement(relatedTarget) && relatedTarget.hasAttribute(useScrollLock.createAttribute('focus-guard')) && relatedTarget.getAttribute('data-type') === 'outside';
178
+ const movedToFocusGuard = useScrollLock.isElement(relatedTarget) && relatedTarget.hasAttribute(useScrollLock.createAttribute('focus-guard')) && relatedTarget.getAttribute('data-type') === 'outside';
180
179
 
181
180
  // Wait for the window blur listener to fire.
182
181
  timeout.start(0, () => {
@@ -334,7 +333,7 @@ function useHoverFloatingInteraction(context, parameters = {}) {
334
333
  if (!enabled) {
335
334
  return undefined;
336
335
  }
337
- if (open && instance.handleCloseOptions?.blockPointerEvents && isHoverOpen() && floatingUi_utils.isElement(domReferenceElement) && floatingElement) {
336
+ if (open && instance.handleCloseOptions?.blockPointerEvents && isHoverOpen() && useScrollLock.isElement(domReferenceElement) && floatingElement) {
338
337
  const ref = domReferenceElement;
339
338
  const floatingEl = floatingElement;
340
339
  const doc = useScrollLock.ownerDocument(floatingElement);
@@ -406,7 +405,7 @@ function useHoverFloatingInteraction(context, parameters = {}) {
406
405
  }
407
406
  const currentNodeId = dataRef.current.floatingContext?.nodeId ?? nodeIdProp;
408
407
  const relatedTarget = event.relatedTarget;
409
- const isMovingIntoDescendantFloating = tree && currentNodeId && floatingUi_utils.isElement(relatedTarget) && useScrollLock.getNodeChildren(tree.nodesRef.current, currentNodeId, false).some(node => useScrollLock.contains(node.context?.elements.floating, relatedTarget));
408
+ const isMovingIntoDescendantFloating = tree && currentNodeId && useScrollLock.isElement(relatedTarget) && useScrollLock.getNodeChildren(tree.nodesRef.current, currentNodeId, false).some(node => useScrollLock.contains(node.context?.elements.floating, relatedTarget));
410
409
  if (isMovingIntoDescendantFloating) {
411
410
  return;
412
411
  }
@@ -460,7 +459,8 @@ function useHoverReferenceInteraction(context, props = {}) {
460
459
  isActiveTrigger = true,
461
460
  getHandleCloseContext,
462
461
  isClosing,
463
- shouldOpen: shouldOpenProp
462
+ shouldOpen: shouldOpenProp,
463
+ guardStaleOpen = false
464
464
  } = props;
465
465
  const store = 'rootStore' in context ? context.rootStore : context;
466
466
  const {
@@ -491,7 +491,7 @@ function useHoverReferenceInteraction(context, props = {}) {
491
491
  }
492
492
 
493
493
  // Fallback for delegated/wrapper usage where currentTarget may be outside the trigger map.
494
- if (!floatingUi_utils.isElement(target)) {
494
+ if (!useScrollLock.isElement(target)) {
495
495
  return false;
496
496
  }
497
497
  const targetElement = target;
@@ -555,7 +555,7 @@ function useHoverReferenceInteraction(context, props = {}) {
555
555
  }
556
556
  }
557
557
  const trigger = triggerElementRef.current ?? (isActiveTrigger ? store.select('domReferenceElement') : null);
558
- if (!floatingUi_utils.isElement(trigger)) {
558
+ if (!useScrollLock.isElement(trigger)) {
559
559
  return undefined;
560
560
  }
561
561
  function onMouseEnter(event) {
@@ -575,7 +575,7 @@ function useHoverReferenceInteraction(context, props = {}) {
575
575
  let triggerNode = currentTarget;
576
576
 
577
577
  // Wrapper/delegated mode: resolve the actual trigger from the event target.
578
- if (floatingUi_utils.isElement(eventTarget) && !store.context.triggerElements.hasElement(eventTarget)) {
578
+ if (useScrollLock.isElement(eventTarget) && !store.context.triggerElements.hasElement(eventTarget)) {
579
579
  for (const triggerElement of store.context.triggerElements.elements()) {
580
580
  if (useScrollLock.contains(triggerElement, eventTarget)) {
581
581
  triggerNode = triggerElement;
@@ -586,14 +586,14 @@ function useHoverReferenceInteraction(context, props = {}) {
586
586
 
587
587
  // Wrapper/delegated mode fallback: if the wrapper contains the active trigger,
588
588
  // treat this as re-entering that active trigger.
589
- if (floatingUi_utils.isElement(currentTarget) && floatingUi_utils.isElement(currentDomReference) && !store.context.triggerElements.hasElement(currentTarget) && useScrollLock.contains(currentTarget, currentDomReference)) {
589
+ if (useScrollLock.isElement(currentTarget) && useScrollLock.isElement(currentDomReference) && !store.context.triggerElements.hasElement(currentTarget) && useScrollLock.contains(currentTarget, currentDomReference)) {
590
590
  triggerNode = currentDomReference;
591
591
  }
592
592
  const isOverInactive = triggerNode == null ? false : isOverInactiveTrigger(currentDomReference, triggerNode, eventTarget);
593
593
  const isOpen = store.select('open');
594
594
  const isInClosingTransition = isClosingRef.current?.() ?? store.select('transitionStatus') === 'ending';
595
595
  const isHoverCloseTransition = !isOpen && isInClosingTransition && isHoverCloseActiveRef.current;
596
- const isReenteringSameTriggerDuringCloseTransition = !isOverInactive && floatingUi_utils.isElement(triggerNode) && floatingUi_utils.isElement(currentDomReference) && useScrollLock.contains(currentDomReference, triggerNode) && isHoverCloseTransition;
596
+ const isReenteringSameTriggerDuringCloseTransition = !isOverInactive && useScrollLock.isElement(triggerNode) && useScrollLock.isElement(currentDomReference) && useScrollLock.contains(currentDomReference, triggerNode) && isHoverCloseTransition;
597
597
  const isRestOnlyDelay = restMsValue > 0 && !openDelay;
598
598
  const shouldOpenImmediately = isOverInactive && (isOpen || isHoverCloseTransition) || isReenteringSameTriggerDuringCloseTransition;
599
599
  const shouldOpen = !isOpen || isOverInactive;
@@ -662,13 +662,24 @@ function useHoverReferenceInteraction(context, props = {}) {
662
662
  closeWithDelay(event);
663
663
  }
664
664
  }
665
+
666
+ // Backup cancellation for Chrome's dropped `mouseleave` — see `guardStaleOpen`.
667
+ function onMouseOut(event) {
668
+ if (useScrollLock.contains(trigger, event.relatedTarget)) {
669
+ return; // moved within the trigger's own subtree
670
+ }
671
+ instance.openChangeTimeout.clear();
672
+ instance.restTimeout.clear();
673
+ instance.restTimeoutPending = false;
674
+ }
675
+ const staleOpenGuard = guardStaleOpen ? useScrollLock.addEventListener(trigger, 'mouseout', onMouseOut) : undefined;
665
676
  if (move) {
666
677
  return useScrollLock.mergeCleanups(useScrollLock.addEventListener(trigger, 'mousemove', onMouseEnter, {
667
678
  once: true
668
- }), useScrollLock.addEventListener(trigger, 'mouseenter', onMouseEnter), useScrollLock.addEventListener(trigger, 'mouseleave', onMouseLeave));
679
+ }), useScrollLock.addEventListener(trigger, 'mouseenter', onMouseEnter), useScrollLock.addEventListener(trigger, 'mouseleave', onMouseLeave), staleOpenGuard);
669
680
  }
670
- return useScrollLock.mergeCleanups(useScrollLock.addEventListener(trigger, 'mouseenter', onMouseEnter), useScrollLock.addEventListener(trigger, 'mouseleave', onMouseLeave));
671
- }, [cleanupMouseMoveHandler, clearPointerEvents, dataRef, delayRef, store, enabled, handleCloseRef, instance, isActiveTrigger, isOverInactiveTrigger, isClickLikeOpenEvent$1, mouseOnly, move, restMsRef, triggerElementRef, tree, enabledRef, getHandleCloseContext, isClosingRef, checkShouldOpen]);
681
+ return useScrollLock.mergeCleanups(useScrollLock.addEventListener(trigger, 'mouseenter', onMouseEnter), useScrollLock.addEventListener(trigger, 'mouseleave', onMouseLeave), staleOpenGuard);
682
+ }, [cleanupMouseMoveHandler, clearPointerEvents, dataRef, delayRef, store, enabled, handleCloseRef, instance, isActiveTrigger, isOverInactiveTrigger, isClickLikeOpenEvent$1, mouseOnly, move, restMsRef, triggerElementRef, tree, enabledRef, getHandleCloseContext, isClosingRef, checkShouldOpen, guardStaleOpen]);
672
683
  return React__namespace.useMemo(() => {
673
684
  if (!enabled) {
674
685
  return undefined;
@@ -848,7 +859,7 @@ function safePolygon(options = {}) {
848
859
 
849
860
  // Prevent overlapping floating element from being stuck in an open-close
850
861
  // loop: https://github.com/floating-ui/floating-ui/issues/1910
851
- if (isLeave && floatingUi_utils.isElement(event.relatedTarget) && useScrollLock.contains(floating, event.relatedTarget)) {
862
+ if (isLeave && useScrollLock.isElement(event.relatedTarget) && useScrollLock.contains(floating, event.relatedTarget)) {
852
863
  return undefined;
853
864
  }
854
865
  function hasOpenChildNode() {
@@ -1009,11 +1020,6 @@ function useContextMenuRootContext(optional = true) {
1009
1020
  return context;
1010
1021
  }
1011
1022
 
1012
- const stateAttributesMapping$1 = {
1013
- ...useScrollLock.popupStateMapping,
1014
- ...useScrollLock.transitionStatusMapping
1015
- };
1016
-
1017
1023
  /**
1018
1024
  * An overlay displayed beneath the menu popup.
1019
1025
  * Renders a `<div>` element.
@@ -1042,7 +1048,7 @@ const MenuBackdrop = /*#__PURE__*/React__namespace.forwardRef(function MenuBackd
1042
1048
  return useRenderElement.useRenderElement('div', componentProps, {
1043
1049
  ref: contextMenuContext?.backdropRef ? [forwardedRef, contextMenuContext.backdropRef] : forwardedRef,
1044
1050
  state,
1045
- stateAttributesMapping: stateAttributesMapping$1,
1051
+ stateAttributesMapping: useScrollLock.popupTransitionStateMapping,
1046
1052
  props: [{
1047
1053
  role: 'presentation',
1048
1054
  hidden: !mounted,
@@ -1124,8 +1130,12 @@ function useMenuItemCommonProps(params) {
1124
1130
  if (itemRef.current && store.context.allowMouseUpTriggerRef.current && (!isContextMenu || event.button === 2)) {
1125
1131
  // This fires whenever the user clicks on the trigger, moves the cursor, and releases it over the item.
1126
1132
  // We trigger the click and override the `closeOnClick` preference to always close the menu.
1127
- if (!itemMetadata || itemMetadata.type === 'regular-item') {
1128
- itemRef.current.click();
1133
+ if (itemMetadata.type === 'regular-item') {
1134
+ // `detail: 1` marks this as a mouse-gesture click so MenuRoot doesn't
1135
+ // treat it as a keyboard activation (`detail === 0` → `data-instant`).
1136
+ useScrollLock.dispatchClickWithModifiers(itemRef.current, event, {
1137
+ detail: 1
1138
+ });
1129
1139
  }
1130
1140
  }
1131
1141
  }
@@ -1138,7 +1148,7 @@ const REGULAR_ITEM = {
1138
1148
  function useMenuItem(params) {
1139
1149
  const {
1140
1150
  closeOnClick,
1141
- disabled: disabledProp = false,
1151
+ disabled,
1142
1152
  highlighted,
1143
1153
  id,
1144
1154
  store,
@@ -1147,8 +1157,6 @@ function useMenuItem(params) {
1147
1157
  itemMetadata,
1148
1158
  nodeId
1149
1159
  } = params;
1150
- const rootDisabled = store.useState('disabled');
1151
- const disabled = disabledProp || rootDisabled;
1152
1160
  const itemRef = React__namespace.useRef(null);
1153
1161
  const {
1154
1162
  getButtonProps,
@@ -1272,7 +1280,7 @@ const MenuGroupLabel = /*#__PURE__*/React__namespace.forwardRef(function MenuGro
1272
1280
  useScrollLock.useIsoLayoutEffect(() => {
1273
1281
  setLabelId(id);
1274
1282
  return () => {
1275
- setLabelId(undefined);
1283
+ setLabelId(currentId => currentId === id ? undefined : currentId);
1276
1284
  };
1277
1285
  }, [setLabelId, id]);
1278
1286
  return useRenderElement.useRenderElement('div', componentProps, {
@@ -1299,12 +1307,13 @@ const MenuItem = /*#__PURE__*/React__namespace.forwardRef(function MenuItem(comp
1299
1307
  id: idProp,
1300
1308
  label,
1301
1309
  nativeButton = false,
1302
- disabled = false,
1310
+ disabled: disabledProp = false,
1303
1311
  closeOnClick = true,
1304
1312
  style,
1305
1313
  ...elementProps
1306
1314
  } = componentProps;
1307
1315
  const listItem = useCompositeListItem.useCompositeListItem({
1316
+ guess: true,
1308
1317
  label
1309
1318
  });
1310
1319
  const menuPositionerContext = useMenuPositionerContext(true);
@@ -1312,6 +1321,8 @@ const MenuItem = /*#__PURE__*/React__namespace.forwardRef(function MenuItem(comp
1312
1321
  const {
1313
1322
  store
1314
1323
  } = useMenuRootContext();
1324
+ const rootDisabled = store.useState('disabled');
1325
+ const disabled = disabledProp || rootDisabled;
1315
1326
  const highlighted = store.useState('isActive', listItem.index);
1316
1327
  const itemProps = store.useState('itemProps');
1317
1328
  const {
@@ -1357,6 +1368,7 @@ const MenuLinkItem = /*#__PURE__*/React__namespace.forwardRef(function MenuLinkI
1357
1368
  } = componentProps;
1358
1369
  const linkRef = React__namespace.useRef(null);
1359
1370
  const listItem = useCompositeListItem.useCompositeListItem({
1371
+ guess: true,
1360
1372
  label
1361
1373
  });
1362
1374
  const menuPositionerContext = useMenuPositionerContext(true);
@@ -1382,7 +1394,8 @@ const MenuLinkItem = /*#__PURE__*/React__namespace.forwardRef(function MenuLinkI
1382
1394
  nodeId,
1383
1395
  store,
1384
1396
  typingRef,
1385
- itemRef: linkRef
1397
+ itemRef: linkRef,
1398
+ itemMetadata: REGULAR_ITEM
1386
1399
  });
1387
1400
  function getItemProps(externalProps) {
1388
1401
  return useRenderElement.mergeProps(commonProps, externalProps, getButtonProps);
@@ -1398,17 +1411,6 @@ const MenuLinkItem = /*#__PURE__*/React__namespace.forwardRef(function MenuLinkI
1398
1411
  });
1399
1412
  if (process.env.NODE_ENV !== "production") MenuLinkItem.displayName = "MenuLinkItem";
1400
1413
 
1401
- const stateAttributesMapping = {
1402
- ...useScrollLock.popupStateMapping,
1403
- ...useScrollLock.transitionStatusMapping
1404
- };
1405
-
1406
- /**
1407
- * A container for the menu items.
1408
- * Renders a `<div>` element.
1409
- *
1410
- * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu)
1411
- */
1412
1414
  const MenuPopup = /*#__PURE__*/React__namespace.forwardRef(function MenuPopup(componentProps, forwardedRef) {
1413
1415
  const {
1414
1416
  render,
@@ -1430,14 +1432,13 @@ const MenuPopup = /*#__PURE__*/React__namespace.forwardRef(function MenuPopup(co
1430
1432
  const popupProps = store.useState('popupProps');
1431
1433
  const mounted = store.useState('mounted');
1432
1434
  const instantType = store.useState('instantType');
1433
- const triggerElement = store.useState('activeTriggerElement');
1435
+ const activeTriggerElement = store.useState('activeTriggerElement');
1434
1436
  const parent = store.useState('parent');
1435
1437
  const lastOpenChangeReason = store.useState('lastOpenChangeReason');
1436
1438
  const rootId = store.useState('rootId');
1437
1439
  const floatingContext = store.useState('floatingRootContext');
1438
1440
  const floatingTreeRoot = store.useState('floatingTreeRoot');
1439
1441
  const closeDelay = store.useState('closeDelay');
1440
- const activeTriggerElement = store.useState('activeTriggerElement');
1441
1442
  const hoverEnabled = store.useState('hoverEnabled');
1442
1443
  const disabled = store.useState('disabled');
1443
1444
  const openMethod = store.useState('openMethod');
@@ -1464,9 +1465,7 @@ const MenuPopup = /*#__PURE__*/React__namespace.forwardRef(function MenuPopup(co
1464
1465
  enabled: hoverEnabled && !disabled && !isContextMenu && parent.type !== 'menubar',
1465
1466
  closeDelay
1466
1467
  });
1467
- const setPopupElement = React__namespace.useCallback(element => {
1468
- store.set('popupElement', element);
1469
- }, [store]);
1468
+ const setPopupElement = store.useStateSetter('popupElement');
1470
1469
  const state = {
1471
1470
  transitionStatus,
1472
1471
  side,
@@ -1478,7 +1477,7 @@ const MenuPopup = /*#__PURE__*/React__namespace.forwardRef(function MenuPopup(co
1478
1477
  const element = useRenderElement.useRenderElement('div', componentProps, {
1479
1478
  state,
1480
1479
  ref: [forwardedRef, store.context.popupRef, setPopupElement],
1481
- stateAttributesMapping,
1480
+ stateAttributesMapping: useScrollLock.popupTransitionStateMapping,
1482
1481
  props: [popupProps, {
1483
1482
  onKeyDown(event) {
1484
1483
  if (insideToolbar && scrollEdges.COMPOSITE_KEYS.has(event.key)) {
@@ -1490,7 +1489,7 @@ const MenuPopup = /*#__PURE__*/React__namespace.forwardRef(function MenuPopup(co
1490
1489
  }]
1491
1490
  });
1492
1491
  let returnFocus = parent.type === undefined || isContextMenu;
1493
- if (triggerElement || parent.type === 'menubar' && lastOpenChangeReason !== useScrollLock.outsidePress) {
1492
+ if (activeTriggerElement || parent.type === 'menubar' && lastOpenChangeReason !== useScrollLock.outsidePress) {
1494
1493
  returnFocus = true;
1495
1494
  }
1496
1495
  return /*#__PURE__*/jsxRuntime.jsx(useScrollLock.FloatingFocusManager, {
@@ -1578,13 +1577,13 @@ const MenuPositioner = /*#__PURE__*/React__namespace.forwardRef(function MenuPos
1578
1577
  const transitionStatus = store.useState('transitionStatus');
1579
1578
  const positionerElement = store.useState('positionerElement');
1580
1579
  const instantType = store.useState('instantType');
1581
- const hasViewport = store.useState('hasViewport');
1580
+ const adaptiveOrigin = store.useState('adaptiveOrigin');
1582
1581
  const lastOpenChangeReason = store.useState('lastOpenChangeReason');
1583
1582
  const floatingNodeId = store.useState('floatingNodeId');
1584
1583
  const floatingParentNodeId = store.useState('floatingParentNodeId');
1585
1584
  const domReference = floatingRootContext.useState('domReferenceElement');
1586
1585
  const previousTriggerRef = React__namespace.useRef(null);
1587
- const runOnceAnimationsFinish = useScrollLock.useAnimationsFinished(positionerElement, false, false);
1586
+ const runOnceAnimationsFinish = useScrollLock.useAnimationsFinished(positionerElement);
1588
1587
  let anchor = anchorProp;
1589
1588
  let sideOffset = sideOffsetProp;
1590
1589
  let alignOffset = alignOffsetProp;
@@ -1626,9 +1625,12 @@ const MenuPositioner = /*#__PURE__*/React__namespace.forwardRef(function MenuPos
1626
1625
  keepMounted,
1627
1626
  disableAnchorTracking,
1628
1627
  collisionAvoidance,
1629
- shiftCrossAxis: contextMenu && !('side' in collisionAvoidance && collisionAvoidance.side === 'flip'),
1628
+ shift: contextMenu ? {
1629
+ crossAxis: !('side' in collisionAvoidance && collisionAvoidance.side === 'flip'),
1630
+ rootBoundary: 'layoutViewport'
1631
+ } : undefined,
1630
1632
  externalTree: floatingTreeRoot,
1631
- adaptiveOrigin: hasViewport ? useCompositeListItem.adaptiveOrigin : undefined
1633
+ adaptiveOrigin
1632
1634
  });
1633
1635
  React__namespace.useEffect(() => {
1634
1636
  function onMenuOpenChange(details) {
@@ -1868,6 +1870,7 @@ const MenuRadioItem = /*#__PURE__*/React__namespace.forwardRef(function MenuRadi
1868
1870
  ...elementProps
1869
1871
  } = componentProps;
1870
1872
  const listItem = useCompositeListItem.useCompositeListItem({
1873
+ guess: true,
1871
1874
  label
1872
1875
  });
1873
1876
  const menuPositionerContext = useMenuPositionerContext(true);
@@ -1882,7 +1885,8 @@ const MenuRadioItem = /*#__PURE__*/React__namespace.forwardRef(function MenuRadi
1882
1885
  setValue: setSelectedValue,
1883
1886
  disabled: groupDisabled
1884
1887
  } = useMenuRadioGroupContext();
1885
- const disabled = groupDisabled || disabledProp;
1888
+ const rootDisabled = store.useState('disabled');
1889
+ const disabled = disabledProp || groupDisabled || rootDisabled;
1886
1890
  const checked = selectedValue === value;
1887
1891
  const {
1888
1892
  getItemProps,
@@ -1904,7 +1908,7 @@ const MenuRadioItem = /*#__PURE__*/React__namespace.forwardRef(function MenuRadi
1904
1908
  }), [disabled, highlighted, checked]);
1905
1909
  function handleClick(event) {
1906
1910
  const details = useScrollLock.createChangeEventDetails(useScrollLock.itemPress, event.nativeEvent, undefined, {
1907
- preventUnmountOnClose() {}
1911
+ preventUnmountOnClose: useRenderElement.NOOP
1908
1912
  });
1909
1913
  setSelectedValue(value, details);
1910
1914
  }
@@ -1943,6 +1947,7 @@ const MenuRadioItemIndicator = /*#__PURE__*/React__namespace.forwardRef(function
1943
1947
  const indicatorRef = React__namespace.useRef(null);
1944
1948
  const {
1945
1949
  transitionStatus,
1950
+ mounted,
1946
1951
  setMounted
1947
1952
  } = useScrollLock.useTransitionStatus(item.checked);
1948
1953
  useScrollLock.useOpenChangeComplete({
@@ -1968,7 +1973,7 @@ const MenuRadioItemIndicator = /*#__PURE__*/React__namespace.forwardRef(function
1968
1973
  'aria-hidden': true,
1969
1974
  ...elementProps
1970
1975
  },
1971
- enabled: keepMounted || item.checked
1976
+ enabled: keepMounted || mounted
1972
1977
  });
1973
1978
  return element;
1974
1979
  });
@@ -1986,36 +1991,35 @@ function useMenubarContext(optional) {
1986
1991
 
1987
1992
  const selectors = {
1988
1993
  ...ScrollAreaViewport.popupStoreSelectors,
1989
- disabled: useScrollLock.createSelector(state => state.parent.type === 'menubar' ? state.parent.context.disabled || state.disabled : state.disabled),
1990
- modal: useScrollLock.createSelector(state => (state.parent.type === undefined || state.parent.type === 'context-menu') && (state.modal ?? true)),
1991
- openMethod: useScrollLock.createSelector(state => state.openMethod),
1992
- allowMouseEnter: useScrollLock.createSelector(state => state.allowMouseEnter),
1993
- highlightItemOnHover: useScrollLock.createSelector(state => state.highlightItemOnHover),
1994
- stickIfOpen: useScrollLock.createSelector(state => state.stickIfOpen),
1995
- parent: useScrollLock.createSelector(state => state.parent),
1996
- rootId: useScrollLock.createSelector(state => {
1994
+ disabled: state => state.parent.type === 'menubar' ? state.parent.context.disabled || state.disabled : state.disabled,
1995
+ modal: state => (state.parent.type === undefined || state.parent.type === 'context-menu') && (state.modal ?? true),
1996
+ openMethod: state => state.openMethod,
1997
+ allowMouseEnter: state => state.allowMouseEnter,
1998
+ highlightItemOnHover: state => state.highlightItemOnHover,
1999
+ parent: state => state.parent,
2000
+ rootId: state => {
1997
2001
  if (state.parent.type === 'menu') {
1998
2002
  return state.parent.store.select('rootId');
1999
2003
  }
2000
2004
  return state.parent.type !== undefined ? state.parent.context.rootId : state.rootId;
2001
- }),
2002
- activeIndex: useScrollLock.createSelector(state => state.activeIndex),
2003
- isActive: useScrollLock.createSelector((state, itemIndex) => state.activeIndex === itemIndex),
2004
- hoverEnabled: useScrollLock.createSelector(state => state.hoverEnabled),
2005
- instantType: useScrollLock.createSelector(state => state.instantType),
2006
- lastOpenChangeReason: useScrollLock.createSelector(state => state.openChangeReason),
2007
- floatingTreeRoot: useScrollLock.createSelector(state => {
2005
+ },
2006
+ activeIndex: state => state.activeIndex,
2007
+ isActive: (state, itemIndex) => state.activeIndex === itemIndex,
2008
+ hoverEnabled: state => state.hoverEnabled,
2009
+ instantType: state => state.instantType,
2010
+ lastOpenChangeReason: state => state.openChangeReason,
2011
+ floatingTreeRoot: state => {
2008
2012
  if (state.parent.type === 'menu') {
2009
2013
  return state.parent.store.select('floatingTreeRoot');
2010
2014
  }
2011
2015
  return state.floatingTreeRoot;
2012
- }),
2013
- floatingNodeId: useScrollLock.createSelector(state => state.floatingNodeId),
2014
- floatingParentNodeId: useScrollLock.createSelector(state => state.floatingParentNodeId),
2015
- itemProps: useScrollLock.createSelector(state => state.itemProps),
2016
- closeDelay: useScrollLock.createSelector(state => state.closeDelay),
2017
- hasViewport: useScrollLock.createSelector(state => state.hasViewport),
2018
- keyboardEventRelay: useScrollLock.createSelector(state => {
2016
+ },
2017
+ floatingNodeId: state => state.floatingNodeId,
2018
+ floatingParentNodeId: state => state.floatingParentNodeId,
2019
+ itemProps: state => state.itemProps,
2020
+ closeDelay: state => state.closeDelay,
2021
+ adaptiveOrigin: state => state.adaptiveOrigin,
2022
+ keyboardEventRelay: state => {
2019
2023
  if (state.keyboardEventRelay) {
2020
2024
  return state.keyboardEventRelay;
2021
2025
  }
@@ -2023,33 +2027,23 @@ const selectors = {
2023
2027
  return state.parent.store.select('keyboardEventRelay');
2024
2028
  }
2025
2029
  return undefined;
2026
- })
2030
+ }
2027
2031
  };
2032
+
2033
+ /**
2034
+ * The store view that detached handle-backed triggers read from. Both the real `MenuStore` and the
2035
+ * inert fallback store satisfy it, so a trigger can read from whichever store the handle currently
2036
+ * exposes. Narrowed to the members a trigger actually uses — the trigger-data members plus `setOpen`
2037
+ * (called by the focus guards) — so the exposed surface can't bypass the open-change pipeline; on
2038
+ * the detached fallback store every one of these mutations is a no-op.
2039
+ */
2040
+
2028
2041
  class MenuStore extends useScrollLock.ReactStore {
2029
2042
  constructor(initialState) {
2030
2043
  super({
2031
2044
  ...createInitialState(),
2032
2045
  ...initialState
2033
- }, {
2034
- positionerRef: /*#__PURE__*/React__namespace.createRef(),
2035
- popupRef: /*#__PURE__*/React__namespace.createRef(),
2036
- typingRef: {
2037
- current: false
2038
- },
2039
- itemDomElements: {
2040
- current: []
2041
- },
2042
- itemLabels: {
2043
- current: []
2044
- },
2045
- allowMouseUpTriggerRef: {
2046
- current: false
2047
- },
2048
- triggerFocusTargetRef: /*#__PURE__*/React__namespace.createRef(),
2049
- beforeContentFocusGuardRef: /*#__PURE__*/React__namespace.createRef(),
2050
- onOpenChangeComplete: undefined,
2051
- triggerElements: new useScrollLock.PopupTriggerMap()
2052
- }, selectors);
2046
+ }, createInitialContext(), selectors);
2053
2047
 
2054
2048
  // Set up propagation of state from parent menu if applicable.
2055
2049
  this.unsubscribeParentListener = this.observe('parent', parent => {
@@ -2085,15 +2079,44 @@ class MenuStore extends useScrollLock.ReactStore {
2085
2079
  eventDetails
2086
2080
  });
2087
2081
  }
2088
- static useStore(externalStore, initialState) {
2089
- // eslint-disable-next-line react-hooks/rules-of-hooks
2090
- const internalStore = useRenderElement.useRefWithInit(() => {
2091
- return new MenuStore(initialState);
2092
- }).current;
2093
- return externalStore ?? internalStore;
2094
- }
2095
2082
  unsubscribeParentListener = null;
2096
2083
  }
2084
+
2085
+ /**
2086
+ * Creates the inert fallback store used by detached handle-backed triggers while no `Menu.Root` is
2087
+ * attached. It preserves a menu-specific trigger registry in context so detached triggers can
2088
+ * register before migrating to the live root store. `setOpen` is a no-op (matching the inert
2089
+ * reads/writes of `NullStore`), so a trigger can hand the store to focus-guard helpers that expect
2090
+ * `setOpen` without it ever taking effect while detached.
2091
+ */
2092
+ function createNullMenuStore() {
2093
+ const store = new ScrollAreaViewport.NullStore(Object.freeze(createInitialState()), Object.freeze(createInitialContext()), selectors);
2094
+ return Object.assign(store, {
2095
+ setOpen: useRenderElement.NOOP
2096
+ });
2097
+ }
2098
+ function createInitialContext() {
2099
+ return {
2100
+ positionerRef: /*#__PURE__*/React__namespace.createRef(),
2101
+ popupRef: /*#__PURE__*/React__namespace.createRef(),
2102
+ typingRef: {
2103
+ current: false
2104
+ },
2105
+ itemDomElements: {
2106
+ current: []
2107
+ },
2108
+ itemLabels: {
2109
+ current: []
2110
+ },
2111
+ allowMouseUpTriggerRef: {
2112
+ current: false
2113
+ },
2114
+ triggerFocusTargetRef: /*#__PURE__*/React__namespace.createRef(),
2115
+ beforeContentFocusGuardRef: /*#__PURE__*/React__namespace.createRef(),
2116
+ onOpenChangeComplete: undefined,
2117
+ triggerElements: new useScrollLock.PopupTriggerMap()
2118
+ };
2119
+ }
2097
2120
  function createInitialState() {
2098
2121
  return {
2099
2122
  ...ScrollAreaViewport.createInitialPopupStoreState(),
@@ -2102,7 +2125,6 @@ function createInitialState() {
2102
2125
  openMethod: null,
2103
2126
  allowMouseEnter: false,
2104
2127
  highlightItemOnHover: true,
2105
- stickIfOpen: true,
2106
2128
  parent: {
2107
2129
  type: undefined
2108
2130
  },
@@ -2117,7 +2139,7 @@ function createInitialState() {
2117
2139
  itemProps: useRenderElement.EMPTY_OBJECT,
2118
2140
  keyboardEventRelay: undefined,
2119
2141
  closeDelay: 0,
2120
- hasViewport: false
2142
+ adaptiveOrigin: undefined
2121
2143
  };
2122
2144
  }
2123
2145
 
@@ -2176,14 +2198,13 @@ const MenuRoot = useScrollLock.fastComponent(function MenuRoot(props) {
2176
2198
  type: undefined
2177
2199
  };
2178
2200
  }, [contextMenuContext, parentMenuRootContext, menubarContext, isSubmenu]);
2179
- const store = MenuStore.useStore(handle?.store, {
2201
+ const store = useMenuRootStore({
2180
2202
  open: defaultOpen,
2181
2203
  openProp,
2182
2204
  activeTriggerId: defaultTriggerIdProp,
2183
2205
  triggerIdProp,
2184
2206
  parent: parentFromContext
2185
2207
  });
2186
- useScrollLock.useInitialOpenSync(store, openProp, defaultOpen, defaultTriggerIdProp);
2187
2208
  store.useControlledProp('openProp', openProp);
2188
2209
  store.useControlledProp('triggerIdProp', triggerIdProp);
2189
2210
  store.useContextCallback('onOpenChangeComplete', onOpenChangeComplete);
@@ -2228,10 +2249,7 @@ const MenuRoot = useScrollLock.fastComponent(function MenuRoot(props) {
2228
2249
  const {
2229
2250
  forceUnmount
2230
2251
  } = useScrollLock.useOpenStateTransitions(open, store, () => {
2231
- store.update({
2232
- allowMouseEnter: false,
2233
- stickIfOpen: true
2234
- });
2252
+ store.set('allowMouseEnter', false);
2235
2253
  });
2236
2254
  useScrollLock.useIsoLayoutEffect(() => {
2237
2255
  if (contextMenuContext && !parentMenuRootContext) {
@@ -2279,6 +2297,12 @@ const MenuRoot = useScrollLock.fastComponent(function MenuRoot(props) {
2279
2297
  }, [open, hoverEnabled, store]);
2280
2298
  const setOpen = useScrollLock.useStableCallback((nextOpen, eventDetails) => {
2281
2299
  const reason = eventDetails.reason;
2300
+
2301
+ // Read the store directly, as relayed tree events and stale hover timers can request
2302
+ // a close after the state changed but before this component re-rendered.
2303
+ if (!nextOpen && !store.select('open')) {
2304
+ return;
2305
+ }
2282
2306
  if (open === nextOpen && eventDetails.trigger === activeTriggerElement && lastOpenChangeReason === reason) {
2283
2307
  return;
2284
2308
  }
@@ -2311,13 +2335,17 @@ const MenuRoot = useScrollLock.fastComponent(function MenuRoot(props) {
2311
2335
  allowTouchToCloseRef.current = true;
2312
2336
  allowTouchToCloseTimeout.clear();
2313
2337
  }
2314
- const isKeyboardClick = (reason === useScrollLock.triggerPress || reason === useScrollLock.itemPress) && nativeEvent.detail === 0 && nativeEvent?.isTrusted;
2338
+
2339
+ // Keyboard and assistive-technology activations produce `detail === 0` clicks;
2340
+ // mouse-gesture clicks (including the synthesized drag-release click from
2341
+ // `useMenuItemCommonProps`) carry `detail >= 1`.
2342
+ const isKeyboardClick = (reason === useScrollLock.triggerPress || reason === useScrollLock.itemPress) && nativeEvent.detail === 0;
2315
2343
  const isDismissClose = !nextOpen && (reason === useScrollLock.escapeKey || reason == null);
2316
2344
  const updatedState = {
2317
2345
  open: nextOpen,
2318
2346
  openChangeReason: reason
2319
2347
  };
2320
- openEventRef.current = eventDetails.event ?? null;
2348
+ openEventRef.current = eventDetails.event;
2321
2349
  useScrollLock.setPopupOpenState(updatedState, nextOpen, eventDetails.trigger, shouldPreventUnmountOnClose());
2322
2350
  store.update(updatedState);
2323
2351
  if (parent.type === 'menubar' && (reason === useScrollLock.triggerFocus || reason === useScrollLock.focusOut || reason === useScrollLock.triggerHover || reason === useScrollLock.listNavigation || reason === useScrollLock.siblingOpen)) {
@@ -2335,7 +2363,11 @@ const MenuRoot = useScrollLock.fastComponent(function MenuRoot(props) {
2335
2363
  onOpenChange: setOpen
2336
2364
  });
2337
2365
  const floatingEvents = floatingRootContext.context.events;
2338
- React__namespace.useEffect(() => {
2366
+
2367
+ // Registered in a layout effect (not a passive one) so `setOpen` emits from imperative
2368
+ // `MenuHandle.open()` calls made in the same commit this root mounts — e.g. from another layout
2369
+ // effect during a route-transition handoff — are received instead of being silently dropped.
2370
+ useScrollLock.useIsoLayoutEffect(() => {
2339
2371
  const handleSetOpenEvent = ({
2340
2372
  open: nextOpen,
2341
2373
  eventDetails
@@ -2464,11 +2496,14 @@ const MenuRoot = useScrollLock.fastComponent(function MenuRoot(props) {
2464
2496
  store,
2465
2497
  parent: parentFromContext
2466
2498
  }), [store, parentFromContext]);
2467
- const content = /*#__PURE__*/jsxRuntime.jsx(MenuRootContext.Provider, {
2499
+ const content = /*#__PURE__*/jsxRuntime.jsxs(MenuRootContext.Provider, {
2468
2500
  value: context,
2469
- children: typeof children === 'function' ? children({
2501
+ children: [handle && /*#__PURE__*/jsxRuntime.jsx(useScrollLock.PopupHandleAttachment, {
2502
+ handle: handle,
2503
+ store: store
2504
+ }), typeof children === 'function' ? children({
2470
2505
  payload
2471
- }) : children
2506
+ }) : children]
2472
2507
  });
2473
2508
  if (parent.type === undefined || parent.type === 'context-menu') {
2474
2509
  // set up a FloatingTree to provide the context to nested menus
@@ -2480,6 +2515,15 @@ const MenuRoot = useScrollLock.fastComponent(function MenuRoot(props) {
2480
2515
  return content;
2481
2516
  });
2482
2517
  if (process.env.NODE_ENV !== "production") MenuRoot.displayName = "MenuRoot";
2518
+ function useMenuRootStore(initialState) {
2519
+ // The store is owned by this Root instance and created exactly once. It is not tied to the handle:
2520
+ // the handle attaches to it, so swapping the handle re-attaches rather than recreating state.
2521
+ // Default values are only initial values; controlled values and root state are synced after creation.
2522
+ // Unlike other popups, Menu wires its floating root context separately (it relays open changes
2523
+ // through an event).
2524
+ const store = useRenderElement.useRefWithInit(() => new MenuStore(initialState)).current;
2525
+ return store;
2526
+ }
2483
2527
 
2484
2528
  /**
2485
2529
  * Groups all parts of a submenu.
@@ -2536,9 +2580,6 @@ function useCompositeItem(params = {}) {
2536
2580
  };
2537
2581
  }
2538
2582
 
2539
- /**
2540
- * @internal
2541
- */
2542
2583
  function CompositeItem(componentProps) {
2543
2584
  const {
2544
2585
  render,
@@ -2560,20 +2601,21 @@ function CompositeItem(componentProps) {
2560
2601
  });
2561
2602
  return useRenderElement.useRenderElement(tag, componentProps, {
2562
2603
  state,
2563
- ref: [...refs, compositeRef],
2604
+ // The composite ref attaches first so an outer item wins when nested items share a DOM node.
2605
+ ref: [compositeRef, ...refs],
2564
2606
  props: [compositeProps, ...props, elementProps],
2565
2607
  stateAttributesMapping
2566
2608
  });
2567
2609
  }
2568
2610
 
2569
2611
  function findRootOwnerId(node) {
2570
- if (floatingUi_utils.isHTMLElement(node) && node.hasAttribute('data-rootownerid')) {
2571
- return node.getAttribute('data-rootownerid') ?? undefined;
2612
+ if (useScrollLock.isHTMLElement(node) && node.hasAttribute('data-rootownerid')) {
2613
+ return node.getAttribute('data-rootownerid');
2572
2614
  }
2573
- if (floatingUi_utils.isLastTraversableNode(node)) {
2615
+ if (useScrollLock.isLastTraversableNode(node)) {
2574
2616
  return undefined;
2575
2617
  }
2576
- return findRootOwnerId(floatingUi_utils.getParentNode(node));
2618
+ return findRootOwnerId(useScrollLock.getParentNode(node));
2577
2619
  }
2578
2620
 
2579
2621
  /**
@@ -2660,14 +2702,6 @@ function useMixedToggleClickHandler(params) {
2660
2702
  }, [enabled, mouseDownAction, open]);
2661
2703
  }
2662
2704
 
2663
- const BOUNDARY_OFFSET = 2;
2664
-
2665
- /**
2666
- * A button that opens the menu.
2667
- * Renders a `<button>` element.
2668
- *
2669
- * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu)
2670
- */
2671
2705
  const MenuTrigger = useScrollLock.fastComponentRef(function MenuTrigger(componentProps, forwardedRef) {
2672
2706
  const {
2673
2707
  render,
@@ -2684,7 +2718,8 @@ const MenuTrigger = useScrollLock.fastComponentRef(function MenuTrigger(componen
2684
2718
  ...elementProps
2685
2719
  } = componentProps;
2686
2720
  const rootContext = useMenuRootContext(true);
2687
- const store = handle?.store ?? rootContext?.store;
2721
+ const handleStore = ScrollAreaViewport.usePopupHandleStore(handle);
2722
+ const store = handleStore ?? rootContext?.store;
2688
2723
  if (!store) {
2689
2724
  throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: <Menu.Trigger> must be either used within a <Menu.Root> component or provided with a handle.' : useRenderElement.formatErrorMessage(85));
2690
2725
  }
@@ -2744,8 +2779,7 @@ const MenuTrigger = useScrollLock.fastComponentRef(function MenuTrigger(componen
2744
2779
  if (mouseUpTarget != null && findRootOwnerId(mouseUpTarget) === store.select('rootId')) {
2745
2780
  return;
2746
2781
  }
2747
- const bounds = useCompositeListItem.getPseudoElementBounds(triggerRef.current);
2748
- if (mouseEvent.clientX >= bounds.left - BOUNDARY_OFFSET && mouseEvent.clientX <= bounds.right + BOUNDARY_OFFSET && mouseEvent.clientY >= bounds.top - BOUNDARY_OFFSET && mouseEvent.clientY <= bounds.bottom + BOUNDARY_OFFSET) {
2782
+ if (useCompositeListItem.isMouseWithinBounds(mouseEvent, triggerRef.current)) {
2749
2783
  return;
2750
2784
  }
2751
2785
  floatingTreeRoot.events.emit('close', {
@@ -2764,7 +2798,7 @@ const MenuTrigger = useScrollLock.fastComponentRef(function MenuTrigger(componen
2764
2798
  const parentMenubarHasSubmenuOpen = isInMenubar && parent.context.hasSubmenuOpen;
2765
2799
  const openOnHover = openOnHoverProp ?? parentMenubarHasSubmenuOpen;
2766
2800
  const hoverProps = useHoverReferenceInteraction(floatingRootContext, {
2767
- enabled: openOnHover && !disabled && parent.type !== 'context-menu' && (!isInMenubar || parentMenubarHasSubmenuOpen && !isMountedByThisTrigger),
2801
+ enabled: openOnHover && !disabled && (!isInMenubar || parentMenubarHasSubmenuOpen && !isMountedByThisTrigger),
2768
2802
  handleClose: safePolygon({
2769
2803
  blockPointerEvents: !isInMenubar
2770
2804
  }),
@@ -2785,7 +2819,7 @@ const MenuTrigger = useScrollLock.fastComponentRef(function MenuTrigger(componen
2785
2819
  // only when `isOpenedByThisTrigger` changes.
2786
2820
  const stickIfOpen = useStickIfOpen(isOpenedByThisTrigger, store.select('lastOpenChangeReason'));
2787
2821
  const click = useScrollLock.useClick(floatingRootContext, {
2788
- enabled: !disabled && parent.type !== 'context-menu',
2822
+ enabled: !disabled,
2789
2823
  event: isOpenedByThisTrigger && isInMenubar ? 'click' : 'mousedown',
2790
2824
  toggle: true,
2791
2825
  ignoreMouse: false,
@@ -2880,7 +2914,7 @@ function useStickIfOpen(open, openReason) {
2880
2914
  const stickIfOpenTimeout = useScrollLock.useTimeout();
2881
2915
  const [stickIfOpen, setStickIfOpen] = React__namespace.useState(false);
2882
2916
  useScrollLock.useIsoLayoutEffect(() => {
2883
- if (open && openReason === 'trigger-hover') {
2917
+ if (open && openReason === useScrollLock.triggerHover) {
2884
2918
  // Only allow "patient" clicks to close the menu if it's open.
2885
2919
  // If they clicked within 500ms of the menu opening, keep it open.
2886
2920
  setStickIfOpen(true);
@@ -2895,8 +2929,6 @@ function useStickIfOpen(open, openReason) {
2895
2929
  return stickIfOpen;
2896
2930
  }
2897
2931
  function useMenuParent() {
2898
- const contextMenuContext = useContextMenuRootContext(true);
2899
- const parentContext = useMenuRootContext(true);
2900
2932
  const menubarContext = useMenubarContext(true);
2901
2933
  const parent = React__namespace.useMemo(() => {
2902
2934
  if (menubarContext) {
@@ -2905,23 +2937,17 @@ function useMenuParent() {
2905
2937
  context: menubarContext
2906
2938
  };
2907
2939
  }
2908
-
2909
- // Ensure this is not a Menu nested inside ContextMenu.Trigger.
2910
- // ContextMenu parentContext is always undefined as ContextMenu.Root is instantiated with
2911
- // <MenuRootContext.Provider value={undefined}>
2912
- if (contextMenuContext && !parentContext) {
2913
- return {
2914
- type: 'context-menu',
2915
- context: contextMenuContext
2916
- };
2917
- }
2918
2940
  return {
2919
2941
  type: undefined
2920
2942
  };
2921
- }, [contextMenuContext, parentContext, menubarContext]);
2943
+ }, [menubarContext]);
2922
2944
  return parent;
2923
2945
  }
2924
2946
 
2947
+ const VOICE_OVER_EXPANDED_PROPS = {
2948
+ 'aria-expanded': undefined
2949
+ };
2950
+
2925
2951
  /**
2926
2952
  * A menu item that opens a submenu.
2927
2953
  * Renders a `<div>` element.
@@ -2942,7 +2968,12 @@ const MenuSubmenuTrigger = /*#__PURE__*/React__namespace.forwardRef(function Men
2942
2968
  disabled: disabledProp = false,
2943
2969
  ...elementProps
2944
2970
  } = componentProps;
2971
+ const submenuRootContext = useMenuSubmenuRootContext();
2972
+ if (!submenuRootContext?.parentMenu) {
2973
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: <Menu.SubmenuTrigger> must be placed in <Menu.SubmenuRoot>.' : useRenderElement.formatErrorMessage(37));
2974
+ }
2945
2975
  const listItem = useCompositeListItem.useCompositeListItem({
2976
+ guess: true,
2946
2977
  label
2947
2978
  });
2948
2979
  const menuPositionerContext = useMenuPositionerContext();
@@ -2971,10 +3002,6 @@ const MenuSubmenuTrigger = /*#__PURE__*/React__namespace.forwardRef(function Men
2971
3002
  triggerElementRef.current = el;
2972
3003
  store.set('activeTriggerElement', el);
2973
3004
  }, [store]);
2974
- const submenuRootContext = useMenuSubmenuRootContext();
2975
- if (!submenuRootContext?.parentMenu) {
2976
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: <Menu.SubmenuTrigger> must be placed in <Menu.SubmenuRoot>.' : useRenderElement.formatErrorMessage(37));
2977
- }
2978
3005
  store.useSyncedValue('closeDelay', closeDelay);
2979
3006
  const parentMenuStore = submenuRootContext.parentMenu;
2980
3007
  const rootDisabled = store.useState('disabled');
@@ -2982,7 +3009,7 @@ const MenuSubmenuTrigger = /*#__PURE__*/React__namespace.forwardRef(function Men
2982
3009
  const disabled = disabledProp || rootDisabled || parentDisabled;
2983
3010
  if (process.env.NODE_ENV !== 'production') {
2984
3011
  // eslint-disable-next-line react-hooks/rules-of-hooks
2985
- useScrollLock.useIsoLayoutEffect(() => {
3012
+ React__namespace.useEffect(() => {
2986
3013
  const element = triggerElementRef.current;
2987
3014
  if (element && SelectGroupLabel.isElementDisabled(element) && !disabled) {
2988
3015
  const ownerStackMessage = useScrollLock.SafeReact.captureOwnerStack?.() || '';
@@ -3030,7 +3057,10 @@ const MenuSubmenuTrigger = /*#__PURE__*/React__namespace.forwardRef(function Men
3030
3057
  shouldOpen: delay > 0 ? () => parentMenuStore.select('allowMouseEnter') : undefined,
3031
3058
  triggerElementRef,
3032
3059
  externalTree: floatingTreeRoot,
3033
- isClosing: () => store.select('transitionStatus') === 'ending'
3060
+ isClosing: () => store.select('transitionStatus') === 'ending',
3061
+ // Chrome can drop the trigger's `mouseleave` during a fast pointer sweep,
3062
+ // leaving a stale submenu open (see #5152) — cancel from `mouseout` too.
3063
+ guardStaleOpen: true
3034
3064
  });
3035
3065
  const click = useScrollLock.useClick(floatingRootContext, {
3036
3066
  enabled: !disabled,
@@ -3047,10 +3077,22 @@ const MenuSubmenuTrigger = /*#__PURE__*/React__namespace.forwardRef(function Men
3047
3077
  highlighted,
3048
3078
  open
3049
3079
  };
3080
+ const openMethod = store.useState('openMethod');
3081
+ const lastOpenChangeReason = store.useState('lastOpenChangeReason');
3082
+ // Arrow keys open the submenu through list navigation without dispatching a click, so
3083
+ // `openMethod` stays null there; Enter and Space do dispatch one and report `keyboard`.
3084
+ const openedByKeyboard = lastOpenChangeReason === useScrollLock.listNavigation || openMethod === 'keyboard';
3085
+ const shouldOmitExpanded = open && openedByKeyboard && useScrollLock.voiceOver;
3050
3086
  const element = useRenderElement.useRenderElement('div', componentProps, {
3051
3087
  state,
3052
3088
  stateAttributesMapping: useScrollLock.triggerOpenStateMapping,
3053
- props: [localInteractionProps, hoverProps, rootTriggerProps, itemProps, {
3089
+ props: [localInteractionProps, hoverProps, rootTriggerProps, itemProps,
3090
+ // Opening a submenu changes the trigger's expanded state while the trigger still holds
3091
+ // focus, and VoiceOver announces that state change instead of the submenu item that focus
3092
+ // moves to a moment later, so the first item is never announced. Dropping the state while
3093
+ // the submenu is open avoids the announcement without claiming the submenu is collapsed;
3094
+ // `aria-haspopup` still conveys that the item opens a submenu.
3095
+ shouldOmitExpanded ? VOICE_OVER_EXPANDED_PROPS : undefined, {
3054
3096
  'aria-controls': popupId,
3055
3097
  tabIndex: open || highlighted ? 0 : -1,
3056
3098
  onBlur() {
@@ -3077,11 +3119,11 @@ exports.MenuRadioGroup = MenuRadioGroup;
3077
3119
  exports.MenuRadioItem = MenuRadioItem;
3078
3120
  exports.MenuRadioItemIndicator = MenuRadioItemIndicator;
3079
3121
  exports.MenuRoot = MenuRoot;
3080
- exports.MenuStore = MenuStore;
3081
3122
  exports.MenuSubmenuRoot = MenuSubmenuRoot;
3082
3123
  exports.MenuSubmenuTrigger = MenuSubmenuTrigger;
3083
3124
  exports.MenuTrigger = MenuTrigger;
3084
3125
  exports.REGULAR_ITEM = REGULAR_ITEM;
3126
+ exports.createNullMenuStore = createNullMenuStore;
3085
3127
  exports.itemMapping = itemMapping;
3086
3128
  exports.useMenuItem = useMenuItem;
3087
3129
  exports.useMenuPositionerContext = useMenuPositionerContext;