@jobber/components 8.14.1 → 8.16.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 (46) hide show
  1. package/dist/Autocomplete-es.js +1 -1
  2. package/dist/BottomSheet-cjs.js +109 -77
  3. package/dist/BottomSheet-es.js +103 -75
  4. package/dist/Card/index.cjs +2 -3
  5. package/dist/Card/index.mjs +2 -3
  6. package/dist/DataDump/index.cjs +2 -3
  7. package/dist/DataDump/index.mjs +2 -3
  8. package/dist/InputNumberExperimental-cjs.js +16 -17
  9. package/dist/InputNumberExperimental-es.js +2 -3
  10. package/dist/Menu/index.cjs +2 -3
  11. package/dist/Menu/index.mjs +2 -3
  12. package/dist/Menu-cjs.js +11 -56
  13. package/dist/Menu-es.js +4 -49
  14. package/dist/MenuSubmenuTrigger-cjs.js +1608 -1793
  15. package/dist/MenuSubmenuTrigger-es.js +1340 -1522
  16. package/dist/Modal/index.mjs +1 -1
  17. package/dist/NumberFieldInput-cjs.js +53 -54
  18. package/dist/NumberFieldInput-es.js +2 -3
  19. package/dist/Page/index.cjs +2 -3
  20. package/dist/Page/index.mjs +2 -3
  21. package/dist/{DrawerRoot-cjs.js → ScrollAreaViewport-cjs.js} +5488 -4449
  22. package/dist/{DrawerRoot-es.js → ScrollAreaViewport-es.js} +5423 -4393
  23. package/dist/{useValueChanged-cjs.js → clamp-cjs.js} +573 -321
  24. package/dist/{useValueChanged-es.js → clamp-es.js} +569 -322
  25. package/dist/floating-ui.react-es.js +1 -1
  26. package/dist/index.cjs +2 -3
  27. package/dist/index.mjs +2 -3
  28. package/dist/primitives/BottomSheet/BottomSheet.d.ts +15 -16
  29. package/dist/primitives/BottomSheet/BottomSheet.types.d.ts +27 -0
  30. package/dist/primitives/BottomSheet/BottomSheet.utils.d.ts +6 -0
  31. package/dist/primitives/BottomSheet/index.cjs +8 -3
  32. package/dist/primitives/BottomSheet/index.d.ts +1 -0
  33. package/dist/primitives/BottomSheet/index.mjs +8 -3
  34. package/dist/primitives/InputNumberExperimental/index.cjs +2 -3
  35. package/dist/primitives/InputNumberExperimental/index.mjs +2 -3
  36. package/dist/primitives/index.cjs +8 -9
  37. package/dist/primitives/index.mjs +8 -9
  38. package/dist/styles.css +62 -1
  39. package/dist/unstyledPrimitives/index.cjs +6373 -5868
  40. package/dist/unstyledPrimitives/index.d.ts +2 -0
  41. package/dist/unstyledPrimitives/index.mjs +6325 -5822
  42. package/dist/useRenderElement-cjs.js +30 -30
  43. package/dist/useRenderElement-es.js +31 -31
  44. package/package.json +2 -2
  45. package/dist/useBaseUiId-cjs.js +0 -275
  46. package/dist/useBaseUiId-es.js +0 -251
@@ -1,65 +1,13 @@
1
1
  import * as React from 'react';
2
- import { u as useDialogRootContext, i as isTypeableElement, e as useFloatingParentNodeId, F as FloatingRootStore, P as PopupTriggerMap, f as useFloatingTree, g as getTarget, m as matchesFocusVisible, h as isTargetInsideEnabledTrigger, j as createAttribute, k as activeElement, l as contains, T as TYPEABLE_SELECTOR, n as getNodeChildren, o as getFloatingFocusElement, p as isTypeableCombobox, q as isIndexOutOfListBounds, r as getMinListIndex, s as getMaxListIndex, t as createGridCellMap, v as isListIndexDisabled, w as getGridNavigatedIndex, x as getGridCellIndices, y as getGridCellIndexOfCorner, A as ARROW_DOWN, z as ARROW_LEFT, B as ARROW_RIGHT, C as findNonDisabledListIndex, E as enqueueFocus, G as ARROW_UP, H as isElementVisible, I as useTriggerDataForwarding, J as useInteractions, K as CLICK_TRIGGER_IDENTIFIER, L as triggerOpenStateMapping, M as DISABLED_TRANSITIONS_STYLE, N as popupStateMapping, O as COMPOSITE_KEYS, Q as FloatingFocusManager, R as FloatingPortal, S as POPUP_COLLISION_AVOIDANCE, U as InternalBackdrop, V as inertValue, W as FloatingNode, X as DROPDOWN_COLLISION_AVOIDANCE, Y as popupStoreSelectors, Z as createSelector, _ as ReactStore, $ as createInitialPopupStoreState, a0 as FloatingTreeStore, a1 as fastComponent, a2 as useOnFirstRender, a3 as useFloatingNodeId, a4 as useOpenInteractionType, a5 as useImplicitActiveTrigger, a6 as useOpenStateTransitions, a7 as useScrollLock, a8 as useSyncedFloatingRootContext, a9 as useDismiss, aa as useRole, ab as TYPEAHEAD_RESET_MS, ac as FloatingTree, ad as fastComponentRef, ae as getTabbableBeforeElement, af as isOutsideEvent, ag as getTabbableAfterElement, ah as getNextTabbable, ai as pressableTriggerOpenStateMapping, aj as FocusGuard, ak as PATIENT_CLICK_THRESHOLD, al as useTriggerRegistration } from './DrawerRoot-es.js';
3
- import { u as useRenderElement, E as EMPTY_OBJECT, b as useRefWithInit, f as formatErrorMessage, m as mergeProps, c as useMergedRefs, d as EMPTY_ARRAY } from './useRenderElement-es.js';
4
- import { k as createChangeEventDetails, B as closePress, C as isMouseLikePointerType, D as useAnimationFrame, c as useTimeout, E as isClickLikeEvent, F as triggerPress, b as useId, u as useIsoLayoutEffect, G as triggerFocus, H as isMac, I as isSafari, s as ownerDocument, J as escapeKey, f as useOnMount, T as Timeout, a as useStableCallback, K as triggerHover, i as useValueAsRef, z as stopEvent, L as listNavigation, M as focusOut, N as isVirtualClick, O as isVirtualPointerEvent, P as itemPress, t as transitionStatusMapping, e as useOpenChangeComplete, Q as outsidePress, R as useAnimationsFinished, U as siblingOpen, h as useControlled, d as useTransitionStatus, V as imperativeAction, W as cancelOpen } from './useValueChanged-es.js';
5
- import { a as useButton, u as useBaseUiId, b as useCompositeRootContext } from './useBaseUiId-es.js';
2
+ import { u as useFloatingParentNodeId, F as FloatingRootStore, P as PopupTriggerMap, g as useFloatingTree, h as getTarget, i as isTypeableElement, m as matchesFocusVisible, j as isTargetInsideEnabledTrigger, k as createAttribute, l as activeElement, n as contains, T as TYPEABLE_SELECTOR, o as getNodeChildren, p as getFloatingFocusElement, q as isTypeableCombobox, r as isIndexOutOfListBounds, s as getMinListIndex, t as getMaxListIndex, v as createGridCellMap, w as isListIndexDisabled, x as getGridNavigatedIndex, y as getGridCellIndices, z as getGridCellIndexOfCorner, A as ARROW_DOWN, B as ARROW_LEFT, C as ARROW_RIGHT, E as findNonDisabledListIndex, G as enqueueFocus, H as ARROW_UP, I as isElementVisible, J as useDirection, K as DISABLED_TRANSITIONS_STYLE, L as useDialogRootContext, M as popupStateMapping, N as COMPOSITE_KEYS, O as FloatingFocusManager, Q as FloatingPortal, R as POPUP_COLLISION_AVOIDANCE, U as InternalBackdrop, V as inertValue, W as FloatingNode, X as DROPDOWN_COLLISION_AVOIDANCE, Y as popupStoreSelectors, Z as createSelector, _ as ReactStore, $ as createInitialPopupStoreState, a0 as FloatingTreeStore, a1 as fastComponent, a2 as useOnFirstRender, a3 as useFloatingNodeId, a4 as useOpenInteractionType, a5 as useImplicitActiveTrigger, a6 as useOpenStateTransitions, a7 as useScrollLock, a8 as useSyncedFloatingRootContext, a9 as useDismiss, aa as useRole, ab as TYPEAHEAD_RESET_MS, ac as useInteractions, ad as FloatingTree, ae as fastComponentRef, af as useTriggerDataForwarding, ag as useClick, ah as getTabbableBeforeElement, ai as isOutsideEvent, aj as getTabbableAfterElement, ak as getNextTabbable, al as pressableTriggerOpenStateMapping, am as FocusGuard, an as PATIENT_CLICK_THRESHOLD, ao as useTriggerRegistration, ap as triggerOpenStateMapping } from './ScrollAreaViewport-es.js';
3
+ import { a as useRefWithInit, E as EMPTY_OBJECT, u as useRenderElement, f as formatErrorMessage, m as mergeProps, b as useMergedRefs, d as EMPTY_ARRAY } from './useRenderElement-es.js';
4
+ import { C as isMouseLikePointerType, D as useId, d as useIsoLayoutEffect, b as useTimeout, k as createChangeEventDetails, E as triggerFocus, F as isMac, G as isSafari, s as ownerDocument, H as triggerPress, I as escapeKey, f as useOnMount, T as Timeout, a as useStableCallback, J as triggerHover, i as useValueAsRef, A as stopEvent, K as listNavigation, L as focusOut, M as isVirtualClick, N as isVirtualPointerEvent, y as useButton, O as closePress, u as useBaseUiId, P as itemPress, t as transitionStatusMapping, e as useOpenChangeComplete, Q as outsidePress, R as useAnimationsFinished, S as siblingOpen, h as useControlled, c as useTransitionStatus, U as imperativeAction, V as useCompositeRootContext, W as cancelOpen } from './clamp-es.js';
6
5
  import { jsx, jsxs } from 'react/jsx-runtime';
7
6
  import { b as isElement, k as getWindow, a as isHTMLElement, d as isLastTraversableNode, e as getParentNode } from './floating-ui.utils.dom-es.js';
8
7
  import { e as evaluate, h as getPaddingObject, b as getAlignmentAxis, d as getAlignment, q as getAxisLength, n as clamp, c as getSide, g as getSideAxis } from './index.esm-es.js';
9
8
  import { u as useFloating$1, h as hide$1, f as flip, d as size, b as autoUpdate, o as offset, s as shift, l as limitShift } from './floating-ui.react-dom-es.js';
10
9
  import * as ReactDOM from 'react-dom';
11
10
 
12
- /**
13
- * A button that closes the dialog.
14
- * Renders a `<button>` element.
15
- *
16
- * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
17
- */
18
- const DialogClose = /*#__PURE__*/React.forwardRef(function DialogClose(componentProps, forwardedRef) {
19
- const {
20
- render,
21
- className,
22
- disabled = false,
23
- nativeButton = true,
24
- ...elementProps
25
- } = componentProps;
26
- const {
27
- store
28
- } = useDialogRootContext();
29
- const open = store.useState('open');
30
- function handleClick(event) {
31
- if (open) {
32
- store.setOpen(false, createChangeEventDetails(closePress, event.nativeEvent));
33
- }
34
- }
35
- const {
36
- getButtonProps,
37
- buttonRef
38
- } = useButton({
39
- disabled,
40
- native: nativeButton
41
- });
42
- const state = {
43
- disabled
44
- };
45
- return useRenderElement('button', componentProps, {
46
- state,
47
- ref: [forwardedRef, buttonRef],
48
- props: [{
49
- onClick: handleClick
50
- }, elementProps, getButtonProps]
51
- });
52
- });
53
- if (process.env.NODE_ENV !== "production") DialogClose.displayName = "DialogClose";
54
-
55
- /**
56
- * A button that closes the drawer.
57
- * Renders a `<button>` element.
58
- *
59
- * Documentation: [Base UI Drawer](https://base-ui.com/react/components/drawer)
60
- */
61
- const DrawerClose = DialogClose;
62
-
63
11
  function resolveValue(value, pointerType) {
64
12
  if (pointerType != null && !isMouseLikePointerType(pointerType)) {
65
13
  return 0;
@@ -86,109 +34,6 @@ function isClickLikeOpenEvent(openEventType, interactedInside) {
86
34
  return interactedInside || openEventType === 'click' || openEventType === 'mousedown';
87
35
  }
88
36
 
89
- /**
90
- * Opens or closes the floating element when clicking the reference element.
91
- * @see https://floating-ui.com/docs/useClick
92
- */
93
- function useClick(context, props = {}) {
94
- const store = 'rootStore' in context ? context.rootStore : context;
95
- const dataRef = store.context.dataRef;
96
- const {
97
- enabled = true,
98
- event: eventOption = 'click',
99
- toggle = true,
100
- ignoreMouse = false,
101
- stickIfOpen = true,
102
- touchOpenDelay = 0,
103
- reason = triggerPress
104
- } = props;
105
- const pointerTypeRef = React.useRef(undefined);
106
- const frame = useAnimationFrame();
107
- const touchOpenTimeout = useTimeout();
108
- const reference = React.useMemo(() => ({
109
- onPointerDown(event) {
110
- pointerTypeRef.current = event.pointerType;
111
- },
112
- onMouseDown(event) {
113
- const pointerType = pointerTypeRef.current;
114
- const nativeEvent = event.nativeEvent;
115
- const open = store.select('open');
116
-
117
- // Ignore all buttons except for the "main" button.
118
- // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
119
- if (event.button !== 0 || eventOption === 'click' || isMouseLikePointerType(pointerType, true) && ignoreMouse) {
120
- return;
121
- }
122
- const openEvent = dataRef.current.openEvent;
123
- const openEventType = openEvent?.type;
124
- const hasClickedOnInactiveTrigger = store.select('domReferenceElement') !== event.currentTarget;
125
- const nextOpen = open && hasClickedOnInactiveTrigger || !(open && toggle && (openEvent && stickIfOpen ? openEventType === 'click' || openEventType === 'mousedown' : true));
126
-
127
- // Animations sometimes won't run on a typeable element if using a rAF.
128
- // Focus is always set on these elements. For touch, we may delay opening.
129
- if (isTypeableElement(nativeEvent.target)) {
130
- const details = createChangeEventDetails(reason, nativeEvent, nativeEvent.target);
131
- if (nextOpen && pointerType === 'touch' && touchOpenDelay > 0) {
132
- touchOpenTimeout.start(touchOpenDelay, () => {
133
- store.setOpen(true, details);
134
- });
135
- } else {
136
- store.setOpen(nextOpen, details);
137
- }
138
- return;
139
- }
140
-
141
- // Capture the currentTarget before the rAF.
142
- // as React sets it to null after the event handler completes.
143
- const eventCurrentTarget = event.currentTarget;
144
-
145
- // Wait until focus is set on the element. This is an alternative to
146
- // `event.preventDefault()` to avoid :focus-visible from appearing when using a pointer.
147
- frame.request(() => {
148
- const details = createChangeEventDetails(reason, nativeEvent, eventCurrentTarget);
149
- if (nextOpen && pointerType === 'touch' && touchOpenDelay > 0) {
150
- touchOpenTimeout.start(touchOpenDelay, () => {
151
- store.setOpen(true, details);
152
- });
153
- } else {
154
- store.setOpen(nextOpen, details);
155
- }
156
- });
157
- },
158
- onClick(event) {
159
- if (eventOption === 'mousedown-only') {
160
- return;
161
- }
162
- const pointerType = pointerTypeRef.current;
163
- if (eventOption === 'mousedown' && pointerType) {
164
- pointerTypeRef.current = undefined;
165
- return;
166
- }
167
- if (isMouseLikePointerType(pointerType, true) && ignoreMouse) {
168
- return;
169
- }
170
- const open = store.select('open');
171
- const openEvent = dataRef.current.openEvent;
172
- const hasClickedOnInactiveTrigger = store.select('domReferenceElement') !== event.currentTarget;
173
- const nextOpen = open && hasClickedOnInactiveTrigger || !(open && toggle && (openEvent && stickIfOpen ? isClickLikeEvent(openEvent) : true));
174
- const details = createChangeEventDetails(reason, event.nativeEvent, event.currentTarget);
175
- if (nextOpen && pointerType === 'touch' && touchOpenDelay > 0) {
176
- touchOpenTimeout.start(touchOpenDelay, () => {
177
- store.setOpen(true, details);
178
- });
179
- } else {
180
- store.setOpen(nextOpen, details);
181
- }
182
- },
183
- onKeyDown() {
184
- pointerTypeRef.current = undefined;
185
- }
186
- }), [dataRef, eventOption, ignoreMouse, store, stickIfOpen, toggle, frame, touchOpenTimeout, touchOpenDelay, reason]);
187
- return React.useMemo(() => enabled ? {
188
- reference
189
- } : EMPTY_OBJECT, [enabled, reference]);
190
- }
191
-
192
37
  function useFloatingRootContext(options) {
193
38
  const {
194
39
  open = false,
@@ -1973,1452 +1818,1460 @@ function safePolygon(options = {}) {
1973
1818
  return fn;
1974
1819
  }
1975
1820
 
1976
- /**
1977
- * A heading that labels the dialog.
1978
- * Renders an `<h2>` element.
1979
- *
1980
- * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
1981
- */
1982
- const DialogTitle = /*#__PURE__*/React.forwardRef(function DialogTitle(componentProps, forwardedRef) {
1983
- const {
1984
- render,
1985
- className,
1986
- id: idProp,
1987
- ...elementProps
1988
- } = componentProps;
1989
- const {
1990
- store
1991
- } = useDialogRootContext();
1992
- const id = useBaseUiId(idProp);
1993
- store.useSyncedValueWithCleanup('titleElementId', id);
1994
- return useRenderElement('h2', componentProps, {
1995
- ref: forwardedRef,
1996
- props: [{
1997
- id
1998
- }, elementProps]
1999
- });
2000
- });
2001
- if (process.env.NODE_ENV !== "production") DialogTitle.displayName = "DialogTitle";
1821
+ function getPseudoElementBounds(element) {
1822
+ const elementRect = element.getBoundingClientRect();
2002
1823
 
2003
- /**
2004
- * A button that opens the dialog.
2005
- * Renders a `<button>` element.
2006
- *
2007
- * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
2008
- */
2009
- const DialogTrigger = /*#__PURE__*/React.forwardRef(function DialogTrigger(componentProps, forwardedRef) {
2010
- const {
2011
- render,
2012
- className,
2013
- disabled = false,
2014
- nativeButton = true,
2015
- id: idProp,
2016
- payload,
2017
- handle,
2018
- ...elementProps
2019
- } = componentProps;
2020
- const dialogRootContext = useDialogRootContext(true);
2021
- const store = handle?.store ?? dialogRootContext?.store;
2022
- if (!store) {
2023
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: <Dialog.Trigger> must be used within <Dialog.Root> or provided with a handle.' : formatErrorMessage(79));
1824
+ // Avoid "Not implemented: window.getComputedStyle(elt, pseudoElt)"
1825
+ if (process.env.NODE_ENV !== 'production') {
1826
+ return elementRect;
1827
+ }
1828
+ const beforeStyles = window.getComputedStyle(element, '::before');
1829
+ const afterStyles = window.getComputedStyle(element, '::after');
1830
+ const hasPseudoElements = beforeStyles.content !== 'none' || afterStyles.content !== 'none';
1831
+ if (!hasPseudoElements) {
1832
+ return elementRect;
2024
1833
  }
2025
- const thisTriggerId = useBaseUiId(idProp);
2026
- const floatingContext = store.useState('floatingRootContext');
2027
- const isOpenedByThisTrigger = store.useState('isOpenedByTrigger', thisTriggerId);
2028
- const triggerElementRef = React.useRef(null);
2029
- const {
2030
- registerTrigger,
2031
- isMountedByThisTrigger
2032
- } = useTriggerDataForwarding(thisTriggerId, triggerElementRef, store, {
2033
- payload
2034
- });
2035
- const {
2036
- getButtonProps,
2037
- buttonRef
2038
- } = useButton({
2039
- disabled,
2040
- native: nativeButton
2041
- });
2042
- const click = useClick(floatingContext, {
2043
- enabled: floatingContext != null
2044
- });
2045
- const localInteractionProps = useInteractions([click]);
2046
- const state = {
2047
- disabled,
2048
- open: isOpenedByThisTrigger
2049
- };
2050
- const rootTriggerProps = store.useState('triggerProps', isMountedByThisTrigger);
2051
- return useRenderElement('button', componentProps, {
2052
- state,
2053
- ref: [buttonRef, forwardedRef, registerTrigger, triggerElementRef],
2054
- props: [localInteractionProps.getReferenceProps(), rootTriggerProps, {
2055
- [CLICK_TRIGGER_IDENTIFIER]: '',
2056
- id: thisTriggerId
2057
- }, elementProps, getButtonProps],
2058
- stateAttributesMapping: triggerOpenStateMapping
2059
- });
2060
- });
2061
- if (process.env.NODE_ENV !== "production") DialogTrigger.displayName = "DialogTrigger";
2062
-
2063
- /**
2064
- * A heading that labels the drawer.
2065
- * Renders an `<h2>` element.
2066
- *
2067
- * Documentation: [Base UI Drawer](https://base-ui.com/react/components/drawer)
2068
- */
2069
- const DrawerTitle = DialogTitle;
2070
1834
 
2071
- /**
2072
- * A button that opens the drawer.
2073
- * Renders a `<button>` element.
2074
- *
2075
- * Documentation: [Base UI Drawer](https://base-ui.com/react/components/drawer)
2076
- */
2077
- const DrawerTrigger = DialogTrigger;
1835
+ // Get dimensions of pseudo-elements
1836
+ const beforeWidth = parseFloat(beforeStyles.width) || 0;
1837
+ const beforeHeight = parseFloat(beforeStyles.height) || 0;
1838
+ const afterWidth = parseFloat(afterStyles.width) || 0;
1839
+ const afterHeight = parseFloat(afterStyles.height) || 0;
2078
1840
 
2079
- const MenuPositionerContext = /*#__PURE__*/React.createContext(undefined);
2080
- if (process.env.NODE_ENV !== "production") MenuPositionerContext.displayName = "MenuPositionerContext";
2081
- function useMenuPositionerContext(optional) {
2082
- const context = React.useContext(MenuPositionerContext);
2083
- if (context === undefined && !optional) {
2084
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: MenuPositionerContext is missing. MenuPositioner parts must be placed within <Menu.Positioner>.' : formatErrorMessage(33));
2085
- }
2086
- return context;
2087
- }
1841
+ // Calculate max dimensions including pseudo-elements
1842
+ const totalWidth = Math.max(elementRect.width, beforeWidth, afterWidth);
1843
+ const totalHeight = Math.max(elementRect.height, beforeHeight, afterHeight);
2088
1844
 
2089
- const MenuRootContext = /*#__PURE__*/React.createContext(undefined);
2090
- if (process.env.NODE_ENV !== "production") MenuRootContext.displayName = "MenuRootContext";
2091
- function useMenuRootContext(optional) {
2092
- const context = React.useContext(MenuRootContext);
2093
- if (context === undefined && !optional) {
2094
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: MenuRootContext is missing. Menu parts must be placed within <Menu.Root>.' : formatErrorMessage(36));
2095
- }
2096
- return context;
1845
+ // Calculate the differences to extend the bounds
1846
+ const widthDiff = totalWidth - elementRect.width;
1847
+ const heightDiff = totalHeight - elementRect.height;
1848
+ return {
1849
+ left: elementRect.left - widthDiff / 2,
1850
+ right: elementRect.right + widthDiff / 2,
1851
+ top: elementRect.top - heightDiff / 2,
1852
+ bottom: elementRect.bottom + heightDiff / 2
1853
+ };
2097
1854
  }
2098
1855
 
2099
- const ContextMenuRootContext = /*#__PURE__*/React.createContext(undefined);
2100
- if (process.env.NODE_ENV !== "production") ContextMenuRootContext.displayName = "ContextMenuRootContext";
2101
- function useContextMenuRootContext(optional = true) {
2102
- const context = React.useContext(ContextMenuRootContext);
2103
- if (context === undefined && !optional) {
2104
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: ContextMenuRootContext is missing. ContextMenu parts must be placed within <ContextMenu.Root>.' : formatErrorMessage(25));
1856
+ const CompositeListContext = /*#__PURE__*/React.createContext({
1857
+ register: () => {},
1858
+ unregister: () => {},
1859
+ subscribeMapChange: () => {
1860
+ return () => {};
1861
+ },
1862
+ elementsRef: {
1863
+ current: []
1864
+ },
1865
+ nextIndexRef: {
1866
+ current: 0
2105
1867
  }
2106
- return context;
1868
+ });
1869
+ if (process.env.NODE_ENV !== "production") CompositeListContext.displayName = "CompositeListContext";
1870
+ function useCompositeListContext() {
1871
+ return React.useContext(CompositeListContext);
2107
1872
  }
2108
1873
 
1874
+ /* eslint-disable no-bitwise */
2109
1875
  /**
2110
- * Returns common props shared by all menu item types.
2111
- * This hook extracts the shared logic for id, role, tabIndex, onKeyDown,
2112
- * onMouseMove, onClick, and onMouseUp handlers.
1876
+ * Provides context for a list of items in a composite component.
1877
+ * @internal
2113
1878
  */
2114
- function useMenuItemCommonProps(params) {
1879
+ function CompositeList(props) {
2115
1880
  const {
2116
- closeOnClick,
2117
- highlighted,
2118
- id,
2119
- nodeId,
2120
- store,
2121
- typingRef,
2122
- itemRef,
2123
- itemMetadata
2124
- } = params;
2125
- const {
2126
- events: menuEvents
2127
- } = store.useState('floatingTreeRoot');
2128
- const contextMenuContext = useContextMenuRootContext(true);
2129
- const isContextMenu = contextMenuContext !== undefined;
2130
- return React.useMemo(() => ({
2131
- id,
2132
- role: 'menuitem',
2133
- tabIndex: highlighted ? 0 : -1,
2134
- onKeyDown(event) {
2135
- if (event.key === ' ' && typingRef?.current) {
2136
- event.preventDefault();
2137
- }
2138
- },
2139
- onMouseMove(event) {
2140
- if (!nodeId) {
2141
- return;
2142
- }
1881
+ children,
1882
+ elementsRef,
1883
+ labelsRef,
1884
+ onMapChange: onMapChangeProp
1885
+ } = props;
1886
+ const onMapChange = useStableCallback(onMapChangeProp);
1887
+ const nextIndexRef = React.useRef(0);
1888
+ const listeners = useRefWithInit(createListeners).current;
2143
1889
 
2144
- // Inform the floating tree that a menu item within this menu was hovered/moved over
2145
- // so unrelated descendant submenus can be closed.
2146
- menuEvents.emit('itemhover', {
2147
- nodeId,
2148
- target: event.currentTarget
1890
+ // We use a stable `map` to avoid O(n^2) re-allocation costs for large lists.
1891
+ // `mapTick` is our re-render trigger mechanism. We also need to update the
1892
+ // elements and label refs, but there's a lot of async work going on and sometimes
1893
+ // the effect that handles `onMapChange` gets called after those refs have been
1894
+ // filled, and we don't want to lose those values by setting their lengths to `0`.
1895
+ // We also need to have them at the proper length because floating-ui uses that
1896
+ // information for list navigation.
1897
+
1898
+ const map = useRefWithInit(createMap).current;
1899
+ // `mapTick` uses a counter rather than objects for low precision-loss risk and better memory efficiency
1900
+ const [mapTick, setMapTick] = React.useState(0);
1901
+ const lastTickRef = React.useRef(mapTick);
1902
+ const register = useStableCallback((node, metadata) => {
1903
+ map.set(node, metadata ?? null);
1904
+ lastTickRef.current += 1;
1905
+ setMapTick(lastTickRef.current);
1906
+ });
1907
+ const unregister = useStableCallback(node => {
1908
+ map.delete(node);
1909
+ lastTickRef.current += 1;
1910
+ setMapTick(lastTickRef.current);
1911
+ });
1912
+ const sortedMap = React.useMemo(() => {
1913
+ const newMap = new Map();
1914
+ // Filter out disconnected elements before sorting to avoid inconsistent
1915
+ // compareDocumentPosition results when elements are detached from the DOM.
1916
+ const sortedNodes = Array.from(map.keys()).filter(node => node.isConnected).sort(sortByDocumentPosition);
1917
+ sortedNodes.forEach((node, index) => {
1918
+ const metadata = map.get(node) ?? {};
1919
+ newMap.set(node, {
1920
+ ...metadata,
1921
+ index
2149
1922
  });
2150
- },
2151
- onClick(event) {
2152
- if (closeOnClick) {
2153
- menuEvents.emit('close', {
2154
- domEvent: event,
2155
- reason: itemPress
1923
+ });
1924
+ return newMap;
1925
+ }, [map, mapTick]);
1926
+ useIsoLayoutEffect(() => {
1927
+ if (typeof MutationObserver !== 'function' || sortedMap.size === 0) {
1928
+ return undefined;
1929
+ }
1930
+ const mutationObserver = new MutationObserver(entries => {
1931
+ const diff = new Set();
1932
+ const updateDiff = node => diff.has(node) ? diff.delete(node) : diff.add(node);
1933
+ entries.forEach(entry => {
1934
+ entry.removedNodes.forEach(updateDiff);
1935
+ entry.addedNodes.forEach(updateDiff);
1936
+ });
1937
+ if (diff.size === 0) {
1938
+ lastTickRef.current += 1;
1939
+ setMapTick(lastTickRef.current);
1940
+ }
1941
+ });
1942
+ sortedMap.forEach((_, node) => {
1943
+ if (node.parentElement) {
1944
+ mutationObserver.observe(node.parentElement, {
1945
+ childList: true
2156
1946
  });
2157
1947
  }
2158
- },
2159
- onMouseUp(event) {
2160
- if (contextMenuContext) {
2161
- const initialCursorPoint = contextMenuContext.initialCursorPointRef.current;
2162
- contextMenuContext.initialCursorPointRef.current = null;
2163
- if (isContextMenu && initialCursorPoint && Math.abs(event.clientX - initialCursorPoint.x) <= 1 && Math.abs(event.clientY - initialCursorPoint.y) <= 1) {
2164
- return;
2165
- }
2166
-
2167
- // On non-macOS platforms, this mouseup belongs to the right-click gesture
2168
- // that opened the context menu, so it must not activate an item.
2169
- if (isContextMenu && !isMac && event.button === 2) {
2170
- return;
2171
- }
1948
+ });
1949
+ return () => {
1950
+ mutationObserver.disconnect();
1951
+ };
1952
+ }, [sortedMap]);
1953
+ useIsoLayoutEffect(() => {
1954
+ const shouldUpdateLengths = lastTickRef.current === mapTick;
1955
+ if (shouldUpdateLengths) {
1956
+ if (elementsRef.current.length !== sortedMap.size) {
1957
+ elementsRef.current.length = sortedMap.size;
2172
1958
  }
2173
- if (itemRef.current && store.context.allowMouseUpTriggerRef.current && (!isContextMenu || event.button === 2)) {
2174
- // This fires whenever the user clicks on the trigger, moves the cursor, and releases it over the item.
2175
- // We trigger the click and override the `closeOnClick` preference to always close the menu.
2176
- if (!itemMetadata || itemMetadata.type === 'regular-item') {
2177
- itemRef.current.click();
2178
- }
1959
+ if (labelsRef && labelsRef.current.length !== sortedMap.size) {
1960
+ labelsRef.current.length = sortedMap.size;
2179
1961
  }
1962
+ nextIndexRef.current = sortedMap.size;
2180
1963
  }
2181
- }), [closeOnClick, highlighted, id, menuEvents, nodeId, store, typingRef, itemRef, contextMenuContext, isContextMenu, itemMetadata]);
2182
- }
2183
-
2184
- const REGULAR_ITEM = {
2185
- type: 'regular-item'
2186
- };
2187
- function useMenuItem(params) {
2188
- const {
2189
- closeOnClick,
2190
- disabled = false,
2191
- highlighted,
2192
- id,
2193
- store,
2194
- typingRef = store.context.typingRef,
2195
- nativeButton,
2196
- itemMetadata,
2197
- nodeId
2198
- } = params;
2199
- const itemRef = React.useRef(null);
2200
- const {
2201
- getButtonProps,
2202
- buttonRef
2203
- } = useButton({
2204
- disabled,
2205
- focusableWhenDisabled: true,
2206
- native: nativeButton,
2207
- composite: true
1964
+ onMapChange(sortedMap);
1965
+ }, [onMapChange, sortedMap, elementsRef, labelsRef, mapTick]);
1966
+ useIsoLayoutEffect(() => {
1967
+ return () => {
1968
+ elementsRef.current = [];
1969
+ };
1970
+ }, [elementsRef]);
1971
+ useIsoLayoutEffect(() => {
1972
+ return () => {
1973
+ if (labelsRef) {
1974
+ labelsRef.current = [];
1975
+ }
1976
+ };
1977
+ }, [labelsRef]);
1978
+ const subscribeMapChange = useStableCallback(fn => {
1979
+ listeners.add(fn);
1980
+ return () => {
1981
+ listeners.delete(fn);
1982
+ };
2208
1983
  });
2209
- const commonProps = useMenuItemCommonProps({
2210
- closeOnClick,
2211
- highlighted,
2212
- id,
2213
- nodeId,
2214
- store,
2215
- typingRef,
2216
- itemRef,
2217
- itemMetadata
1984
+ useIsoLayoutEffect(() => {
1985
+ listeners.forEach(l => l(sortedMap));
1986
+ }, [listeners, sortedMap]);
1987
+ const contextValue = React.useMemo(() => ({
1988
+ register,
1989
+ unregister,
1990
+ subscribeMapChange,
1991
+ elementsRef,
1992
+ labelsRef,
1993
+ nextIndexRef
1994
+ }), [register, unregister, subscribeMapChange, elementsRef, labelsRef, nextIndexRef]);
1995
+ return /*#__PURE__*/jsx(CompositeListContext.Provider, {
1996
+ value: contextValue,
1997
+ children: children
2218
1998
  });
2219
- const getItemProps = React.useCallback(externalProps => {
2220
- return mergeProps(commonProps, {
2221
- onMouseEnter() {
2222
- if (itemMetadata.type !== 'submenu-trigger') {
2223
- return;
2224
- }
2225
- itemMetadata.setActive();
2226
- }
2227
- }, externalProps, getButtonProps);
2228
- }, [commonProps, getButtonProps, itemMetadata]);
2229
- const mergedRef = useMergedRefs(itemRef, buttonRef);
2230
- return React.useMemo(() => ({
2231
- getItemProps,
2232
- itemRef: mergedRef
2233
- }), [getItemProps, mergedRef]);
2234
1999
  }
2235
-
2236
- const CompositeListContext = /*#__PURE__*/React.createContext({
2237
- register: () => {},
2238
- unregister: () => {},
2239
- subscribeMapChange: () => {
2240
- return () => {};
2241
- },
2242
- elementsRef: {
2243
- current: []
2244
- },
2245
- nextIndexRef: {
2246
- current: 0
2000
+ function createMap() {
2001
+ return new Map();
2002
+ }
2003
+ function createListeners() {
2004
+ return new Set();
2005
+ }
2006
+ function sortByDocumentPosition(a, b) {
2007
+ const position = a.compareDocumentPosition(b);
2008
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING || position & Node.DOCUMENT_POSITION_CONTAINED_BY) {
2009
+ return -1;
2247
2010
  }
2248
- });
2249
- if (process.env.NODE_ENV !== "production") CompositeListContext.displayName = "CompositeListContext";
2250
- function useCompositeListContext() {
2251
- return React.useContext(CompositeListContext);
2011
+ if (position & Node.DOCUMENT_POSITION_PRECEDING || position & Node.DOCUMENT_POSITION_CONTAINS) {
2012
+ return 1;
2013
+ }
2014
+ return 0;
2252
2015
  }
2253
2016
 
2254
- let IndexGuessBehavior = /*#__PURE__*/function (IndexGuessBehavior) {
2255
- IndexGuessBehavior[IndexGuessBehavior["None"] = 0] = "None";
2256
- IndexGuessBehavior[IndexGuessBehavior["GuessFromOrder"] = 1] = "GuessFromOrder";
2257
- return IndexGuessBehavior;
2258
- }({});
2259
-
2260
2017
  /**
2261
- * Used to register a list item and its index (DOM position) in the `CompositeList`.
2018
+ * Fork of the original `arrow` middleware from Floating UI that allows
2019
+ * configuring the offset parent.
2262
2020
  */
2263
- function useCompositeListItem(params = {}) {
2264
- const {
2265
- label,
2266
- metadata,
2267
- textRef,
2268
- indexGuessBehavior,
2269
- index: externalIndex
2270
- } = params;
2271
- const {
2272
- register,
2273
- unregister,
2274
- subscribeMapChange,
2275
- elementsRef,
2276
- labelsRef,
2277
- nextIndexRef
2278
- } = useCompositeListContext();
2279
- const indexRef = React.useRef(-1);
2280
- const [index, setIndex] = React.useState(externalIndex ?? (indexGuessBehavior === IndexGuessBehavior.GuessFromOrder ? () => {
2281
- if (indexRef.current === -1) {
2282
- const newIndex = nextIndexRef.current;
2283
- nextIndexRef.current += 1;
2284
- indexRef.current = newIndex;
2285
- }
2286
- return indexRef.current;
2287
- } : -1));
2288
- const componentRef = React.useRef(null);
2289
- const ref = React.useCallback(node => {
2290
- componentRef.current = node;
2291
- if (index !== -1 && node !== null) {
2292
- elementsRef.current[index] = node;
2293
- if (labelsRef) {
2294
- const isLabelDefined = label !== undefined;
2295
- labelsRef.current[index] = isLabelDefined ? label : textRef?.current?.textContent ?? node.textContent;
2296
- }
2297
- }
2298
- }, [index, elementsRef, labelsRef, label, textRef]);
2299
- useIsoLayoutEffect(() => {
2300
- if (externalIndex != null) {
2301
- return undefined;
2302
- }
2303
- const node = componentRef.current;
2304
- if (node) {
2305
- register(node, metadata);
2306
- return () => {
2307
- unregister(node);
2308
- };
2021
+ const baseArrow = options => ({
2022
+ name: 'arrow',
2023
+ options,
2024
+ async fn(state) {
2025
+ const {
2026
+ x,
2027
+ y,
2028
+ placement,
2029
+ rects,
2030
+ platform,
2031
+ elements,
2032
+ middlewareData
2033
+ } = state;
2034
+ // Since `element` is required, we don't Partial<> the type.
2035
+ const {
2036
+ element,
2037
+ padding = 0,
2038
+ offsetParent = 'real'
2039
+ } = evaluate(options, state) || {};
2040
+ if (element == null) {
2041
+ return {};
2309
2042
  }
2310
- return undefined;
2311
- }, [externalIndex, register, unregister, metadata]);
2312
- useIsoLayoutEffect(() => {
2313
- if (externalIndex != null) {
2314
- return undefined;
2043
+ const paddingObject = getPaddingObject(padding);
2044
+ const coords = {
2045
+ x,
2046
+ y
2047
+ };
2048
+ const axis = getAlignmentAxis(placement);
2049
+ const length = getAxisLength(axis);
2050
+ const arrowDimensions = await platform.getDimensions(element);
2051
+ const isYAxis = axis === 'y';
2052
+ const minProp = isYAxis ? 'top' : 'left';
2053
+ const maxProp = isYAxis ? 'bottom' : 'right';
2054
+ const clientProp = isYAxis ? 'clientHeight' : 'clientWidth';
2055
+ const endDiff = rects.reference[length] + rects.reference[axis] - coords[axis] - rects.floating[length];
2056
+ const startDiff = coords[axis] - rects.reference[axis];
2057
+ const arrowOffsetParent = offsetParent === 'real' ? await platform.getOffsetParent?.(element) : elements.floating;
2058
+ let clientSize = elements.floating[clientProp] || rects.floating[length];
2059
+
2060
+ // DOM platform can return `window` as the `offsetParent`.
2061
+ if (!clientSize || !(await platform.isElement?.(arrowOffsetParent))) {
2062
+ clientSize = elements.floating[clientProp] || rects.floating[length];
2315
2063
  }
2316
- return subscribeMapChange(map => {
2317
- const i = componentRef.current ? map.get(componentRef.current)?.index : null;
2318
- if (i != null) {
2319
- setIndex(i);
2320
- }
2321
- });
2322
- }, [externalIndex, subscribeMapChange, setIndex]);
2323
- return React.useMemo(() => ({
2324
- ref,
2325
- index
2326
- }), [index, ref]);
2327
- }
2064
+ const centerToReference = endDiff / 2 - startDiff / 2;
2328
2065
 
2329
- let MenuCheckboxItemDataAttributes = /*#__PURE__*/function (MenuCheckboxItemDataAttributes) {
2330
- /**
2331
- * Present when the menu checkbox item is checked.
2332
- */
2333
- MenuCheckboxItemDataAttributes["checked"] = "data-checked";
2334
- /**
2335
- * Present when the menu checkbox item is not checked.
2336
- */
2337
- MenuCheckboxItemDataAttributes["unchecked"] = "data-unchecked";
2338
- /**
2339
- * Present when the menu checkbox item is disabled.
2340
- */
2341
- MenuCheckboxItemDataAttributes["disabled"] = "data-disabled";
2342
- /**
2343
- * Present when the menu checkbox item is highlighted.
2344
- */
2345
- MenuCheckboxItemDataAttributes["highlighted"] = "data-highlighted";
2346
- return MenuCheckboxItemDataAttributes;
2347
- }({});
2066
+ // If the padding is large enough that it causes the arrow to no longer be
2067
+ // centered, modify the padding so that it is centered.
2068
+ const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1;
2069
+ const minPadding = Math.min(paddingObject[minProp], largestPossiblePadding);
2070
+ const maxPadding = Math.min(paddingObject[maxProp], largestPossiblePadding);
2348
2071
 
2349
- const itemMapping = {
2350
- checked(value) {
2351
- if (value) {
2352
- return {
2353
- [MenuCheckboxItemDataAttributes.checked]: ''
2354
- };
2355
- }
2072
+ // Make sure the arrow doesn't overflow the floating element if the center
2073
+ // point is outside the floating element's bounds.
2074
+ const min = minPadding;
2075
+ const max = clientSize - arrowDimensions[length] - maxPadding;
2076
+ const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference;
2077
+ const offset = clamp(min, center, max);
2078
+
2079
+ // If the reference is small enough that the arrow's padding causes it to
2080
+ // to point to nothing for an aligned placement, adjust the offset of the
2081
+ // floating element itself. To ensure `shift()` continues to take action,
2082
+ // a single reset is performed when this is true.
2083
+ const shouldAddOffset = !middlewareData.arrow && getAlignment(placement) != null && center !== offset && rects.reference[length] / 2 - (center < min ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0;
2084
+ // eslint-disable-next-line no-nested-ternary
2085
+ const alignmentOffset = shouldAddOffset ? center < min ? center - min : center - max : 0;
2356
2086
  return {
2357
- [MenuCheckboxItemDataAttributes.unchecked]: ''
2087
+ [axis]: coords[axis] + alignmentOffset,
2088
+ data: {
2089
+ [axis]: offset,
2090
+ centerOffset: center - offset - alignmentOffset,
2091
+ ...(shouldAddOffset && {
2092
+ alignmentOffset
2093
+ })
2094
+ },
2095
+ reset: shouldAddOffset
2358
2096
  };
2359
- },
2360
- ...transitionStatusMapping
2361
- };
2362
-
2363
- const MenuGroupContext = /*#__PURE__*/React.createContext(undefined);
2364
- if (process.env.NODE_ENV !== "production") MenuGroupContext.displayName = "MenuGroupContext";
2365
- function useMenuGroupRootContext() {
2366
- const context = React.useContext(MenuGroupContext);
2367
- if (context === undefined) {
2368
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: MenuGroupRootContext is missing. Menu group parts must be used within <Menu.Group>.' : formatErrorMessage(31));
2369
2097
  }
2370
- return context;
2371
- }
2372
-
2373
- const MenuGroup = /*#__PURE__*/React.forwardRef(function MenuGroup(componentProps, forwardedRef) {
2374
- const {
2375
- render,
2376
- className,
2377
- ...elementProps
2378
- } = componentProps;
2379
- const [labelId, setLabelId] = React.useState(undefined);
2380
- const context = React.useMemo(() => ({
2381
- setLabelId
2382
- }), [setLabelId]);
2383
- const element = useRenderElement('div', componentProps, {
2384
- ref: forwardedRef,
2385
- props: {
2386
- role: 'group',
2387
- 'aria-labelledby': labelId,
2388
- ...elementProps
2389
- }
2390
- });
2391
- return /*#__PURE__*/jsx(MenuGroupContext.Provider, {
2392
- value: context,
2393
- children: element
2394
- });
2395
2098
  });
2396
- if (process.env.NODE_ENV !== "production") MenuGroup.displayName = "MenuGroup";
2397
2099
 
2398
2100
  /**
2399
- * An accessible label that is automatically associated with its parent group.
2400
- * Renders a `<div>` element.
2401
- *
2402
- * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu)
2101
+ * Provides data to position an inner element of the floating element so that it
2102
+ * appears centered to the reference element.
2103
+ * This wraps the core `arrow` middleware to allow React refs as the element.
2104
+ * @see https://floating-ui.com/docs/arrow
2403
2105
  */
2404
- const MenuGroupLabel = /*#__PURE__*/React.forwardRef(function MenuGroupLabelComponent(componentProps, forwardedRef) {
2405
- const {
2406
- className,
2407
- render,
2408
- id: idProp,
2409
- ...elementProps
2410
- } = componentProps;
2411
- const id = useBaseUiId(idProp);
2412
- const {
2413
- setLabelId
2414
- } = useMenuGroupRootContext();
2415
- useIsoLayoutEffect(() => {
2416
- setLabelId(id);
2417
- return () => {
2418
- setLabelId(undefined);
2419
- };
2420
- }, [setLabelId, id]);
2421
- return useRenderElement('div', componentProps, {
2422
- ref: forwardedRef,
2423
- props: {
2424
- id,
2425
- role: 'presentation',
2426
- ...elementProps
2427
- }
2428
- });
2106
+ const arrow = (options, deps) => ({
2107
+ ...baseArrow(options),
2108
+ options: [options, deps]
2429
2109
  });
2430
- if (process.env.NODE_ENV !== "production") MenuGroupLabel.displayName = "MenuGroupLabel";
2431
2110
 
2432
- /**
2433
- * An individual interactive item in the menu.
2434
- * Renders a `<div>` element.
2435
- *
2436
- * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu)
2437
- */
2438
- const MenuItem = /*#__PURE__*/React.forwardRef(function MenuItem(componentProps, forwardedRef) {
2439
- const {
2440
- render,
2441
- className,
2442
- id: idProp,
2443
- label,
2444
- nativeButton = false,
2445
- disabled = false,
2446
- closeOnClick = true,
2447
- ...elementProps
2448
- } = componentProps;
2449
- const listItem = useCompositeListItem({
2450
- label
2451
- });
2452
- const menuPositionerContext = useMenuPositionerContext(true);
2453
- const id = useBaseUiId(idProp);
2454
- const {
2455
- store
2456
- } = useMenuRootContext();
2457
- const highlighted = store.useState('isActive', listItem.index);
2458
- const itemProps = store.useState('itemProps');
2459
- const {
2460
- getItemProps,
2461
- itemRef
2462
- } = useMenuItem({
2463
- closeOnClick,
2464
- disabled,
2465
- highlighted,
2466
- id,
2467
- store,
2468
- nativeButton,
2469
- nodeId: menuPositionerContext?.nodeId,
2470
- itemMetadata: REGULAR_ITEM
2471
- });
2472
- const state = {
2473
- disabled,
2474
- highlighted
2475
- };
2476
- return useRenderElement('div', componentProps, {
2477
- state,
2478
- props: [itemProps, elementProps, getItemProps],
2479
- ref: [itemRef, forwardedRef, listItem.ref]
2480
- });
2481
- });
2482
- if (process.env.NODE_ENV !== "production") MenuItem.displayName = "MenuItem";
2111
+ const hide = {
2112
+ name: 'hide',
2113
+ async fn(state) {
2114
+ const {
2115
+ width,
2116
+ height,
2117
+ x,
2118
+ y
2119
+ } = state.rects.reference;
2120
+ const anchorHidden = width === 0 && height === 0 && x === 0 && y === 0;
2121
+ const nativeHideResult = await hide$1().fn(state);
2122
+ return {
2123
+ data: {
2124
+ referenceHidden: nativeHideResult.data?.referenceHidden || anchorHidden
2125
+ }
2126
+ };
2127
+ }
2128
+ };
2129
+
2130
+ const DEFAULT_SIDES = {
2131
+ sideX: 'left',
2132
+ sideY: 'top'
2133
+ };
2134
+ const adaptiveOrigin = {
2135
+ name: 'adaptiveOrigin',
2136
+ async fn(state) {
2137
+ const {
2138
+ x: rawX,
2139
+ y: rawY,
2140
+ rects: {
2141
+ floating: floatRect
2142
+ },
2143
+ elements: {
2144
+ floating
2145
+ },
2146
+ platform,
2147
+ strategy,
2148
+ placement
2149
+ } = state;
2150
+ const win = getWindow(floating);
2151
+ const styles = win.getComputedStyle(floating);
2152
+ const hasTransition = styles.transitionDuration !== '0s' && styles.transitionDuration !== '';
2153
+ if (!hasTransition) {
2154
+ return {
2155
+ x: rawX,
2156
+ y: rawY,
2157
+ data: DEFAULT_SIDES
2158
+ };
2159
+ }
2160
+ const offsetParent = await platform.getOffsetParent?.(floating);
2161
+ let offsetDimensions = {
2162
+ width: 0,
2163
+ height: 0
2164
+ };
2165
+
2166
+ // For fixed strategy, prefer visualViewport if available
2167
+ if (strategy === 'fixed' && win?.visualViewport) {
2168
+ offsetDimensions = {
2169
+ width: win.visualViewport.width,
2170
+ height: win.visualViewport.height
2171
+ };
2172
+ } else if (offsetParent === win) {
2173
+ const doc = ownerDocument(floating);
2174
+ offsetDimensions = {
2175
+ width: doc.documentElement.clientWidth,
2176
+ height: doc.documentElement.clientHeight
2177
+ };
2178
+ } else if (await platform.isElement?.(offsetParent)) {
2179
+ offsetDimensions = await platform.getDimensions(offsetParent);
2180
+ }
2181
+ const currentSide = getSide(placement);
2182
+ let x = rawX;
2183
+ let y = rawY;
2184
+ if (currentSide === 'left') {
2185
+ x = offsetDimensions.width - (rawX + floatRect.width);
2186
+ }
2187
+ if (currentSide === 'top') {
2188
+ y = offsetDimensions.height - (rawY + floatRect.height);
2189
+ }
2190
+ const sideX = currentSide === 'left' ? 'right' : DEFAULT_SIDES.sideX;
2191
+ const sideY = currentSide === 'top' ? 'bottom' : DEFAULT_SIDES.sideY;
2192
+ return {
2193
+ x,
2194
+ y,
2195
+ data: {
2196
+ sideX,
2197
+ sideY
2198
+ }
2199
+ };
2200
+ }
2201
+ };
2483
2202
 
2203
+ function getLogicalSide(sideParam, renderedSide, isRtl) {
2204
+ const isLogicalSideParam = sideParam === 'inline-start' || sideParam === 'inline-end';
2205
+ const logicalRight = isRtl ? 'inline-start' : 'inline-end';
2206
+ const logicalLeft = isRtl ? 'inline-end' : 'inline-start';
2207
+ return {
2208
+ top: 'top',
2209
+ right: isLogicalSideParam ? logicalRight : 'right',
2210
+ bottom: 'bottom',
2211
+ left: isLogicalSideParam ? logicalLeft : 'left'
2212
+ }[renderedSide];
2213
+ }
2214
+ function getOffsetData(state, sideParam, isRtl) {
2215
+ const {
2216
+ rects,
2217
+ placement
2218
+ } = state;
2219
+ const data = {
2220
+ side: getLogicalSide(sideParam, getSide(placement), isRtl),
2221
+ align: getAlignment(placement) || 'center',
2222
+ anchor: {
2223
+ width: rects.reference.width,
2224
+ height: rects.reference.height
2225
+ },
2226
+ positioner: {
2227
+ width: rects.floating.width,
2228
+ height: rects.floating.height
2229
+ }
2230
+ };
2231
+ return data;
2232
+ }
2484
2233
  /**
2485
- * A link in the menu that can be used to navigate to a different page or section.
2486
- * Renders an `<a>` element.
2487
- *
2488
- * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu)
2234
+ * Provides standardized anchor positioning behavior for floating elements. Wraps Floating UI's
2235
+ * `useFloating` hook.
2489
2236
  */
2490
- const MenuLinkItem = /*#__PURE__*/React.forwardRef(function MenuLinkItem(componentProps, forwardedRef) {
2491
- const {
2492
- render,
2493
- className,
2494
- id: idProp,
2495
- label,
2496
- closeOnClick = false,
2497
- ...elementProps
2498
- } = componentProps;
2499
- const linkRef = React.useRef(null);
2500
- const listItem = useCompositeListItem({
2501
- label
2502
- });
2503
- const menuPositionerContext = useMenuPositionerContext(true);
2504
- const nodeId = menuPositionerContext?.nodeId;
2505
- const id = useBaseUiId(idProp);
2506
- const {
2507
- store
2508
- } = useMenuRootContext();
2509
- const highlighted = store.useState('isActive', listItem.index);
2510
- const itemProps = store.useState('itemProps');
2511
- const typingRef = store.context.typingRef;
2237
+ function useAnchorPositioning(params) {
2512
2238
  const {
2513
- getButtonProps,
2514
- buttonRef
2515
- } = useButton({
2516
- native: false,
2517
- composite: true
2518
- });
2519
- const commonProps = useMenuItemCommonProps({
2520
- closeOnClick,
2521
- highlighted,
2522
- id,
2239
+ // Public parameters
2240
+ anchor,
2241
+ positionMethod = 'absolute',
2242
+ side: sideParam = 'bottom',
2243
+ sideOffset = 0,
2244
+ align = 'center',
2245
+ alignOffset = 0,
2246
+ collisionBoundary,
2247
+ collisionPadding: collisionPaddingParam = 5,
2248
+ sticky = false,
2249
+ arrowPadding = 5,
2250
+ disableAnchorTracking = false,
2251
+ // Private parameters
2252
+ keepMounted = false,
2253
+ floatingRootContext,
2254
+ mounted,
2255
+ collisionAvoidance,
2256
+ shiftCrossAxis = false,
2523
2257
  nodeId,
2524
- store,
2525
- typingRef,
2526
- itemRef: linkRef
2258
+ adaptiveOrigin,
2259
+ lazyFlip = false,
2260
+ externalTree
2261
+ } = params;
2262
+ const [mountSide, setMountSide] = React.useState(null);
2263
+ if (!mounted && mountSide !== null) {
2264
+ setMountSide(null);
2265
+ }
2266
+ const collisionAvoidanceSide = collisionAvoidance.side || 'flip';
2267
+ const collisionAvoidanceAlign = collisionAvoidance.align || 'flip';
2268
+ const collisionAvoidanceFallbackAxisSide = collisionAvoidance.fallbackAxisSide || 'end';
2269
+ const anchorFn = typeof anchor === 'function' ? anchor : undefined;
2270
+ const anchorFnCallback = useStableCallback(anchorFn);
2271
+ const anchorDep = anchorFn ? anchorFnCallback : anchor;
2272
+ const anchorValueRef = useValueAsRef(anchor);
2273
+ const direction = useDirection();
2274
+ const isRtl = direction === 'rtl';
2275
+ const side = mountSide || {
2276
+ top: 'top',
2277
+ right: 'right',
2278
+ bottom: 'bottom',
2279
+ left: 'left',
2280
+ 'inline-end': isRtl ? 'left' : 'right',
2281
+ 'inline-start': isRtl ? 'right' : 'left'
2282
+ }[sideParam];
2283
+ const placement = align === 'center' ? side : `${side}-${align}`;
2284
+ let collisionPadding = collisionPaddingParam;
2285
+
2286
+ // Create a bias to the preferred side.
2287
+ // On iOS, when the mobile software keyboard opens, the input is exactly centered
2288
+ // in the viewport, but this can cause it to flip to the top undesirably.
2289
+ const bias = 1;
2290
+ const biasTop = sideParam === 'bottom' ? bias : 0;
2291
+ const biasBottom = sideParam === 'top' ? bias : 0;
2292
+ const biasLeft = sideParam === 'right' ? bias : 0;
2293
+ const biasRight = sideParam === 'left' ? bias : 0;
2294
+ if (typeof collisionPadding === 'number') {
2295
+ collisionPadding = {
2296
+ top: collisionPadding + biasTop,
2297
+ right: collisionPadding + biasRight,
2298
+ bottom: collisionPadding + biasBottom,
2299
+ left: collisionPadding + biasLeft
2300
+ };
2301
+ } else if (collisionPadding) {
2302
+ collisionPadding = {
2303
+ top: (collisionPadding.top || 0) + biasTop,
2304
+ right: (collisionPadding.right || 0) + biasRight,
2305
+ bottom: (collisionPadding.bottom || 0) + biasBottom,
2306
+ left: (collisionPadding.left || 0) + biasLeft
2307
+ };
2308
+ }
2309
+ const commonCollisionProps = {
2310
+ boundary: collisionBoundary === 'clipping-ancestors' ? 'clippingAncestors' : collisionBoundary,
2311
+ padding: collisionPadding
2312
+ };
2313
+
2314
+ // Using a ref assumes that the arrow element is always present in the DOM for the lifetime of the
2315
+ // popup. If this assumption ends up being false, we can switch to state to manage the arrow's
2316
+ // presence.
2317
+ const arrowRef = React.useRef(null);
2318
+
2319
+ // Keep these reactive if they're not functions
2320
+ const sideOffsetRef = useValueAsRef(sideOffset);
2321
+ const alignOffsetRef = useValueAsRef(alignOffset);
2322
+ const sideOffsetDep = typeof sideOffset !== 'function' ? sideOffset : 0;
2323
+ const alignOffsetDep = typeof alignOffset !== 'function' ? alignOffset : 0;
2324
+ const middleware = [offset(state => {
2325
+ const data = getOffsetData(state, sideParam, isRtl);
2326
+ const sideAxis = typeof sideOffsetRef.current === 'function' ? sideOffsetRef.current(data) : sideOffsetRef.current;
2327
+ const alignAxis = typeof alignOffsetRef.current === 'function' ? alignOffsetRef.current(data) : alignOffsetRef.current;
2328
+ return {
2329
+ mainAxis: sideAxis,
2330
+ crossAxis: alignAxis,
2331
+ alignmentAxis: alignAxis
2332
+ };
2333
+ }, [sideOffsetDep, alignOffsetDep, isRtl, sideParam])];
2334
+ const shiftDisabled = collisionAvoidanceAlign === 'none' && collisionAvoidanceSide !== 'shift';
2335
+ const crossAxisShiftEnabled = !shiftDisabled && (sticky || shiftCrossAxis || collisionAvoidanceSide === 'shift');
2336
+ const flipMiddleware = collisionAvoidanceSide === 'none' ? null : flip({
2337
+ ...commonCollisionProps,
2338
+ // Ensure the popup flips if it's been limited by its --available-height and it resizes.
2339
+ // Since the size() padding is smaller than the flip() padding, flip() will take precedence.
2340
+ padding: {
2341
+ top: collisionPadding.top + bias,
2342
+ right: collisionPadding.right + bias,
2343
+ bottom: collisionPadding.bottom + bias,
2344
+ left: collisionPadding.left + bias
2345
+ },
2346
+ mainAxis: !shiftCrossAxis && collisionAvoidanceSide === 'flip',
2347
+ crossAxis: collisionAvoidanceAlign === 'flip' ? 'alignment' : false,
2348
+ fallbackAxisSideDirection: collisionAvoidanceFallbackAxisSide
2527
2349
  });
2528
- function getItemProps(externalProps) {
2529
- return mergeProps(commonProps, externalProps, getButtonProps);
2350
+ const shiftMiddleware = shiftDisabled ? null : shift(data => {
2351
+ const html = ownerDocument(data.elements.floating).documentElement;
2352
+ return {
2353
+ ...commonCollisionProps,
2354
+ // Use the Layout Viewport to avoid shifting around when pinch-zooming
2355
+ // for context menus.
2356
+ rootBoundary: shiftCrossAxis ? {
2357
+ x: 0,
2358
+ y: 0,
2359
+ width: html.clientWidth,
2360
+ height: html.clientHeight
2361
+ } : undefined,
2362
+ mainAxis: collisionAvoidanceAlign !== 'none',
2363
+ crossAxis: crossAxisShiftEnabled,
2364
+ limiter: sticky || shiftCrossAxis ? undefined : limitShift(limitData => {
2365
+ if (!arrowRef.current) {
2366
+ return {};
2367
+ }
2368
+ const {
2369
+ width,
2370
+ height
2371
+ } = arrowRef.current.getBoundingClientRect();
2372
+ const sideAxis = getSideAxis(getSide(limitData.placement));
2373
+ const arrowSize = sideAxis === 'y' ? width : height;
2374
+ const offsetAmount = sideAxis === 'y' ? collisionPadding.left + collisionPadding.right : collisionPadding.top + collisionPadding.bottom;
2375
+ return {
2376
+ offset: arrowSize / 2 + offsetAmount / 2
2377
+ };
2378
+ })
2379
+ };
2380
+ }, [commonCollisionProps, sticky, shiftCrossAxis, collisionPadding, collisionAvoidanceAlign]);
2381
+
2382
+ // https://floating-ui.com/docs/flip#combining-with-shift
2383
+ if (collisionAvoidanceSide === 'shift' || collisionAvoidanceAlign === 'shift' || align === 'center') {
2384
+ middleware.push(shiftMiddleware, flipMiddleware);
2385
+ } else {
2386
+ middleware.push(flipMiddleware, shiftMiddleware);
2530
2387
  }
2531
- const state = React.useMemo(() => ({
2532
- highlighted
2533
- }), [highlighted]);
2534
- return useRenderElement('a', componentProps, {
2535
- state,
2536
- props: [itemProps, elementProps, getItemProps],
2537
- ref: [linkRef, buttonRef, forwardedRef, listItem.ref]
2388
+ middleware.push(size({
2389
+ ...commonCollisionProps,
2390
+ apply({
2391
+ elements: {
2392
+ floating
2393
+ },
2394
+ availableWidth,
2395
+ availableHeight,
2396
+ rects
2397
+ }) {
2398
+ const floatingStyle = floating.style;
2399
+ floatingStyle.setProperty('--available-width', `${availableWidth}px`);
2400
+ floatingStyle.setProperty('--available-height', `${availableHeight}px`);
2401
+
2402
+ // Snap anchor dimensions to device pixels to ensure the popup's visual width matches the anchor's one.
2403
+ const dpr = window.devicePixelRatio || 1;
2404
+ const {
2405
+ x,
2406
+ y,
2407
+ width,
2408
+ height
2409
+ } = rects.reference;
2410
+ const anchorWidth = (Math.round((x + width) * dpr) - Math.round(x * dpr)) / dpr;
2411
+ const anchorHeight = (Math.round((y + height) * dpr) - Math.round(y * dpr)) / dpr;
2412
+ floatingStyle.setProperty('--anchor-width', `${anchorWidth}px`);
2413
+ floatingStyle.setProperty('--anchor-height', `${anchorHeight}px`);
2414
+ }
2415
+ }), arrow(() => ({
2416
+ // `transform-origin` calculations rely on an element existing. If the arrow hasn't been set,
2417
+ // we'll create a fake element.
2418
+ element: arrowRef.current || document.createElement('div'),
2419
+ padding: arrowPadding,
2420
+ offsetParent: 'floating'
2421
+ }), [arrowPadding]), {
2422
+ name: 'transformOrigin',
2423
+ fn(state) {
2424
+ const {
2425
+ elements,
2426
+ middlewareData,
2427
+ placement: renderedPlacement,
2428
+ rects,
2429
+ y
2430
+ } = state;
2431
+ const currentRenderedSide = getSide(renderedPlacement);
2432
+ const currentRenderedAxis = getSideAxis(currentRenderedSide);
2433
+ const arrowEl = arrowRef.current;
2434
+ const arrowX = middlewareData.arrow?.x || 0;
2435
+ const arrowY = middlewareData.arrow?.y || 0;
2436
+ const arrowWidth = arrowEl?.clientWidth || 0;
2437
+ const arrowHeight = arrowEl?.clientHeight || 0;
2438
+ const transformX = arrowX + arrowWidth / 2;
2439
+ const transformY = arrowY + arrowHeight / 2;
2440
+ const shiftY = Math.abs(middlewareData.shift?.y || 0);
2441
+ const halfAnchorHeight = rects.reference.height / 2;
2442
+ const sideOffsetValue = typeof sideOffset === 'function' ? sideOffset(getOffsetData(state, sideParam, isRtl)) : sideOffset;
2443
+ const isOverlappingAnchor = shiftY > sideOffsetValue;
2444
+ const adjacentTransformOrigin = {
2445
+ top: `${transformX}px calc(100% + ${sideOffsetValue}px)`,
2446
+ bottom: `${transformX}px ${-sideOffsetValue}px`,
2447
+ left: `calc(100% + ${sideOffsetValue}px) ${transformY}px`,
2448
+ right: `${-sideOffsetValue}px ${transformY}px`
2449
+ }[currentRenderedSide];
2450
+ const overlapTransformOrigin = `${transformX}px ${rects.reference.y + halfAnchorHeight - y}px`;
2451
+ elements.floating.style.setProperty('--transform-origin', crossAxisShiftEnabled && currentRenderedAxis === 'y' && isOverlappingAnchor ? overlapTransformOrigin : adjacentTransformOrigin);
2452
+ return {};
2453
+ }
2454
+ }, hide, adaptiveOrigin);
2455
+ useIsoLayoutEffect(() => {
2456
+ // Ensure positioning doesn't run initially for `keepMounted` elements that
2457
+ // aren't initially open.
2458
+ if (!mounted && floatingRootContext) {
2459
+ floatingRootContext.update({
2460
+ referenceElement: null,
2461
+ floatingElement: null,
2462
+ domReferenceElement: null
2463
+ });
2464
+ }
2465
+ }, [mounted, floatingRootContext]);
2466
+ const autoUpdateOptions = React.useMemo(() => ({
2467
+ elementResize: !disableAnchorTracking && typeof ResizeObserver !== 'undefined',
2468
+ layoutShift: !disableAnchorTracking && typeof IntersectionObserver !== 'undefined'
2469
+ }), [disableAnchorTracking]);
2470
+ const {
2471
+ refs,
2472
+ elements,
2473
+ x,
2474
+ y,
2475
+ middlewareData,
2476
+ update,
2477
+ placement: renderedPlacement,
2478
+ context,
2479
+ isPositioned,
2480
+ floatingStyles: originalFloatingStyles
2481
+ } = useFloating({
2482
+ rootContext: floatingRootContext,
2483
+ placement,
2484
+ middleware,
2485
+ strategy: positionMethod,
2486
+ whileElementsMounted: keepMounted ? undefined : (...args) => autoUpdate(...args, autoUpdateOptions),
2487
+ nodeId,
2488
+ externalTree
2538
2489
  });
2539
- });
2540
- if (process.env.NODE_ENV !== "production") MenuLinkItem.displayName = "MenuLinkItem";
2490
+ const {
2491
+ sideX,
2492
+ sideY
2493
+ } = middlewareData.adaptiveOrigin || DEFAULT_SIDES;
2494
+
2495
+ // Default to `fixed` when not positioned to prevent `autoFocus` scroll jumps.
2496
+ // This ensures the popup is inside the viewport initially before it gets positioned.
2497
+ const resolvedPosition = isPositioned ? positionMethod : 'fixed';
2498
+ const floatingStyles = React.useMemo(() => {
2499
+ const base = adaptiveOrigin ? {
2500
+ position: resolvedPosition,
2501
+ [sideX]: x,
2502
+ [sideY]: y
2503
+ } : {
2504
+ position: resolvedPosition,
2505
+ ...originalFloatingStyles
2506
+ };
2507
+ if (!isPositioned) {
2508
+ base.opacity = 0;
2509
+ }
2510
+ return base;
2511
+ }, [adaptiveOrigin, resolvedPosition, sideX, x, sideY, y, originalFloatingStyles, isPositioned]);
2512
+ const registeredPositionReferenceRef = React.useRef(null);
2513
+ useIsoLayoutEffect(() => {
2514
+ if (!mounted) {
2515
+ return;
2516
+ }
2517
+ const anchorValue = anchorValueRef.current;
2518
+ const resolvedAnchor = typeof anchorValue === 'function' ? anchorValue() : anchorValue;
2519
+ const unwrappedElement = (isRef(resolvedAnchor) ? resolvedAnchor.current : resolvedAnchor) || null;
2520
+ const finalAnchor = unwrappedElement || null;
2521
+ if (finalAnchor !== registeredPositionReferenceRef.current) {
2522
+ refs.setPositionReference(finalAnchor);
2523
+ registeredPositionReferenceRef.current = finalAnchor;
2524
+ }
2525
+ }, [mounted, refs, anchorDep, anchorValueRef]);
2526
+ React.useEffect(() => {
2527
+ if (!mounted) {
2528
+ return;
2529
+ }
2530
+ const anchorValue = anchorValueRef.current;
2541
2531
 
2542
- const ToolbarRootContext = /*#__PURE__*/React.createContext(undefined);
2543
- if (process.env.NODE_ENV !== "production") ToolbarRootContext.displayName = "ToolbarRootContext";
2544
- function useToolbarRootContext(optional) {
2545
- const context = React.useContext(ToolbarRootContext);
2546
- if (context === undefined && !optional) {
2547
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: ToolbarRootContext is missing. Toolbar parts must be placed within <Toolbar.Root>.' : formatErrorMessage(69));
2548
- }
2549
- return context;
2532
+ // Refs from parent components are set after useLayoutEffect runs and are available in useEffect.
2533
+ // Therefore, if the anchor is a ref, we need to update the position reference in useEffect.
2534
+ if (typeof anchorValue === 'function') {
2535
+ return;
2536
+ }
2537
+ if (isRef(anchorValue) && anchorValue.current !== registeredPositionReferenceRef.current) {
2538
+ refs.setPositionReference(anchorValue.current);
2539
+ registeredPositionReferenceRef.current = anchorValue.current;
2540
+ }
2541
+ }, [mounted, refs, anchorDep, anchorValueRef]);
2542
+ React.useEffect(() => {
2543
+ if (keepMounted && mounted && elements.domReference && elements.floating) {
2544
+ return autoUpdate(elements.domReference, elements.floating, update, autoUpdateOptions);
2545
+ }
2546
+ return undefined;
2547
+ }, [keepMounted, mounted, elements, update, autoUpdateOptions]);
2548
+ const renderedSide = getSide(renderedPlacement);
2549
+ const logicalRenderedSide = getLogicalSide(sideParam, renderedSide, isRtl);
2550
+ const renderedAlign = getAlignment(renderedPlacement) || 'center';
2551
+ const anchorHidden = Boolean(middlewareData.hide?.referenceHidden);
2552
+
2553
+ /**
2554
+ * Locks the flip (makes it "sticky") so it doesn't prefer a given placement
2555
+ * and flips back lazily, not eagerly. Ideal for filtered lists that change
2556
+ * the size of the popup dynamically to avoid unwanted flipping when typing.
2557
+ */
2558
+ useIsoLayoutEffect(() => {
2559
+ if (lazyFlip && mounted && isPositioned) {
2560
+ setMountSide(renderedSide);
2561
+ }
2562
+ }, [lazyFlip, mounted, isPositioned, renderedSide]);
2563
+ const arrowStyles = React.useMemo(() => ({
2564
+ position: 'absolute',
2565
+ top: middlewareData.arrow?.y,
2566
+ left: middlewareData.arrow?.x
2567
+ }), [middlewareData.arrow]);
2568
+ const arrowUncentered = middlewareData.arrow?.centerOffset !== 0;
2569
+ return React.useMemo(() => ({
2570
+ positionerStyles: floatingStyles,
2571
+ arrowStyles,
2572
+ arrowRef,
2573
+ arrowUncentered,
2574
+ side: logicalRenderedSide,
2575
+ align: renderedAlign,
2576
+ physicalSide: renderedSide,
2577
+ anchorHidden,
2578
+ refs,
2579
+ context,
2580
+ isPositioned,
2581
+ update
2582
+ }), [floatingStyles, arrowStyles, arrowRef, arrowUncentered, logicalRenderedSide, renderedAlign, renderedSide, anchorHidden, refs, context, isPositioned, update]);
2583
+ }
2584
+ function isRef(param) {
2585
+ return param != null && 'current' in param;
2550
2586
  }
2551
2587
 
2552
2588
  function getDisabledMountTransitionStyles(transitionStatus) {
2553
2589
  return transitionStatus === 'starting' ? DISABLED_TRANSITIONS_STYLE : EMPTY_OBJECT;
2554
2590
  }
2555
2591
 
2556
- const stateAttributesMapping = {
2557
- ...popupStateMapping,
2558
- ...transitionStatusMapping
2559
- };
2592
+ let IndexGuessBehavior = /*#__PURE__*/function (IndexGuessBehavior) {
2593
+ IndexGuessBehavior[IndexGuessBehavior["None"] = 0] = "None";
2594
+ IndexGuessBehavior[IndexGuessBehavior["GuessFromOrder"] = 1] = "GuessFromOrder";
2595
+ return IndexGuessBehavior;
2596
+ }({});
2560
2597
 
2561
2598
  /**
2562
- * A container for the menu items.
2563
- * Renders a `<div>` element.
2564
- *
2565
- * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu)
2599
+ * Used to register a list item and its index (DOM position) in the `CompositeList`.
2566
2600
  */
2567
- const MenuPopup = /*#__PURE__*/React.forwardRef(function MenuPopup(componentProps, forwardedRef) {
2568
- const {
2569
- render,
2570
- className,
2571
- finalFocus,
2572
- ...elementProps
2573
- } = componentProps;
2601
+ function useCompositeListItem(params = {}) {
2574
2602
  const {
2575
- store
2576
- } = useMenuRootContext();
2603
+ label,
2604
+ metadata,
2605
+ textRef,
2606
+ indexGuessBehavior,
2607
+ index: externalIndex
2608
+ } = params;
2577
2609
  const {
2578
- side,
2579
- align
2580
- } = useMenuPositionerContext();
2581
- const insideToolbar = useToolbarRootContext(true) != null;
2582
- const open = store.useState('open');
2583
- const transitionStatus = store.useState('transitionStatus');
2584
- const popupProps = store.useState('popupProps');
2585
- const mounted = store.useState('mounted');
2586
- const instantType = store.useState('instantType');
2587
- const triggerElement = store.useState('activeTriggerElement');
2588
- const parent = store.useState('parent');
2589
- const lastOpenChangeReason = store.useState('lastOpenChangeReason');
2590
- const rootId = store.useState('rootId');
2591
- const floatingContext = store.useState('floatingRootContext');
2592
- const floatingTreeRoot = store.useState('floatingTreeRoot');
2593
- const closeDelay = store.useState('closeDelay');
2594
- const activeTriggerElement = store.useState('activeTriggerElement');
2595
- const isContextMenu = parent.type === 'context-menu';
2596
- useOpenChangeComplete({
2597
- open,
2598
- ref: store.context.popupRef,
2599
- onComplete() {
2600
- if (open) {
2601
- store.context.onOpenChangeComplete?.(true);
2610
+ register,
2611
+ unregister,
2612
+ subscribeMapChange,
2613
+ elementsRef,
2614
+ labelsRef,
2615
+ nextIndexRef
2616
+ } = useCompositeListContext();
2617
+ const indexRef = React.useRef(-1);
2618
+ const [index, setIndex] = React.useState(externalIndex ?? (indexGuessBehavior === IndexGuessBehavior.GuessFromOrder ? () => {
2619
+ if (indexRef.current === -1) {
2620
+ const newIndex = nextIndexRef.current;
2621
+ nextIndexRef.current += 1;
2622
+ indexRef.current = newIndex;
2623
+ }
2624
+ return indexRef.current;
2625
+ } : -1));
2626
+ const componentRef = React.useRef(null);
2627
+ const ref = React.useCallback(node => {
2628
+ componentRef.current = node;
2629
+ if (index !== -1 && node !== null) {
2630
+ elementsRef.current[index] = node;
2631
+ if (labelsRef) {
2632
+ const isLabelDefined = label !== undefined;
2633
+ labelsRef.current[index] = isLabelDefined ? label : textRef?.current?.textContent ?? node.textContent;
2602
2634
  }
2603
2635
  }
2604
- });
2605
- React.useEffect(() => {
2606
- function handleClose(event) {
2607
- store.setOpen(false, createChangeEventDetails(event.reason, event.domEvent));
2636
+ }, [index, elementsRef, labelsRef, label, textRef]);
2637
+ useIsoLayoutEffect(() => {
2638
+ if (externalIndex != null) {
2639
+ return undefined;
2608
2640
  }
2609
- floatingTreeRoot.events.on('close', handleClose);
2610
- return () => {
2611
- floatingTreeRoot.events.off('close', handleClose);
2612
- };
2613
- }, [floatingTreeRoot.events, store]);
2614
- const hoverEnabled = store.useState('hoverEnabled');
2615
- const disabled = store.useState('disabled');
2616
- useHoverFloatingInteraction(floatingContext, {
2617
- enabled: hoverEnabled && !disabled && !isContextMenu && parent.type !== 'menubar',
2618
- closeDelay
2619
- });
2620
- const state = {
2621
- transitionStatus,
2622
- side,
2623
- align,
2624
- open,
2625
- nested: parent.type === 'menu',
2626
- instant: instantType
2627
- };
2628
- const setPopupElement = React.useCallback(element => {
2629
- store.set('popupElement', element);
2630
- }, [store]);
2631
- const element = useRenderElement('div', componentProps, {
2632
- state,
2633
- ref: [forwardedRef, store.context.popupRef, setPopupElement],
2634
- stateAttributesMapping,
2635
- props: [popupProps, {
2636
- onKeyDown(event) {
2637
- if (insideToolbar && COMPOSITE_KEYS.has(event.key)) {
2638
- event.stopPropagation();
2639
- }
2641
+ const node = componentRef.current;
2642
+ if (node) {
2643
+ register(node, metadata);
2644
+ return () => {
2645
+ unregister(node);
2646
+ };
2647
+ }
2648
+ return undefined;
2649
+ }, [externalIndex, register, unregister, metadata]);
2650
+ useIsoLayoutEffect(() => {
2651
+ if (externalIndex != null) {
2652
+ return undefined;
2653
+ }
2654
+ return subscribeMapChange(map => {
2655
+ const i = componentRef.current ? map.get(componentRef.current)?.index : null;
2656
+ if (i != null) {
2657
+ setIndex(i);
2640
2658
  }
2641
- }, getDisabledMountTransitionStyles(transitionStatus), elementProps, {
2642
- 'data-rootownerid': rootId
2643
- }]
2644
- });
2645
- let returnFocus = parent.type === undefined || isContextMenu;
2646
- if (triggerElement || parent.type === 'menubar' && lastOpenChangeReason !== outsidePress) {
2647
- returnFocus = true;
2648
- }
2649
- return /*#__PURE__*/jsx(FloatingFocusManager, {
2650
- context: floatingContext,
2651
- modal: isContextMenu,
2652
- disabled: !mounted,
2653
- returnFocus: finalFocus === undefined ? returnFocus : finalFocus,
2654
- initialFocus: parent.type !== 'menu',
2655
- restoreFocus: true,
2656
- externalTree: parent.type !== 'menubar' ? floatingTreeRoot : undefined,
2657
- previousFocusableElement: activeTriggerElement,
2658
- nextFocusableElement: parent.type === undefined ? store.context.triggerFocusTargetRef : undefined,
2659
- beforeContentFocusGuardRef: parent.type === undefined ? store.context.beforeContentFocusGuardRef : undefined,
2660
- children: element
2661
- });
2662
- });
2663
- if (process.env.NODE_ENV !== "production") MenuPopup.displayName = "MenuPopup";
2664
-
2665
- const MenuPortalContext = /*#__PURE__*/React.createContext(undefined);
2666
- if (process.env.NODE_ENV !== "production") MenuPortalContext.displayName = "MenuPortalContext";
2667
- function useMenuPortalContext() {
2668
- const value = React.useContext(MenuPortalContext);
2669
- if (value === undefined) {
2670
- throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: <Menu.Portal> is missing.' : formatErrorMessage(32));
2671
- }
2672
- return value;
2659
+ });
2660
+ }, [externalIndex, subscribeMapChange, setIndex]);
2661
+ return React.useMemo(() => ({
2662
+ ref,
2663
+ index
2664
+ }), [index, ref]);
2673
2665
  }
2674
2666
 
2675
- const MenuPortal = /*#__PURE__*/React.forwardRef(function MenuPortal(props, forwardedRef) {
2667
+ /**
2668
+ * A button that closes the dialog.
2669
+ * Renders a `<button>` element.
2670
+ *
2671
+ * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
2672
+ */
2673
+ const DialogClose = /*#__PURE__*/React.forwardRef(function DialogClose(componentProps, forwardedRef) {
2676
2674
  const {
2677
- keepMounted = false,
2678
- ...portalProps
2679
- } = props;
2675
+ render,
2676
+ className,
2677
+ disabled = false,
2678
+ nativeButton = true,
2679
+ ...elementProps
2680
+ } = componentProps;
2680
2681
  const {
2681
2682
  store
2682
- } = useMenuRootContext();
2683
- const mounted = store.useState('mounted');
2684
- const shouldRender = mounted || keepMounted;
2685
- if (!shouldRender) {
2686
- return null;
2683
+ } = useDialogRootContext();
2684
+ const open = store.useState('open');
2685
+ function handleClick(event) {
2686
+ if (open) {
2687
+ store.setOpen(false, createChangeEventDetails(closePress, event.nativeEvent));
2688
+ }
2687
2689
  }
2688
- return /*#__PURE__*/jsx(MenuPortalContext.Provider, {
2689
- value: keepMounted,
2690
- children: /*#__PURE__*/jsx(FloatingPortal, {
2691
- ref: forwardedRef,
2692
- ...portalProps
2693
- })
2690
+ const {
2691
+ getButtonProps,
2692
+ buttonRef
2693
+ } = useButton({
2694
+ disabled,
2695
+ native: nativeButton
2696
+ });
2697
+ const state = {
2698
+ disabled
2699
+ };
2700
+ return useRenderElement('button', componentProps, {
2701
+ state,
2702
+ ref: [forwardedRef, buttonRef],
2703
+ props: [{
2704
+ onClick: handleClick
2705
+ }, elementProps, getButtonProps]
2694
2706
  });
2695
2707
  });
2696
- if (process.env.NODE_ENV !== "production") MenuPortal.displayName = "MenuPortal";
2697
-
2698
- /**
2699
- * @internal
2700
- */
2701
- const DirectionContext = /*#__PURE__*/React.createContext(undefined);
2702
- if (process.env.NODE_ENV !== "production") DirectionContext.displayName = "DirectionContext";
2703
- function useDirection() {
2704
- const context = React.useContext(DirectionContext);
2705
- return context?.direction ?? 'ltr';
2706
- }
2708
+ if (process.env.NODE_ENV !== "production") DialogClose.displayName = "DialogClose";
2707
2709
 
2708
2710
  /**
2709
- * Fork of the original `arrow` middleware from Floating UI that allows
2710
- * configuring the offset parent.
2711
+ * A button that closes the drawer.
2712
+ * Renders a `<button>` element.
2713
+ *
2714
+ * Documentation: [Base UI Drawer](https://base-ui.com/react/components/drawer)
2711
2715
  */
2712
- const baseArrow = options => ({
2713
- name: 'arrow',
2714
- options,
2715
- async fn(state) {
2716
- const {
2717
- x,
2718
- y,
2719
- placement,
2720
- rects,
2721
- platform,
2722
- elements,
2723
- middlewareData
2724
- } = state;
2725
- // Since `element` is required, we don't Partial<> the type.
2726
- const {
2727
- element,
2728
- padding = 0,
2729
- offsetParent = 'real'
2730
- } = evaluate(options, state) || {};
2731
- if (element == null) {
2732
- return {};
2733
- }
2734
- const paddingObject = getPaddingObject(padding);
2735
- const coords = {
2736
- x,
2737
- y
2738
- };
2739
- const axis = getAlignmentAxis(placement);
2740
- const length = getAxisLength(axis);
2741
- const arrowDimensions = await platform.getDimensions(element);
2742
- const isYAxis = axis === 'y';
2743
- const minProp = isYAxis ? 'top' : 'left';
2744
- const maxProp = isYAxis ? 'bottom' : 'right';
2745
- const clientProp = isYAxis ? 'clientHeight' : 'clientWidth';
2746
- const endDiff = rects.reference[length] + rects.reference[axis] - coords[axis] - rects.floating[length];
2747
- const startDiff = coords[axis] - rects.reference[axis];
2748
- const arrowOffsetParent = offsetParent === 'real' ? await platform.getOffsetParent?.(element) : elements.floating;
2749
- let clientSize = elements.floating[clientProp] || rects.floating[length];
2750
-
2751
- // DOM platform can return `window` as the `offsetParent`.
2752
- if (!clientSize || !(await platform.isElement?.(arrowOffsetParent))) {
2753
- clientSize = elements.floating[clientProp] || rects.floating[length];
2754
- }
2755
- const centerToReference = endDiff / 2 - startDiff / 2;
2756
-
2757
- // If the padding is large enough that it causes the arrow to no longer be
2758
- // centered, modify the padding so that it is centered.
2759
- const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1;
2760
- const minPadding = Math.min(paddingObject[minProp], largestPossiblePadding);
2761
- const maxPadding = Math.min(paddingObject[maxProp], largestPossiblePadding);
2762
-
2763
- // Make sure the arrow doesn't overflow the floating element if the center
2764
- // point is outside the floating element's bounds.
2765
- const min = minPadding;
2766
- const max = clientSize - arrowDimensions[length] - maxPadding;
2767
- const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference;
2768
- const offset = clamp(min, center, max);
2769
-
2770
- // If the reference is small enough that the arrow's padding causes it to
2771
- // to point to nothing for an aligned placement, adjust the offset of the
2772
- // floating element itself. To ensure `shift()` continues to take action,
2773
- // a single reset is performed when this is true.
2774
- const shouldAddOffset = !middlewareData.arrow && getAlignment(placement) != null && center !== offset && rects.reference[length] / 2 - (center < min ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0;
2775
- // eslint-disable-next-line no-nested-ternary
2776
- const alignmentOffset = shouldAddOffset ? center < min ? center - min : center - max : 0;
2777
- return {
2778
- [axis]: coords[axis] + alignmentOffset,
2779
- data: {
2780
- [axis]: offset,
2781
- centerOffset: center - offset - alignmentOffset,
2782
- ...(shouldAddOffset && {
2783
- alignmentOffset
2784
- })
2785
- },
2786
- reset: shouldAddOffset
2787
- };
2788
- }
2789
- });
2716
+ const DrawerClose = DialogClose;
2790
2717
 
2791
2718
  /**
2792
- * Provides data to position an inner element of the floating element so that it
2793
- * appears centered to the reference element.
2794
- * This wraps the core `arrow` middleware to allow React refs as the element.
2795
- * @see https://floating-ui.com/docs/arrow
2719
+ * A heading that labels the dialog.
2720
+ * Renders an `<h2>` element.
2721
+ *
2722
+ * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
2796
2723
  */
2797
- const arrow = (options, deps) => ({
2798
- ...baseArrow(options),
2799
- options: [options, deps]
2800
- });
2801
-
2802
- const hide = {
2803
- name: 'hide',
2804
- async fn(state) {
2805
- const {
2806
- width,
2807
- height,
2808
- x,
2809
- y
2810
- } = state.rects.reference;
2811
- const anchorHidden = width === 0 && height === 0 && x === 0 && y === 0;
2812
- const nativeHideResult = await hide$1().fn(state);
2813
- return {
2814
- data: {
2815
- referenceHidden: nativeHideResult.data?.referenceHidden || anchorHidden
2816
- }
2817
- };
2818
- }
2819
- };
2820
-
2821
- const DEFAULT_SIDES = {
2822
- sideX: 'left',
2823
- sideY: 'top'
2824
- };
2825
- const adaptiveOrigin = {
2826
- name: 'adaptiveOrigin',
2827
- async fn(state) {
2828
- const {
2829
- x: rawX,
2830
- y: rawY,
2831
- rects: {
2832
- floating: floatRect
2833
- },
2834
- elements: {
2835
- floating
2836
- },
2837
- platform,
2838
- strategy,
2839
- placement
2840
- } = state;
2841
- const win = getWindow(floating);
2842
- const styles = win.getComputedStyle(floating);
2843
- const hasTransition = styles.transitionDuration !== '0s' && styles.transitionDuration !== '';
2844
- if (!hasTransition) {
2845
- return {
2846
- x: rawX,
2847
- y: rawY,
2848
- data: DEFAULT_SIDES
2849
- };
2850
- }
2851
- const offsetParent = await platform.getOffsetParent?.(floating);
2852
- let offsetDimensions = {
2853
- width: 0,
2854
- height: 0
2855
- };
2856
-
2857
- // For fixed strategy, prefer visualViewport if available
2858
- if (strategy === 'fixed' && win?.visualViewport) {
2859
- offsetDimensions = {
2860
- width: win.visualViewport.width,
2861
- height: win.visualViewport.height
2862
- };
2863
- } else if (offsetParent === win) {
2864
- const doc = ownerDocument(floating);
2865
- offsetDimensions = {
2866
- width: doc.documentElement.clientWidth,
2867
- height: doc.documentElement.clientHeight
2868
- };
2869
- } else if (await platform.isElement?.(offsetParent)) {
2870
- offsetDimensions = await platform.getDimensions(offsetParent);
2871
- }
2872
- const currentSide = getSide(placement);
2873
- let x = rawX;
2874
- let y = rawY;
2875
- if (currentSide === 'left') {
2876
- x = offsetDimensions.width - (rawX + floatRect.width);
2877
- }
2878
- if (currentSide === 'top') {
2879
- y = offsetDimensions.height - (rawY + floatRect.height);
2880
- }
2881
- const sideX = currentSide === 'left' ? 'right' : DEFAULT_SIDES.sideX;
2882
- const sideY = currentSide === 'top' ? 'bottom' : DEFAULT_SIDES.sideY;
2883
- return {
2884
- x,
2885
- y,
2886
- data: {
2887
- sideX,
2888
- sideY
2889
- }
2890
- };
2724
+ const DialogTitle = /*#__PURE__*/React.forwardRef(function DialogTitle(componentProps, forwardedRef) {
2725
+ const {
2726
+ render,
2727
+ className,
2728
+ id: idProp,
2729
+ ...elementProps
2730
+ } = componentProps;
2731
+ const {
2732
+ store
2733
+ } = useDialogRootContext();
2734
+ const id = useBaseUiId(idProp);
2735
+ store.useSyncedValueWithCleanup('titleElementId', id);
2736
+ return useRenderElement('h2', componentProps, {
2737
+ ref: forwardedRef,
2738
+ props: [{
2739
+ id
2740
+ }, elementProps]
2741
+ });
2742
+ });
2743
+ if (process.env.NODE_ENV !== "production") DialogTitle.displayName = "DialogTitle";
2744
+
2745
+ /**
2746
+ * A heading that labels the drawer.
2747
+ * Renders an `<h2>` element.
2748
+ *
2749
+ * Documentation: [Base UI Drawer](https://base-ui.com/react/components/drawer)
2750
+ */
2751
+ const DrawerTitle = DialogTitle;
2752
+
2753
+ const MenuPositionerContext = /*#__PURE__*/React.createContext(undefined);
2754
+ if (process.env.NODE_ENV !== "production") MenuPositionerContext.displayName = "MenuPositionerContext";
2755
+ function useMenuPositionerContext(optional) {
2756
+ const context = React.useContext(MenuPositionerContext);
2757
+ if (context === undefined && !optional) {
2758
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: MenuPositionerContext is missing. MenuPositioner parts must be placed within <Menu.Positioner>.' : formatErrorMessage(33));
2891
2759
  }
2892
- };
2760
+ return context;
2761
+ }
2893
2762
 
2894
- function getLogicalSide(sideParam, renderedSide, isRtl) {
2895
- const isLogicalSideParam = sideParam === 'inline-start' || sideParam === 'inline-end';
2896
- const logicalRight = isRtl ? 'inline-start' : 'inline-end';
2897
- const logicalLeft = isRtl ? 'inline-end' : 'inline-start';
2898
- return {
2899
- top: 'top',
2900
- right: isLogicalSideParam ? logicalRight : 'right',
2901
- bottom: 'bottom',
2902
- left: isLogicalSideParam ? logicalLeft : 'left'
2903
- }[renderedSide];
2763
+ const MenuRootContext = /*#__PURE__*/React.createContext(undefined);
2764
+ if (process.env.NODE_ENV !== "production") MenuRootContext.displayName = "MenuRootContext";
2765
+ function useMenuRootContext(optional) {
2766
+ const context = React.useContext(MenuRootContext);
2767
+ if (context === undefined && !optional) {
2768
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: MenuRootContext is missing. Menu parts must be placed within <Menu.Root>.' : formatErrorMessage(36));
2769
+ }
2770
+ return context;
2904
2771
  }
2905
- function getOffsetData(state, sideParam, isRtl) {
2906
- const {
2907
- rects,
2908
- placement
2909
- } = state;
2910
- const data = {
2911
- side: getLogicalSide(sideParam, getSide(placement), isRtl),
2912
- align: getAlignment(placement) || 'center',
2913
- anchor: {
2914
- width: rects.reference.width,
2915
- height: rects.reference.height
2916
- },
2917
- positioner: {
2918
- width: rects.floating.width,
2919
- height: rects.floating.height
2920
- }
2921
- };
2922
- return data;
2772
+
2773
+ const ContextMenuRootContext = /*#__PURE__*/React.createContext(undefined);
2774
+ if (process.env.NODE_ENV !== "production") ContextMenuRootContext.displayName = "ContextMenuRootContext";
2775
+ function useContextMenuRootContext(optional = true) {
2776
+ const context = React.useContext(ContextMenuRootContext);
2777
+ if (context === undefined && !optional) {
2778
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: ContextMenuRootContext is missing. ContextMenu parts must be placed within <ContextMenu.Root>.' : formatErrorMessage(25));
2779
+ }
2780
+ return context;
2923
2781
  }
2782
+
2924
2783
  /**
2925
- * Provides standardized anchor positioning behavior for floating elements. Wraps Floating UI's
2926
- * `useFloating` hook.
2784
+ * Returns common props shared by all menu item types.
2785
+ * This hook extracts the shared logic for id, role, tabIndex, onKeyDown,
2786
+ * onMouseMove, onClick, and onMouseUp handlers.
2927
2787
  */
2928
- function useAnchorPositioning(params) {
2788
+ function useMenuItemCommonProps(params) {
2929
2789
  const {
2930
- // Public parameters
2931
- anchor,
2932
- positionMethod = 'absolute',
2933
- side: sideParam = 'bottom',
2934
- sideOffset = 0,
2935
- align = 'center',
2936
- alignOffset = 0,
2937
- collisionBoundary,
2938
- collisionPadding: collisionPaddingParam = 5,
2939
- sticky = false,
2940
- arrowPadding = 5,
2941
- disableAnchorTracking = false,
2942
- // Private parameters
2943
- keepMounted = false,
2944
- floatingRootContext,
2945
- mounted,
2946
- collisionAvoidance,
2947
- shiftCrossAxis = false,
2790
+ closeOnClick,
2791
+ highlighted,
2792
+ id,
2948
2793
  nodeId,
2949
- adaptiveOrigin,
2950
- lazyFlip = false,
2951
- externalTree
2794
+ store,
2795
+ typingRef,
2796
+ itemRef,
2797
+ itemMetadata
2952
2798
  } = params;
2953
- const [mountSide, setMountSide] = React.useState(null);
2954
- if (!mounted && mountSide !== null) {
2955
- setMountSide(null);
2956
- }
2957
- const collisionAvoidanceSide = collisionAvoidance.side || 'flip';
2958
- const collisionAvoidanceAlign = collisionAvoidance.align || 'flip';
2959
- const collisionAvoidanceFallbackAxisSide = collisionAvoidance.fallbackAxisSide || 'end';
2960
- const anchorFn = typeof anchor === 'function' ? anchor : undefined;
2961
- const anchorFnCallback = useStableCallback(anchorFn);
2962
- const anchorDep = anchorFn ? anchorFnCallback : anchor;
2963
- const anchorValueRef = useValueAsRef(anchor);
2964
- const direction = useDirection();
2965
- const isRtl = direction === 'rtl';
2966
- const side = mountSide || {
2967
- top: 'top',
2968
- right: 'right',
2969
- bottom: 'bottom',
2970
- left: 'left',
2971
- 'inline-end': isRtl ? 'left' : 'right',
2972
- 'inline-start': isRtl ? 'right' : 'left'
2973
- }[sideParam];
2974
- const placement = align === 'center' ? side : `${side}-${align}`;
2975
- let collisionPadding = collisionPaddingParam;
2976
-
2977
- // Create a bias to the preferred side.
2978
- // On iOS, when the mobile software keyboard opens, the input is exactly centered
2979
- // in the viewport, but this can cause it to flip to the top undesirably.
2980
- const bias = 1;
2981
- const biasTop = sideParam === 'bottom' ? bias : 0;
2982
- const biasBottom = sideParam === 'top' ? bias : 0;
2983
- const biasLeft = sideParam === 'right' ? bias : 0;
2984
- const biasRight = sideParam === 'left' ? bias : 0;
2985
- if (typeof collisionPadding === 'number') {
2986
- collisionPadding = {
2987
- top: collisionPadding + biasTop,
2988
- right: collisionPadding + biasRight,
2989
- bottom: collisionPadding + biasBottom,
2990
- left: collisionPadding + biasLeft
2991
- };
2992
- } else if (collisionPadding) {
2993
- collisionPadding = {
2994
- top: (collisionPadding.top || 0) + biasTop,
2995
- right: (collisionPadding.right || 0) + biasRight,
2996
- bottom: (collisionPadding.bottom || 0) + biasBottom,
2997
- left: (collisionPadding.left || 0) + biasLeft
2998
- };
2999
- }
3000
- const commonCollisionProps = {
3001
- boundary: collisionBoundary === 'clipping-ancestors' ? 'clippingAncestors' : collisionBoundary,
3002
- padding: collisionPadding
3003
- };
3004
-
3005
- // Using a ref assumes that the arrow element is always present in the DOM for the lifetime of the
3006
- // popup. If this assumption ends up being false, we can switch to state to manage the arrow's
3007
- // presence.
3008
- const arrowRef = React.useRef(null);
3009
-
3010
- // Keep these reactive if they're not functions
3011
- const sideOffsetRef = useValueAsRef(sideOffset);
3012
- const alignOffsetRef = useValueAsRef(alignOffset);
3013
- const sideOffsetDep = typeof sideOffset !== 'function' ? sideOffset : 0;
3014
- const alignOffsetDep = typeof alignOffset !== 'function' ? alignOffset : 0;
3015
- const middleware = [offset(state => {
3016
- const data = getOffsetData(state, sideParam, isRtl);
3017
- const sideAxis = typeof sideOffsetRef.current === 'function' ? sideOffsetRef.current(data) : sideOffsetRef.current;
3018
- const alignAxis = typeof alignOffsetRef.current === 'function' ? alignOffsetRef.current(data) : alignOffsetRef.current;
3019
- return {
3020
- mainAxis: sideAxis,
3021
- crossAxis: alignAxis,
3022
- alignmentAxis: alignAxis
3023
- };
3024
- }, [sideOffsetDep, alignOffsetDep, isRtl, sideParam])];
3025
- const shiftDisabled = collisionAvoidanceAlign === 'none' && collisionAvoidanceSide !== 'shift';
3026
- const crossAxisShiftEnabled = !shiftDisabled && (sticky || shiftCrossAxis || collisionAvoidanceSide === 'shift');
3027
- const flipMiddleware = collisionAvoidanceSide === 'none' ? null : flip({
3028
- ...commonCollisionProps,
3029
- // Ensure the popup flips if it's been limited by its --available-height and it resizes.
3030
- // Since the size() padding is smaller than the flip() padding, flip() will take precedence.
3031
- padding: {
3032
- top: collisionPadding.top + bias,
3033
- right: collisionPadding.right + bias,
3034
- bottom: collisionPadding.bottom + bias,
3035
- left: collisionPadding.left + bias
2799
+ const {
2800
+ events: menuEvents
2801
+ } = store.useState('floatingTreeRoot');
2802
+ const contextMenuContext = useContextMenuRootContext(true);
2803
+ const isContextMenu = contextMenuContext !== undefined;
2804
+ return React.useMemo(() => ({
2805
+ id,
2806
+ role: 'menuitem',
2807
+ tabIndex: highlighted ? 0 : -1,
2808
+ onKeyDown(event) {
2809
+ if (event.key === ' ' && typingRef?.current) {
2810
+ event.preventDefault();
2811
+ }
3036
2812
  },
3037
- mainAxis: !shiftCrossAxis && collisionAvoidanceSide === 'flip',
3038
- crossAxis: collisionAvoidanceAlign === 'flip' ? 'alignment' : false,
3039
- fallbackAxisSideDirection: collisionAvoidanceFallbackAxisSide
3040
- });
3041
- const shiftMiddleware = shiftDisabled ? null : shift(data => {
3042
- const html = ownerDocument(data.elements.floating).documentElement;
3043
- return {
3044
- ...commonCollisionProps,
3045
- // Use the Layout Viewport to avoid shifting around when pinch-zooming
3046
- // for context menus.
3047
- rootBoundary: shiftCrossAxis ? {
3048
- x: 0,
3049
- y: 0,
3050
- width: html.clientWidth,
3051
- height: html.clientHeight
3052
- } : undefined,
3053
- mainAxis: collisionAvoidanceAlign !== 'none',
3054
- crossAxis: crossAxisShiftEnabled,
3055
- limiter: sticky || shiftCrossAxis ? undefined : limitShift(limitData => {
3056
- if (!arrowRef.current) {
3057
- return {};
3058
- }
3059
- const {
3060
- width,
3061
- height
3062
- } = arrowRef.current.getBoundingClientRect();
3063
- const sideAxis = getSideAxis(getSide(limitData.placement));
3064
- const arrowSize = sideAxis === 'y' ? width : height;
3065
- const offsetAmount = sideAxis === 'y' ? collisionPadding.left + collisionPadding.right : collisionPadding.top + collisionPadding.bottom;
3066
- return {
3067
- offset: arrowSize / 2 + offsetAmount / 2
3068
- };
3069
- })
3070
- };
3071
- }, [commonCollisionProps, sticky, shiftCrossAxis, collisionPadding, collisionAvoidanceAlign]);
3072
-
3073
- // https://floating-ui.com/docs/flip#combining-with-shift
3074
- if (collisionAvoidanceSide === 'shift' || collisionAvoidanceAlign === 'shift' || align === 'center') {
3075
- middleware.push(shiftMiddleware, flipMiddleware);
3076
- } else {
3077
- middleware.push(flipMiddleware, shiftMiddleware);
3078
- }
3079
- middleware.push(size({
3080
- ...commonCollisionProps,
3081
- apply({
3082
- elements: {
3083
- floating
3084
- },
3085
- availableWidth,
3086
- availableHeight,
3087
- rects
3088
- }) {
3089
- const floatingStyle = floating.style;
3090
- floatingStyle.setProperty('--available-width', `${availableWidth}px`);
3091
- floatingStyle.setProperty('--available-height', `${availableHeight}px`);
2813
+ onMouseMove(event) {
2814
+ if (!nodeId) {
2815
+ return;
2816
+ }
3092
2817
 
3093
- // Snap anchor dimensions to device pixels to ensure the popup's visual width matches the anchor's one.
3094
- const dpr = window.devicePixelRatio || 1;
3095
- const {
3096
- x,
3097
- y,
3098
- width,
3099
- height
3100
- } = rects.reference;
3101
- const anchorWidth = (Math.round((x + width) * dpr) - Math.round(x * dpr)) / dpr;
3102
- const anchorHeight = (Math.round((y + height) * dpr) - Math.round(y * dpr)) / dpr;
3103
- floatingStyle.setProperty('--anchor-width', `${anchorWidth}px`);
3104
- floatingStyle.setProperty('--anchor-height', `${anchorHeight}px`);
3105
- }
3106
- }), arrow(() => ({
3107
- // `transform-origin` calculations rely on an element existing. If the arrow hasn't been set,
3108
- // we'll create a fake element.
3109
- element: arrowRef.current || document.createElement('div'),
3110
- padding: arrowPadding,
3111
- offsetParent: 'floating'
3112
- }), [arrowPadding]), {
3113
- name: 'transformOrigin',
3114
- fn(state) {
3115
- const {
3116
- elements,
3117
- middlewareData,
3118
- placement: renderedPlacement,
3119
- rects,
3120
- y
3121
- } = state;
3122
- const currentRenderedSide = getSide(renderedPlacement);
3123
- const currentRenderedAxis = getSideAxis(currentRenderedSide);
3124
- const arrowEl = arrowRef.current;
3125
- const arrowX = middlewareData.arrow?.x || 0;
3126
- const arrowY = middlewareData.arrow?.y || 0;
3127
- const arrowWidth = arrowEl?.clientWidth || 0;
3128
- const arrowHeight = arrowEl?.clientHeight || 0;
3129
- const transformX = arrowX + arrowWidth / 2;
3130
- const transformY = arrowY + arrowHeight / 2;
3131
- const shiftY = Math.abs(middlewareData.shift?.y || 0);
3132
- const halfAnchorHeight = rects.reference.height / 2;
3133
- const sideOffsetValue = typeof sideOffset === 'function' ? sideOffset(getOffsetData(state, sideParam, isRtl)) : sideOffset;
3134
- const isOverlappingAnchor = shiftY > sideOffsetValue;
3135
- const adjacentTransformOrigin = {
3136
- top: `${transformX}px calc(100% + ${sideOffsetValue}px)`,
3137
- bottom: `${transformX}px ${-sideOffsetValue}px`,
3138
- left: `calc(100% + ${sideOffsetValue}px) ${transformY}px`,
3139
- right: `${-sideOffsetValue}px ${transformY}px`
3140
- }[currentRenderedSide];
3141
- const overlapTransformOrigin = `${transformX}px ${rects.reference.y + halfAnchorHeight - y}px`;
3142
- elements.floating.style.setProperty('--transform-origin', crossAxisShiftEnabled && currentRenderedAxis === 'y' && isOverlappingAnchor ? overlapTransformOrigin : adjacentTransformOrigin);
3143
- return {};
3144
- }
3145
- }, hide, adaptiveOrigin);
3146
- useIsoLayoutEffect(() => {
3147
- // Ensure positioning doesn't run initially for `keepMounted` elements that
3148
- // aren't initially open.
3149
- if (!mounted && floatingRootContext) {
3150
- floatingRootContext.update({
3151
- referenceElement: null,
3152
- floatingElement: null,
3153
- domReferenceElement: null
2818
+ // Inform the floating tree that a menu item within this menu was hovered/moved over
2819
+ // so unrelated descendant submenus can be closed.
2820
+ menuEvents.emit('itemhover', {
2821
+ nodeId,
2822
+ target: event.currentTarget
3154
2823
  });
2824
+ },
2825
+ onClick(event) {
2826
+ if (closeOnClick) {
2827
+ menuEvents.emit('close', {
2828
+ domEvent: event,
2829
+ reason: itemPress
2830
+ });
2831
+ }
2832
+ },
2833
+ onMouseUp(event) {
2834
+ if (contextMenuContext) {
2835
+ const initialCursorPoint = contextMenuContext.initialCursorPointRef.current;
2836
+ contextMenuContext.initialCursorPointRef.current = null;
2837
+ if (isContextMenu && initialCursorPoint && Math.abs(event.clientX - initialCursorPoint.x) <= 1 && Math.abs(event.clientY - initialCursorPoint.y) <= 1) {
2838
+ return;
2839
+ }
2840
+
2841
+ // On non-macOS platforms, this mouseup belongs to the right-click gesture
2842
+ // that opened the context menu, so it must not activate an item.
2843
+ if (isContextMenu && !isMac && event.button === 2) {
2844
+ return;
2845
+ }
2846
+ }
2847
+ if (itemRef.current && store.context.allowMouseUpTriggerRef.current && (!isContextMenu || event.button === 2)) {
2848
+ // This fires whenever the user clicks on the trigger, moves the cursor, and releases it over the item.
2849
+ // We trigger the click and override the `closeOnClick` preference to always close the menu.
2850
+ if (!itemMetadata || itemMetadata.type === 'regular-item') {
2851
+ itemRef.current.click();
2852
+ }
2853
+ }
3155
2854
  }
3156
- }, [mounted, floatingRootContext]);
3157
- const autoUpdateOptions = React.useMemo(() => ({
3158
- elementResize: !disableAnchorTracking && typeof ResizeObserver !== 'undefined',
3159
- layoutShift: !disableAnchorTracking && typeof IntersectionObserver !== 'undefined'
3160
- }), [disableAnchorTracking]);
2855
+ }), [closeOnClick, highlighted, id, menuEvents, nodeId, store, typingRef, itemRef, contextMenuContext, isContextMenu, itemMetadata]);
2856
+ }
2857
+
2858
+ const REGULAR_ITEM = {
2859
+ type: 'regular-item'
2860
+ };
2861
+ function useMenuItem(params) {
3161
2862
  const {
3162
- refs,
3163
- elements,
3164
- x,
3165
- y,
3166
- middlewareData,
3167
- update,
3168
- placement: renderedPlacement,
3169
- context,
3170
- isPositioned,
3171
- floatingStyles: originalFloatingStyles
3172
- } = useFloating({
3173
- rootContext: floatingRootContext,
3174
- placement,
3175
- middleware,
3176
- strategy: positionMethod,
3177
- whileElementsMounted: keepMounted ? undefined : (...args) => autoUpdate(...args, autoUpdateOptions),
2863
+ closeOnClick,
2864
+ disabled = false,
2865
+ highlighted,
2866
+ id,
2867
+ store,
2868
+ typingRef = store.context.typingRef,
2869
+ nativeButton,
2870
+ itemMetadata,
2871
+ nodeId
2872
+ } = params;
2873
+ const itemRef = React.useRef(null);
2874
+ const {
2875
+ getButtonProps,
2876
+ buttonRef
2877
+ } = useButton({
2878
+ disabled,
2879
+ focusableWhenDisabled: true,
2880
+ native: nativeButton,
2881
+ composite: true
2882
+ });
2883
+ const commonProps = useMenuItemCommonProps({
2884
+ closeOnClick,
2885
+ highlighted,
2886
+ id,
3178
2887
  nodeId,
3179
- externalTree
2888
+ store,
2889
+ typingRef,
2890
+ itemRef,
2891
+ itemMetadata
3180
2892
  });
3181
- const {
3182
- sideX,
3183
- sideY
3184
- } = middlewareData.adaptiveOrigin || DEFAULT_SIDES;
2893
+ const getItemProps = React.useCallback(externalProps => {
2894
+ return mergeProps(commonProps, {
2895
+ onMouseEnter() {
2896
+ if (itemMetadata.type !== 'submenu-trigger') {
2897
+ return;
2898
+ }
2899
+ itemMetadata.setActive();
2900
+ }
2901
+ }, externalProps, getButtonProps);
2902
+ }, [commonProps, getButtonProps, itemMetadata]);
2903
+ const mergedRef = useMergedRefs(itemRef, buttonRef);
2904
+ return React.useMemo(() => ({
2905
+ getItemProps,
2906
+ itemRef: mergedRef
2907
+ }), [getItemProps, mergedRef]);
2908
+ }
3185
2909
 
3186
- // Default to `fixed` when not positioned to prevent `autoFocus` scroll jumps.
3187
- // This ensures the popup is inside the viewport initially before it gets positioned.
3188
- const resolvedPosition = isPositioned ? positionMethod : 'fixed';
3189
- const floatingStyles = React.useMemo(() => {
3190
- const base = adaptiveOrigin ? {
3191
- position: resolvedPosition,
3192
- [sideX]: x,
3193
- [sideY]: y
3194
- } : {
3195
- position: resolvedPosition,
3196
- ...originalFloatingStyles
3197
- };
3198
- if (!isPositioned) {
3199
- base.opacity = 0;
3200
- }
3201
- return base;
3202
- }, [adaptiveOrigin, resolvedPosition, sideX, x, sideY, y, originalFloatingStyles, isPositioned]);
3203
- const registeredPositionReferenceRef = React.useRef(null);
3204
- useIsoLayoutEffect(() => {
3205
- if (!mounted) {
3206
- return;
3207
- }
3208
- const anchorValue = anchorValueRef.current;
3209
- const resolvedAnchor = typeof anchorValue === 'function' ? anchorValue() : anchorValue;
3210
- const unwrappedElement = (isRef(resolvedAnchor) ? resolvedAnchor.current : resolvedAnchor) || null;
3211
- const finalAnchor = unwrappedElement || null;
3212
- if (finalAnchor !== registeredPositionReferenceRef.current) {
3213
- refs.setPositionReference(finalAnchor);
3214
- registeredPositionReferenceRef.current = finalAnchor;
3215
- }
3216
- }, [mounted, refs, anchorDep, anchorValueRef]);
3217
- React.useEffect(() => {
3218
- if (!mounted) {
3219
- return;
3220
- }
3221
- const anchorValue = anchorValueRef.current;
2910
+ let MenuCheckboxItemDataAttributes = /*#__PURE__*/function (MenuCheckboxItemDataAttributes) {
2911
+ /**
2912
+ * Present when the menu checkbox item is checked.
2913
+ */
2914
+ MenuCheckboxItemDataAttributes["checked"] = "data-checked";
2915
+ /**
2916
+ * Present when the menu checkbox item is not checked.
2917
+ */
2918
+ MenuCheckboxItemDataAttributes["unchecked"] = "data-unchecked";
2919
+ /**
2920
+ * Present when the menu checkbox item is disabled.
2921
+ */
2922
+ MenuCheckboxItemDataAttributes["disabled"] = "data-disabled";
2923
+ /**
2924
+ * Present when the menu checkbox item is highlighted.
2925
+ */
2926
+ MenuCheckboxItemDataAttributes["highlighted"] = "data-highlighted";
2927
+ return MenuCheckboxItemDataAttributes;
2928
+ }({});
3222
2929
 
3223
- // Refs from parent components are set after useLayoutEffect runs and are available in useEffect.
3224
- // Therefore, if the anchor is a ref, we need to update the position reference in useEffect.
3225
- if (typeof anchorValue === 'function') {
3226
- return;
3227
- }
3228
- if (isRef(anchorValue) && anchorValue.current !== registeredPositionReferenceRef.current) {
3229
- refs.setPositionReference(anchorValue.current);
3230
- registeredPositionReferenceRef.current = anchorValue.current;
2930
+ const itemMapping = {
2931
+ checked(value) {
2932
+ if (value) {
2933
+ return {
2934
+ [MenuCheckboxItemDataAttributes.checked]: ''
2935
+ };
3231
2936
  }
3232
- }, [mounted, refs, anchorDep, anchorValueRef]);
3233
- React.useEffect(() => {
3234
- if (keepMounted && mounted && elements.domReference && elements.floating) {
3235
- return autoUpdate(elements.domReference, elements.floating, update, autoUpdateOptions);
2937
+ return {
2938
+ [MenuCheckboxItemDataAttributes.unchecked]: ''
2939
+ };
2940
+ },
2941
+ ...transitionStatusMapping
2942
+ };
2943
+
2944
+ const MenuGroupContext = /*#__PURE__*/React.createContext(undefined);
2945
+ if (process.env.NODE_ENV !== "production") MenuGroupContext.displayName = "MenuGroupContext";
2946
+ function useMenuGroupRootContext() {
2947
+ const context = React.useContext(MenuGroupContext);
2948
+ if (context === undefined) {
2949
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: MenuGroupRootContext is missing. Menu group parts must be used within <Menu.Group>.' : formatErrorMessage(31));
2950
+ }
2951
+ return context;
2952
+ }
2953
+
2954
+ const MenuGroup = /*#__PURE__*/React.forwardRef(function MenuGroup(componentProps, forwardedRef) {
2955
+ const {
2956
+ render,
2957
+ className,
2958
+ ...elementProps
2959
+ } = componentProps;
2960
+ const [labelId, setLabelId] = React.useState(undefined);
2961
+ const context = React.useMemo(() => ({
2962
+ setLabelId
2963
+ }), [setLabelId]);
2964
+ const element = useRenderElement('div', componentProps, {
2965
+ ref: forwardedRef,
2966
+ props: {
2967
+ role: 'group',
2968
+ 'aria-labelledby': labelId,
2969
+ ...elementProps
3236
2970
  }
3237
- return undefined;
3238
- }, [keepMounted, mounted, elements, update, autoUpdateOptions]);
3239
- const renderedSide = getSide(renderedPlacement);
3240
- const logicalRenderedSide = getLogicalSide(sideParam, renderedSide, isRtl);
3241
- const renderedAlign = getAlignment(renderedPlacement) || 'center';
3242
- const anchorHidden = Boolean(middlewareData.hide?.referenceHidden);
2971
+ });
2972
+ return /*#__PURE__*/jsx(MenuGroupContext.Provider, {
2973
+ value: context,
2974
+ children: element
2975
+ });
2976
+ });
2977
+ if (process.env.NODE_ENV !== "production") MenuGroup.displayName = "MenuGroup";
3243
2978
 
3244
- /**
3245
- * Locks the flip (makes it "sticky") so it doesn't prefer a given placement
3246
- * and flips back lazily, not eagerly. Ideal for filtered lists that change
3247
- * the size of the popup dynamically to avoid unwanted flipping when typing.
3248
- */
2979
+ /**
2980
+ * An accessible label that is automatically associated with its parent group.
2981
+ * Renders a `<div>` element.
2982
+ *
2983
+ * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu)
2984
+ */
2985
+ const MenuGroupLabel = /*#__PURE__*/React.forwardRef(function MenuGroupLabelComponent(componentProps, forwardedRef) {
2986
+ const {
2987
+ className,
2988
+ render,
2989
+ id: idProp,
2990
+ ...elementProps
2991
+ } = componentProps;
2992
+ const id = useBaseUiId(idProp);
2993
+ const {
2994
+ setLabelId
2995
+ } = useMenuGroupRootContext();
3249
2996
  useIsoLayoutEffect(() => {
3250
- if (lazyFlip && mounted && isPositioned) {
3251
- setMountSide(renderedSide);
2997
+ setLabelId(id);
2998
+ return () => {
2999
+ setLabelId(undefined);
3000
+ };
3001
+ }, [setLabelId, id]);
3002
+ return useRenderElement('div', componentProps, {
3003
+ ref: forwardedRef,
3004
+ props: {
3005
+ id,
3006
+ role: 'presentation',
3007
+ ...elementProps
3252
3008
  }
3253
- }, [lazyFlip, mounted, isPositioned, renderedSide]);
3254
- const arrowStyles = React.useMemo(() => ({
3255
- position: 'absolute',
3256
- top: middlewareData.arrow?.y,
3257
- left: middlewareData.arrow?.x
3258
- }), [middlewareData.arrow]);
3259
- const arrowUncentered = middlewareData.arrow?.centerOffset !== 0;
3260
- return React.useMemo(() => ({
3261
- positionerStyles: floatingStyles,
3262
- arrowStyles,
3263
- arrowRef,
3264
- arrowUncentered,
3265
- side: logicalRenderedSide,
3266
- align: renderedAlign,
3267
- physicalSide: renderedSide,
3268
- anchorHidden,
3269
- refs,
3270
- context,
3271
- isPositioned,
3272
- update
3273
- }), [floatingStyles, arrowStyles, arrowRef, arrowUncentered, logicalRenderedSide, renderedAlign, renderedSide, anchorHidden, refs, context, isPositioned, update]);
3274
- }
3275
- function isRef(param) {
3276
- return param != null && 'current' in param;
3277
- }
3009
+ });
3010
+ });
3011
+ if (process.env.NODE_ENV !== "production") MenuGroupLabel.displayName = "MenuGroupLabel";
3278
3012
 
3279
- /* eslint-disable no-bitwise */
3280
3013
  /**
3281
- * Provides context for a list of items in a composite component.
3282
- * @internal
3014
+ * An individual interactive item in the menu.
3015
+ * Renders a `<div>` element.
3016
+ *
3017
+ * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu)
3283
3018
  */
3284
- function CompositeList(props) {
3019
+ const MenuItem = /*#__PURE__*/React.forwardRef(function MenuItem(componentProps, forwardedRef) {
3285
3020
  const {
3286
- children,
3287
- elementsRef,
3288
- labelsRef,
3289
- onMapChange: onMapChangeProp
3290
- } = props;
3291
- const onMapChange = useStableCallback(onMapChangeProp);
3292
- const nextIndexRef = React.useRef(0);
3293
- const listeners = useRefWithInit(createListeners).current;
3294
-
3295
- // We use a stable `map` to avoid O(n^2) re-allocation costs for large lists.
3296
- // `mapTick` is our re-render trigger mechanism. We also need to update the
3297
- // elements and label refs, but there's a lot of async work going on and sometimes
3298
- // the effect that handles `onMapChange` gets called after those refs have been
3299
- // filled, and we don't want to lose those values by setting their lengths to `0`.
3300
- // We also need to have them at the proper length because floating-ui uses that
3301
- // information for list navigation.
3302
-
3303
- const map = useRefWithInit(createMap).current;
3304
- // `mapTick` uses a counter rather than objects for low precision-loss risk and better memory efficiency
3305
- const [mapTick, setMapTick] = React.useState(0);
3306
- const lastTickRef = React.useRef(mapTick);
3307
- const register = useStableCallback((node, metadata) => {
3308
- map.set(node, metadata ?? null);
3309
- lastTickRef.current += 1;
3310
- setMapTick(lastTickRef.current);
3021
+ render,
3022
+ className,
3023
+ id: idProp,
3024
+ label,
3025
+ nativeButton = false,
3026
+ disabled = false,
3027
+ closeOnClick = true,
3028
+ ...elementProps
3029
+ } = componentProps;
3030
+ const listItem = useCompositeListItem({
3031
+ label
3311
3032
  });
3312
- const unregister = useStableCallback(node => {
3313
- map.delete(node);
3314
- lastTickRef.current += 1;
3315
- setMapTick(lastTickRef.current);
3033
+ const menuPositionerContext = useMenuPositionerContext(true);
3034
+ const id = useBaseUiId(idProp);
3035
+ const {
3036
+ store
3037
+ } = useMenuRootContext();
3038
+ const highlighted = store.useState('isActive', listItem.index);
3039
+ const itemProps = store.useState('itemProps');
3040
+ const {
3041
+ getItemProps,
3042
+ itemRef
3043
+ } = useMenuItem({
3044
+ closeOnClick,
3045
+ disabled,
3046
+ highlighted,
3047
+ id,
3048
+ store,
3049
+ nativeButton,
3050
+ nodeId: menuPositionerContext?.nodeId,
3051
+ itemMetadata: REGULAR_ITEM
3316
3052
  });
3317
- const sortedMap = React.useMemo(() => {
3318
- const newMap = new Map();
3319
- // Filter out disconnected elements before sorting to avoid inconsistent
3320
- // compareDocumentPosition results when elements are detached from the DOM.
3321
- const sortedNodes = Array.from(map.keys()).filter(node => node.isConnected).sort(sortByDocumentPosition);
3322
- sortedNodes.forEach((node, index) => {
3323
- const metadata = map.get(node) ?? {};
3324
- newMap.set(node, {
3325
- ...metadata,
3326
- index
3327
- });
3328
- });
3329
- return newMap;
3330
- }, [map, mapTick]);
3331
- useIsoLayoutEffect(() => {
3332
- if (typeof MutationObserver !== 'function' || sortedMap.size === 0) {
3333
- return undefined;
3334
- }
3335
- const mutationObserver = new MutationObserver(entries => {
3336
- const diff = new Set();
3337
- const updateDiff = node => diff.has(node) ? diff.delete(node) : diff.add(node);
3338
- entries.forEach(entry => {
3339
- entry.removedNodes.forEach(updateDiff);
3340
- entry.addedNodes.forEach(updateDiff);
3341
- });
3342
- if (diff.size === 0) {
3343
- lastTickRef.current += 1;
3344
- setMapTick(lastTickRef.current);
3345
- }
3346
- });
3347
- sortedMap.forEach((_, node) => {
3348
- if (node.parentElement) {
3349
- mutationObserver.observe(node.parentElement, {
3350
- childList: true
3351
- });
3352
- }
3353
- });
3354
- return () => {
3355
- mutationObserver.disconnect();
3356
- };
3357
- }, [sortedMap]);
3358
- useIsoLayoutEffect(() => {
3359
- const shouldUpdateLengths = lastTickRef.current === mapTick;
3360
- if (shouldUpdateLengths) {
3361
- if (elementsRef.current.length !== sortedMap.size) {
3362
- elementsRef.current.length = sortedMap.size;
3363
- }
3364
- if (labelsRef && labelsRef.current.length !== sortedMap.size) {
3365
- labelsRef.current.length = sortedMap.size;
3053
+ const state = {
3054
+ disabled,
3055
+ highlighted
3056
+ };
3057
+ return useRenderElement('div', componentProps, {
3058
+ state,
3059
+ props: [itemProps, elementProps, getItemProps],
3060
+ ref: [itemRef, forwardedRef, listItem.ref]
3061
+ });
3062
+ });
3063
+ if (process.env.NODE_ENV !== "production") MenuItem.displayName = "MenuItem";
3064
+
3065
+ /**
3066
+ * A link in the menu that can be used to navigate to a different page or section.
3067
+ * Renders an `<a>` element.
3068
+ *
3069
+ * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu)
3070
+ */
3071
+ const MenuLinkItem = /*#__PURE__*/React.forwardRef(function MenuLinkItem(componentProps, forwardedRef) {
3072
+ const {
3073
+ render,
3074
+ className,
3075
+ id: idProp,
3076
+ label,
3077
+ closeOnClick = false,
3078
+ ...elementProps
3079
+ } = componentProps;
3080
+ const linkRef = React.useRef(null);
3081
+ const listItem = useCompositeListItem({
3082
+ label
3083
+ });
3084
+ const menuPositionerContext = useMenuPositionerContext(true);
3085
+ const nodeId = menuPositionerContext?.nodeId;
3086
+ const id = useBaseUiId(idProp);
3087
+ const {
3088
+ store
3089
+ } = useMenuRootContext();
3090
+ const highlighted = store.useState('isActive', listItem.index);
3091
+ const itemProps = store.useState('itemProps');
3092
+ const typingRef = store.context.typingRef;
3093
+ const {
3094
+ getButtonProps,
3095
+ buttonRef
3096
+ } = useButton({
3097
+ native: false,
3098
+ composite: true
3099
+ });
3100
+ const commonProps = useMenuItemCommonProps({
3101
+ closeOnClick,
3102
+ highlighted,
3103
+ id,
3104
+ nodeId,
3105
+ store,
3106
+ typingRef,
3107
+ itemRef: linkRef
3108
+ });
3109
+ function getItemProps(externalProps) {
3110
+ return mergeProps(commonProps, externalProps, getButtonProps);
3111
+ }
3112
+ const state = React.useMemo(() => ({
3113
+ highlighted
3114
+ }), [highlighted]);
3115
+ return useRenderElement('a', componentProps, {
3116
+ state,
3117
+ props: [itemProps, elementProps, getItemProps],
3118
+ ref: [linkRef, buttonRef, forwardedRef, listItem.ref]
3119
+ });
3120
+ });
3121
+ if (process.env.NODE_ENV !== "production") MenuLinkItem.displayName = "MenuLinkItem";
3122
+
3123
+ const ToolbarRootContext = /*#__PURE__*/React.createContext(undefined);
3124
+ if (process.env.NODE_ENV !== "production") ToolbarRootContext.displayName = "ToolbarRootContext";
3125
+ function useToolbarRootContext(optional) {
3126
+ const context = React.useContext(ToolbarRootContext);
3127
+ if (context === undefined && !optional) {
3128
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: ToolbarRootContext is missing. Toolbar parts must be placed within <Toolbar.Root>.' : formatErrorMessage(69));
3129
+ }
3130
+ return context;
3131
+ }
3132
+
3133
+ const stateAttributesMapping = {
3134
+ ...popupStateMapping,
3135
+ ...transitionStatusMapping
3136
+ };
3137
+
3138
+ /**
3139
+ * A container for the menu items.
3140
+ * Renders a `<div>` element.
3141
+ *
3142
+ * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu)
3143
+ */
3144
+ const MenuPopup = /*#__PURE__*/React.forwardRef(function MenuPopup(componentProps, forwardedRef) {
3145
+ const {
3146
+ render,
3147
+ className,
3148
+ finalFocus,
3149
+ ...elementProps
3150
+ } = componentProps;
3151
+ const {
3152
+ store
3153
+ } = useMenuRootContext();
3154
+ const {
3155
+ side,
3156
+ align
3157
+ } = useMenuPositionerContext();
3158
+ const insideToolbar = useToolbarRootContext(true) != null;
3159
+ const open = store.useState('open');
3160
+ const transitionStatus = store.useState('transitionStatus');
3161
+ const popupProps = store.useState('popupProps');
3162
+ const mounted = store.useState('mounted');
3163
+ const instantType = store.useState('instantType');
3164
+ const triggerElement = store.useState('activeTriggerElement');
3165
+ const parent = store.useState('parent');
3166
+ const lastOpenChangeReason = store.useState('lastOpenChangeReason');
3167
+ const rootId = store.useState('rootId');
3168
+ const floatingContext = store.useState('floatingRootContext');
3169
+ const floatingTreeRoot = store.useState('floatingTreeRoot');
3170
+ const closeDelay = store.useState('closeDelay');
3171
+ const activeTriggerElement = store.useState('activeTriggerElement');
3172
+ const isContextMenu = parent.type === 'context-menu';
3173
+ useOpenChangeComplete({
3174
+ open,
3175
+ ref: store.context.popupRef,
3176
+ onComplete() {
3177
+ if (open) {
3178
+ store.context.onOpenChangeComplete?.(true);
3366
3179
  }
3367
- nextIndexRef.current = sortedMap.size;
3368
3180
  }
3369
- onMapChange(sortedMap);
3370
- }, [onMapChange, sortedMap, elementsRef, labelsRef, mapTick]);
3371
- useIsoLayoutEffect(() => {
3372
- return () => {
3373
- elementsRef.current = [];
3374
- };
3375
- }, [elementsRef]);
3376
- useIsoLayoutEffect(() => {
3377
- return () => {
3378
- if (labelsRef) {
3379
- labelsRef.current = [];
3380
- }
3381
- };
3382
- }, [labelsRef]);
3383
- const subscribeMapChange = useStableCallback(fn => {
3384
- listeners.add(fn);
3181
+ });
3182
+ React.useEffect(() => {
3183
+ function handleClose(event) {
3184
+ store.setOpen(false, createChangeEventDetails(event.reason, event.domEvent));
3185
+ }
3186
+ floatingTreeRoot.events.on('close', handleClose);
3385
3187
  return () => {
3386
- listeners.delete(fn);
3188
+ floatingTreeRoot.events.off('close', handleClose);
3387
3189
  };
3190
+ }, [floatingTreeRoot.events, store]);
3191
+ const hoverEnabled = store.useState('hoverEnabled');
3192
+ const disabled = store.useState('disabled');
3193
+ useHoverFloatingInteraction(floatingContext, {
3194
+ enabled: hoverEnabled && !disabled && !isContextMenu && parent.type !== 'menubar',
3195
+ closeDelay
3388
3196
  });
3389
- useIsoLayoutEffect(() => {
3390
- listeners.forEach(l => l(sortedMap));
3391
- }, [listeners, sortedMap]);
3392
- const contextValue = React.useMemo(() => ({
3393
- register,
3394
- unregister,
3395
- subscribeMapChange,
3396
- elementsRef,
3397
- labelsRef,
3398
- nextIndexRef
3399
- }), [register, unregister, subscribeMapChange, elementsRef, labelsRef, nextIndexRef]);
3400
- return /*#__PURE__*/jsx(CompositeListContext.Provider, {
3401
- value: contextValue,
3402
- children: children
3197
+ const state = {
3198
+ transitionStatus,
3199
+ side,
3200
+ align,
3201
+ open,
3202
+ nested: parent.type === 'menu',
3203
+ instant: instantType
3204
+ };
3205
+ const setPopupElement = React.useCallback(element => {
3206
+ store.set('popupElement', element);
3207
+ }, [store]);
3208
+ const element = useRenderElement('div', componentProps, {
3209
+ state,
3210
+ ref: [forwardedRef, store.context.popupRef, setPopupElement],
3211
+ stateAttributesMapping,
3212
+ props: [popupProps, {
3213
+ onKeyDown(event) {
3214
+ if (insideToolbar && COMPOSITE_KEYS.has(event.key)) {
3215
+ event.stopPropagation();
3216
+ }
3217
+ }
3218
+ }, getDisabledMountTransitionStyles(transitionStatus), elementProps, {
3219
+ 'data-rootownerid': rootId
3220
+ }]
3403
3221
  });
3404
- }
3405
- function createMap() {
3406
- return new Map();
3407
- }
3408
- function createListeners() {
3409
- return new Set();
3410
- }
3411
- function sortByDocumentPosition(a, b) {
3412
- const position = a.compareDocumentPosition(b);
3413
- if (position & Node.DOCUMENT_POSITION_FOLLOWING || position & Node.DOCUMENT_POSITION_CONTAINED_BY) {
3414
- return -1;
3222
+ let returnFocus = parent.type === undefined || isContextMenu;
3223
+ if (triggerElement || parent.type === 'menubar' && lastOpenChangeReason !== outsidePress) {
3224
+ returnFocus = true;
3415
3225
  }
3416
- if (position & Node.DOCUMENT_POSITION_PRECEDING || position & Node.DOCUMENT_POSITION_CONTAINS) {
3417
- return 1;
3226
+ return /*#__PURE__*/jsx(FloatingFocusManager, {
3227
+ context: floatingContext,
3228
+ modal: isContextMenu,
3229
+ disabled: !mounted,
3230
+ returnFocus: finalFocus === undefined ? returnFocus : finalFocus,
3231
+ initialFocus: parent.type !== 'menu',
3232
+ restoreFocus: true,
3233
+ externalTree: parent.type !== 'menubar' ? floatingTreeRoot : undefined,
3234
+ previousFocusableElement: activeTriggerElement,
3235
+ nextFocusableElement: parent.type === undefined ? store.context.triggerFocusTargetRef : undefined,
3236
+ beforeContentFocusGuardRef: parent.type === undefined ? store.context.beforeContentFocusGuardRef : undefined,
3237
+ children: element
3238
+ });
3239
+ });
3240
+ if (process.env.NODE_ENV !== "production") MenuPopup.displayName = "MenuPopup";
3241
+
3242
+ const MenuPortalContext = /*#__PURE__*/React.createContext(undefined);
3243
+ if (process.env.NODE_ENV !== "production") MenuPortalContext.displayName = "MenuPortalContext";
3244
+ function useMenuPortalContext() {
3245
+ const value = React.useContext(MenuPortalContext);
3246
+ if (value === undefined) {
3247
+ throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: <Menu.Portal> is missing.' : formatErrorMessage(32));
3418
3248
  }
3419
- return 0;
3249
+ return value;
3420
3250
  }
3421
3251
 
3252
+ const MenuPortal = /*#__PURE__*/React.forwardRef(function MenuPortal(props, forwardedRef) {
3253
+ const {
3254
+ keepMounted = false,
3255
+ ...portalProps
3256
+ } = props;
3257
+ const {
3258
+ store
3259
+ } = useMenuRootContext();
3260
+ const mounted = store.useState('mounted');
3261
+ const shouldRender = mounted || keepMounted;
3262
+ if (!shouldRender) {
3263
+ return null;
3264
+ }
3265
+ return /*#__PURE__*/jsx(MenuPortalContext.Provider, {
3266
+ value: keepMounted,
3267
+ children: /*#__PURE__*/jsx(FloatingPortal, {
3268
+ ref: forwardedRef,
3269
+ ...portalProps
3270
+ })
3271
+ });
3272
+ });
3273
+ if (process.env.NODE_ENV !== "production") MenuPortal.displayName = "MenuPortal";
3274
+
3422
3275
  const MenuPositioner = /*#__PURE__*/React.forwardRef(function MenuPositioner(componentProps, forwardedRef) {
3423
3276
  const {
3424
3277
  anchor: anchorProp,
@@ -4429,41 +4282,6 @@ function MenuSubmenuRoot(props) {
4429
4282
  });
4430
4283
  }
4431
4284
 
4432
- function getPseudoElementBounds(element) {
4433
- const elementRect = element.getBoundingClientRect();
4434
-
4435
- // Avoid "Not implemented: window.getComputedStyle(elt, pseudoElt)"
4436
- if (process.env.NODE_ENV !== 'production') {
4437
- return elementRect;
4438
- }
4439
- const beforeStyles = window.getComputedStyle(element, '::before');
4440
- const afterStyles = window.getComputedStyle(element, '::after');
4441
- const hasPseudoElements = beforeStyles.content !== 'none' || afterStyles.content !== 'none';
4442
- if (!hasPseudoElements) {
4443
- return elementRect;
4444
- }
4445
-
4446
- // Get dimensions of pseudo-elements
4447
- const beforeWidth = parseFloat(beforeStyles.width) || 0;
4448
- const beforeHeight = parseFloat(beforeStyles.height) || 0;
4449
- const afterWidth = parseFloat(afterStyles.width) || 0;
4450
- const afterHeight = parseFloat(afterStyles.height) || 0;
4451
-
4452
- // Calculate max dimensions including pseudo-elements
4453
- const totalWidth = Math.max(elementRect.width, beforeWidth, afterWidth);
4454
- const totalHeight = Math.max(elementRect.height, beforeHeight, afterHeight);
4455
-
4456
- // Calculate the differences to extend the bounds
4457
- const widthDiff = totalWidth - elementRect.width;
4458
- const heightDiff = totalHeight - elementRect.height;
4459
- return {
4460
- left: elementRect.left - widthDiff / 2,
4461
- right: elementRect.right + widthDiff / 2,
4462
- top: elementRect.top - heightDiff / 2,
4463
- bottom: elementRect.bottom + heightDiff / 2
4464
- };
4465
- }
4466
-
4467
4285
  function useCompositeItem(params = {}) {
4468
4286
  const {
4469
4287
  highlightItemOnHover,
@@ -4979,4 +4797,4 @@ const MenuSubmenuTrigger = /*#__PURE__*/React.forwardRef(function SubmenuTrigger
4979
4797
  });
4980
4798
  if (process.env.NODE_ENV !== "production") MenuSubmenuTrigger.displayName = "MenuSubmenuTrigger";
4981
4799
 
4982
- export { useTypeahead as A, getPseudoElementBounds as B, CompositeList as C, DrawerTrigger as D, useAnchorPositioning as E, getDisabledMountTransitionStyles as F, IndexGuessBehavior as I, MenuTrigger as M, REGULAR_ITEM as R, DrawerTitle as a, DrawerClose as b, MenuLinkItem as c, MenuItem as d, MenuRadioItem as e, MenuRadioItemIndicator as f, MenuSubmenuTrigger as g, MenuRoot as h, MenuPortal as i, MenuPositioner as j, MenuPopup as k, MenuRadioGroup as l, MenuGroup as m, MenuSubmenuRoot as n, MenuGroupLabel as o, useMenuPositionerContext as p, useContextMenuRootContext as q, useCompositeListItem as r, useMenuItem as s, itemMapping as t, useMenuRootContext as u, useDirection as v, MenuStore as w, useFloatingRootContext as x, useClick as y, useListNavigation as z };
4800
+ export { MenuStore as A, CompositeList as C, DrawerTitle as D, IndexGuessBehavior as I, MenuTrigger as M, REGULAR_ITEM as R, DrawerClose as a, MenuLinkItem as b, MenuItem as c, MenuRadioItem as d, MenuRadioItemIndicator as e, MenuSubmenuTrigger as f, MenuRoot as g, MenuPortal as h, MenuPositioner as i, MenuPopup as j, MenuRadioGroup as k, MenuGroup as l, MenuSubmenuRoot as m, MenuGroupLabel as n, useListNavigation as o, useTypeahead as p, getPseudoElementBounds as q, useAnchorPositioning as r, getDisabledMountTransitionStyles as s, useCompositeListItem as t, useFloatingRootContext as u, useMenuRootContext as v, useMenuPositionerContext as w, useContextMenuRootContext as x, useMenuItem as y, itemMapping as z };