@octanejs/base-ui 0.1.14 → 0.1.16

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 (49) hide show
  1. package/package.json +5 -5
  2. package/src/alert-dialog.ts +2 -0
  3. package/src/button.ts +63 -0
  4. package/src/context-menu.ts +317 -0
  5. package/src/csp-provider.ts +37 -0
  6. package/src/dialog.ts +89 -22
  7. package/src/direction-provider.ts +32 -0
  8. package/src/index.ts +10 -0
  9. package/src/menu.ts +3387 -0
  10. package/src/menubar.ts +160 -0
  11. package/src/popover.ts +99 -25
  12. package/src/preview-card.ts +812 -0
  13. package/src/tooltip.ts +1053 -0
  14. package/src/unstable-use-media-query.ts +101 -0
  15. package/src/utils/CSPContext.ts +21 -0
  16. package/src/utils/ContextMenuRootContext.ts +44 -0
  17. package/src/utils/FloatingPortalLite.ts +29 -0
  18. package/src/utils/MenubarContext.ts +38 -0
  19. package/src/utils/RequestQueue.ts +132 -0
  20. package/src/utils/TimeoutManager.ts +32 -0
  21. package/src/utils/composite/grid-utils.ts +437 -0
  22. package/src/utils/composite/gridNavigation.ts +127 -0
  23. package/src/utils/composite/list-utils.ts +56 -28
  24. package/src/utils/composite/useCompositeRoot.ts +11 -13
  25. package/src/utils/constants.ts +22 -0
  26. package/src/utils/createChangeEventDetails.ts +4 -0
  27. package/src/utils/findRootOwnerId.ts +19 -0
  28. package/src/utils/floating/FloatingDelayGroup.ts +280 -0
  29. package/src/utils/floating/element.ts +36 -2
  30. package/src/utils/floating/gridNavigation.ts +54 -0
  31. package/src/utils/floating/safePolygon.ts +448 -0
  32. package/src/utils/floating/useClientPoint.ts +276 -0
  33. package/src/utils/floating/useFocus.ts +248 -0
  34. package/src/utils/floating/useHoverFloatingInteraction.ts +301 -11
  35. package/src/utils/floating/useHoverInteractionSharedState.ts +139 -0
  36. package/src/utils/floating/useHoverReferenceInteraction.ts +507 -14
  37. package/src/utils/floating/useHoverShared.ts +78 -0
  38. package/src/utils/floating/useListNavigation.ts +1018 -0
  39. package/src/utils/floating/useTypeahead.ts +287 -0
  40. package/src/utils/getCssDimensions.ts +28 -0
  41. package/src/utils/getPseudoElementBounds.ts +54 -0
  42. package/src/utils/platform.ts +10 -0
  43. package/src/utils/popups/index.ts +6 -0
  44. package/src/utils/popups/inlineRect.ts +289 -0
  45. package/src/utils/useMixedToggleClickHandler.ts +77 -0
  46. package/src/utils/useOpenInteractionType.ts +37 -1
  47. package/src/utils/usePopupAutoResize.ts +250 -0
  48. package/src/utils/usePopupViewport.ts +375 -0
  49. package/src/utils/usePreviousValue.ts +25 -0
package/src/menubar.ts ADDED
@@ -0,0 +1,160 @@
1
+ // Ported from .base-ui/packages/react/src/menubar/ (v1.6.0) — Phase 3f STAGE 4.
2
+ //
3
+ // A `Menubar` is a `CompositeRoot` of `Menu.Trigger`s that share one `FloatingTree`, plus the
4
+ // `MenubarContext` that `Menu` has been branching on since stage 1 (`parent.type === 'menubar'`).
5
+ // Providing it here is what activates those already-transcribed branches: triggers render as
6
+ // `CompositeItem`s with `role="menuitem"`, sibling menus open on hover once one is open, the
7
+ // positioner flips its default side to match the bar's orientation, and the modal backdrop moves to
8
+ // the bar itself so the whole menubar stays clickable.
9
+ //
10
+ // `MenubarContext` itself lives in `./utils/MenubarContext` — it landed as a context-only module in
11
+ // stage 1 so `Menu`'s parent-union branches could be written verbatim ahead of this component.
12
+ //
13
+ // octane adaptations: `createElement`, not JSX; `forwardRef` → ref-as-prop; per-call-site `S('…')`
14
+ // slots with `subSlot` for every composed hook; `useFloatingNodeId()` takes an explicit
15
+ // (tree, slot) pair.
16
+ import { createElement, useState, useRef, useEffect, useMemo } from 'octane';
17
+
18
+ import { S, subSlot } from './internal';
19
+ import { useBaseUiId } from './utils/useBaseUiId';
20
+ import { CompositeRoot } from './utils/composite/CompositeRoot';
21
+ import {
22
+ FloatingNode,
23
+ FloatingTree,
24
+ useFloatingNodeId,
25
+ useFloatingTree,
26
+ } from './utils/floating/FloatingTree';
27
+ import { MenubarContext, useMenubarContext } from './utils/MenubarContext';
28
+ import type { MenubarContextValue } from './utils/MenubarContext';
29
+ import type { MenuOpenEventDetails, MenuRootOrientation } from './menu';
30
+ import type { StateAttributesMapping } from './utils/getStateAttributesProps';
31
+ import { REASONS } from './utils/createChangeEventDetails';
32
+
33
+ const menubarStateAttributesMapping: StateAttributesMapping<any> = {
34
+ hasSubmenuOpen(value: boolean) {
35
+ return value ? { 'data-has-submenu-open': '' } : null;
36
+ },
37
+ };
38
+
39
+ function MenubarContent(props: { children?: any }): any {
40
+ const slot = S('MenubarContent');
41
+ const nodeId = useFloatingNodeId(undefined, subSlot(slot, 'nodeId'));
42
+ const tree = useFloatingTree();
43
+ const menuEvents = (tree as any).events;
44
+ const rootContext = useMenubarContext();
45
+
46
+ useEffect(
47
+ () => {
48
+ function onSubmenuOpenChange(details: MenuOpenEventDetails) {
49
+ if (!details.nodeId || details.parentNodeId !== nodeId) {
50
+ return;
51
+ }
52
+
53
+ if (details.open) {
54
+ if (!rootContext.hasSubmenuOpen) {
55
+ rootContext.setHasSubmenuOpen(true);
56
+ }
57
+ } else if (
58
+ details.reason !== REASONS.siblingOpen &&
59
+ details.reason !== REASONS.listNavigation
60
+ ) {
61
+ rootContext.setHasSubmenuOpen(false);
62
+ }
63
+ }
64
+
65
+ menuEvents.on('menuopenchange', onSubmenuOpenChange);
66
+
67
+ return () => {
68
+ menuEvents.off('menuopenchange', onSubmenuOpenChange);
69
+ };
70
+ },
71
+ [menuEvents, nodeId, rootContext],
72
+ subSlot(slot, 'e'),
73
+ );
74
+
75
+ return createElement(FloatingNode, { id: nodeId, children: props.children });
76
+ }
77
+
78
+ /**
79
+ * The container for menus.
80
+ *
81
+ * Documentation: [Base UI Menubar](https://base-ui.com/react/components/menubar)
82
+ */
83
+ export function Menubar(props: any): any {
84
+ const slot = S('Menubar');
85
+ const {
86
+ orientation = 'horizontal',
87
+ loopFocus = true,
88
+ render,
89
+ className,
90
+ modal = true,
91
+ disabled = false,
92
+ id: idProp,
93
+ style,
94
+ ref: forwardedRef,
95
+ ...elementProps
96
+ } = props;
97
+
98
+ const [contentElement, setContentElement] = useState<HTMLElement | null>(
99
+ null,
100
+ subSlot(slot, 'content'),
101
+ );
102
+ const [hasSubmenuOpen, setHasSubmenuOpen] = useState(false, subSlot(slot, 'hasSub'));
103
+
104
+ const id = useBaseUiId(idProp, subSlot(slot, 'id'));
105
+
106
+ const state: MenubarState = {
107
+ orientation,
108
+ modal,
109
+ hasSubmenuOpen,
110
+ };
111
+
112
+ const contentRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'contentRef'));
113
+ const allowMouseUpTriggerRef = useRef(false, subSlot(slot, 'amut'));
114
+
115
+ const context: MenubarContextValue = useMemo(
116
+ () => ({
117
+ contentElement,
118
+ setContentElement,
119
+ setHasSubmenuOpen,
120
+ hasSubmenuOpen,
121
+ modal,
122
+ disabled,
123
+ orientation,
124
+ allowMouseUpTriggerRef,
125
+ rootId: id,
126
+ }),
127
+ [contentElement, hasSubmenuOpen, modal, disabled, orientation, id],
128
+ subSlot(slot, 'ctx'),
129
+ );
130
+
131
+ return createElement(MenubarContext.Provider, {
132
+ value: context,
133
+ children: createElement(FloatingTree, {
134
+ children: createElement(MenubarContent, {
135
+ children: createElement(CompositeRoot, {
136
+ render,
137
+ className,
138
+ style,
139
+ state,
140
+ stateAttributesMapping: menubarStateAttributesMapping,
141
+ refs: [forwardedRef, setContentElement, contentRef],
142
+ props: [{ role: 'menubar', id, 'aria-orientation': orientation }, elementProps],
143
+ orientation,
144
+ loopFocus,
145
+ enableHomeAndEndKeys: true,
146
+ highlightItemOnHover: hasSubmenuOpen,
147
+ }),
148
+ }),
149
+ }),
150
+ });
151
+ }
152
+
153
+ export interface MenubarState {
154
+ /** The orientation of the menubar. */
155
+ orientation: MenuRootOrientation;
156
+ /** Whether the menubar is modal. */
157
+ modal: boolean;
158
+ /** Whether any submenu within the menubar is open. */
159
+ hasSubmenuOpen: boolean;
160
+ }
package/src/popover.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  // Ported from .base-ui/packages/react/src/popover/ (v1.6.0): store (PopoverStore/PopoverHandle),
2
- // root (context/PopoverRoot/PopoverInteractions), trigger. The CLOSED-state path (Root + Trigger),
3
- // reusing the Store + floating stack from Dialog. octane adaptations: forwardRef → ref-as-prop;
4
- // native events; every Store hook-method + hook threads an explicit slot; `React.createContext` →
5
- // octane `createContext`; `ReactDOM.flushSync` → octane `flushSync`.
2
+ // root (context/PopoverRoot/PopoverInteractions), trigger, and the anchored open path
3
+ // (Portal/Positioner/Popup/Arrow/Backdrop/Title/Description/Close), reusing the Store + floating
4
+ // stack from Dialog. octane adaptations: forwardRef → ref-as-prop; native events; every Store
5
+ // hook-method + hook threads an explicit slot; `React.createContext` → octane `createContext`;
6
+ // `ReactDOM.flushSync` → octane `flushSync`.
6
7
  //
7
- // STUBBED/DEFERRED (behind off-by-default features): `useHoverReferenceInteraction` (openOnHover);
8
- // the Positioner + Popup + Arrow + Backdrop + Title/Description/Close parts (the open path) land next
9
- // with `useAnchorPositioning`. Interactions are only mounted when `open || mounted`, and — as with
10
- // Dialog — a stable no-DOM descriptor wraps the children (octane Provider children shape-flip bug).
8
+ // `openOnHover` is fully wired: the trigger side runs `useHoverReferenceInteraction` (+ safePolygon)
9
+ // and the popup side runs `useHoverFloatingInteraction`.
11
10
  import {
12
11
  createContext,
13
12
  createElement,
13
+ Fragment,
14
14
  useContext,
15
15
  useMemo,
16
16
  useRef,
@@ -51,13 +51,14 @@ import {
51
51
  type UseAnchorPositioningSharedParameters,
52
52
  } from './utils/useAnchorPositioning';
53
53
  import { usePositioner } from './utils/usePositioner';
54
+ import { usePopupViewport } from './utils/usePopupViewport';
54
55
  import { useAnchoredPopupScrollLock } from './utils/useAnchoredPopupScrollLock';
55
56
  import { adaptiveOrigin } from './utils/adaptiveOriginMiddleware';
56
57
  import { InternalBackdrop } from './utils/InternalBackdrop';
57
58
  import { useAnimationsFinished } from './utils/useAnimationsFinished';
58
59
  import { useOpenChangeComplete } from './utils/useOpenChangeComplete';
59
60
  import { getDisabledMountTransitionStyles } from './utils/getDisabledMountTransitionStyles';
60
- import { POPUP_COLLISION_AVOIDANCE } from './utils/constants';
61
+ import { POPUP_COLLISION_AVOIDANCE, PATIENT_CLICK_THRESHOLD } from './utils/constants';
61
62
  import { ClosePartProvider, useClosePartCount, useClosePartRegistration } from './utils/closePart';
62
63
  import { popupStateMapping } from './utils/popupStateMapping';
63
64
  import { transitionStatusMapping } from './utils/useTransitionStatus';
@@ -97,7 +98,6 @@ import { PopupTriggerMap } from './utils/popups/popupTriggerMap';
97
98
  import type { InteractionType } from './utils/useEnhancedClickHandler';
98
99
 
99
100
  const OPEN_DELAY = 300;
100
- const PATIENT_CLICK_THRESHOLD = 500;
101
101
  const CLICK_TRIGGER_IDENTIFIER = 'data-base-ui-click-trigger';
102
102
 
103
103
  // --- Store -------------------------------------------------------------------
@@ -415,11 +415,7 @@ function PopoverInteractions(props: any): any {
415
415
  subSlot(slot, 'pip'),
416
416
  );
417
417
 
418
- return props.children ?? null;
419
- }
420
-
421
- function PopoverChildren(props: any): any {
422
- return props.children ?? null;
418
+ return null;
423
419
  }
424
420
 
425
421
  // --- Root --------------------------------------------------------------------
@@ -519,12 +515,16 @@ function PopoverRootComponent<Payload>(props: any): any {
519
515
  const resolvedChildren =
520
516
  typeof children === 'function' && !isChildrenBlock(children) ? children({ payload }) : children;
521
517
 
522
- // Stable descriptor shape (see the Dialog note / octane Provider children shape-flip bug).
523
- const content = createElement(shouldRenderInteractions ? PopoverInteractions : PopoverChildren, {
524
- store,
525
- modal,
526
- children: resolvedChildren,
527
- });
518
+ // `PopoverInteractions` is a headless SIBLING of the children, exactly as Base UI renders it, so
519
+ // toggling it never disturbs them. Rendering it as a WRAPPER instead swapped the wrapper's
520
+ // component type on every open, which tore down and rebuilt the whole subtree — including the
521
+ // trigger, whose element listeners and store registration then pointed at a detached node.
522
+ const content = [
523
+ shouldRenderInteractions
524
+ ? createElement(PopoverInteractions, { key: 'interactions', store, modal })
525
+ : null,
526
+ createElement(Fragment, { key: 'children', children: resolvedChildren }),
527
+ ];
528
528
 
529
529
  return createElement(PopoverRootContext.Provider, {
530
530
  value: contextValue as PopoverRootContextValue,
@@ -532,7 +532,12 @@ function PopoverRootComponent<Payload>(props: any): any {
532
532
  });
533
533
  }
534
534
 
535
- function PopoverRoot<Payload = unknown>(props: any): any {
535
+ interface PopoverRootProps<Payload> {
536
+ handle?: PopoverHandle<Payload>;
537
+ [key: string]: unknown;
538
+ }
539
+
540
+ function PopoverRoot<Payload = unknown>(props: PopoverRootProps<Payload>): any {
536
541
  const slot = S('PopoverRootWrapper');
537
542
  // Top-level popovers establish a FloatingTree; nested ones reuse the parent's.
538
543
  if (usePopoverRootContext(true)) {
@@ -680,18 +685,33 @@ function PopoverTrigger(componentProps: any): any {
680
685
  subSlot(slot, 're'),
681
686
  );
682
687
 
688
+ // The trigger's children shape flips between a lone element (closed) and
689
+ // [guard, element, guard] (open, non-modal). Base UI keeps the trigger's identity across that
690
+ // flip by wrapping it in a fragment keyed on the trigger id in BOTH branches — without the key
691
+ // the button is torn down and rebuilt on every open, which detaches the element listeners
692
+ // (hover open/close) and makes the positioner see a spurious trigger change.
693
+ const keyedElement = createElement(Fragment, { key: thisTriggerId, children: element });
694
+
683
695
  if (isMountedByThisTrigger && !focusManagerModal) {
696
+ // Base UI renders these as static JSX fragment children; octane reconciles a returned array
697
+ // as a list, so the guards carry stable keys too — otherwise the unkeyed siblings shift the
698
+ // keyed trigger and it is rebuilt anyway.
684
699
  return [
685
- createElement(FocusGuard, { ref: preFocusGuardRef, onFocus: handlePreFocusGuardFocus }),
686
- element,
687
700
  createElement(FocusGuard, {
701
+ key: `${thisTriggerId}-pre-guard`,
702
+ ref: preFocusGuardRef,
703
+ onFocus: handlePreFocusGuardFocus,
704
+ }),
705
+ keyedElement,
706
+ createElement(FocusGuard, {
707
+ key: `${thisTriggerId}-post-guard`,
688
708
  ref: store.context.triggerFocusTargetRef,
689
709
  onFocus: handleFocusTargetFocus,
690
710
  }),
691
711
  ];
692
712
  }
693
713
 
694
- return element;
714
+ return keyedElement;
695
715
  }
696
716
 
697
717
  // --- Portal ------------------------------------------------------------------
@@ -1188,6 +1208,58 @@ function PopoverClose(componentProps: any): any {
1188
1208
  );
1189
1209
  }
1190
1210
 
1211
+ // --- Viewport ----------------------------------------------------------------
1212
+
1213
+ export const PopoverViewportCssVars = {
1214
+ /**
1215
+ * The width of the parent popup when the previous content was rendered. Placed on the
1216
+ * 'previous' container so the popup's dimensions can be frozen while content animates.
1217
+ */
1218
+ popupWidth: '--popup-width',
1219
+ /** The height of the parent popup when the previous content was rendered. */
1220
+ popupHeight: '--popup-height',
1221
+ } as const;
1222
+
1223
+ const viewportStateAttributesMapping: StateAttributesMapping<any> = {
1224
+ activationDirection: (value: string | undefined) =>
1225
+ value ? { 'data-activation-direction': value } : null,
1226
+ };
1227
+
1228
+ // A viewport for displaying content transitions. Only needed when one popup can be opened by
1229
+ // multiple triggers, its content changes per trigger, and switching between them is animated.
1230
+ function PopoverViewport(componentProps: any): any {
1231
+ const slot = S('PopoverViewport');
1232
+ const { render, className, style, children, ref, ...elementProps } = componentProps;
1233
+
1234
+ const { store } = usePopoverRootContext() as PopoverRootContextValue;
1235
+ const { side } = usePopoverPositionerContext();
1236
+
1237
+ const instantType = store.useState('instantType', subSlot(slot, 'instant'));
1238
+
1239
+ const { children: childrenToRender, state: viewportState } = usePopupViewport(
1240
+ { store, side, cssVars: PopoverViewportCssVars, children },
1241
+ subSlot(slot, 'vp'),
1242
+ );
1243
+
1244
+ const state = {
1245
+ activationDirection: viewportState.activationDirection,
1246
+ transitioning: viewportState.transitioning,
1247
+ instant: instantType,
1248
+ };
1249
+
1250
+ return useRenderElement(
1251
+ 'div',
1252
+ { render, className, style },
1253
+ {
1254
+ state,
1255
+ ref,
1256
+ props: [elementProps, { children: childrenToRender }],
1257
+ stateAttributesMapping: viewportStateAttributesMapping,
1258
+ },
1259
+ subSlot(slot, 're'),
1260
+ );
1261
+ }
1262
+
1191
1263
  // --- Namespace ---------------------------------------------------------------
1192
1264
 
1193
1265
  export const Popover = {
@@ -1196,10 +1268,12 @@ export const Popover = {
1196
1268
  Portal: PopoverPortal,
1197
1269
  Positioner: PopoverPositioner,
1198
1270
  Popup: PopoverPopup,
1271
+ Viewport: PopoverViewport,
1199
1272
  Arrow: PopoverArrow,
1200
1273
  Backdrop: PopoverBackdrop,
1201
1274
  Title: PopoverTitle,
1202
1275
  Description: PopoverDescription,
1203
1276
  Close: PopoverClose,
1204
1277
  createHandle: createPopoverHandle,
1278
+ Handle: PopoverHandle,
1205
1279
  };