@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
@@ -0,0 +1,77 @@
1
+ // Ported from .base-ui/packages/react/src/utils/useMixedToggleClickHandler.ts (v1.6.0),
2
+ // octane-adapted (slot-threaded; native events). Returns `click`/`mousedown` handlers for a trigger
3
+ // whose popup is toggled by two different events — opened on mousedown, closed on click — so the
4
+ // click that follows the opening mousedown does not immediately close the popup again.
5
+ //
6
+ // octane adaptations: events are NATIVE (not synthetic), so the handlers take the native event
7
+ // and `preventBaseUIHandler` is the shim `mergeProps`/`makeEventPreventable` attaches to it.
8
+ // Upstream's empty `UseMixedToggleClickHandlerState` interface is a docs-pipeline artifact with no
9
+ // members, so it is dropped.
10
+ //
11
+ // SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
12
+ import { useMemo, useRef } from 'octane';
13
+
14
+ import { S, splitSlot, subSlot } from '../internal';
15
+ import { EMPTY_OBJECT } from './empty';
16
+ import { ownerDocument } from './owner';
17
+
18
+ export interface UseMixedToggleClickHandlerParameters {
19
+ /**
20
+ * Whether the mixed toggle click handler is enabled.
21
+ * @default true
22
+ */
23
+ enabled?: boolean | undefined;
24
+ /**
25
+ * Determines what action is performed on mousedown.
26
+ */
27
+ mouseDownAction: 'open' | 'close';
28
+ /**
29
+ * The current open state of the popup.
30
+ */
31
+ open: boolean;
32
+ }
33
+
34
+ export interface UseMixedToggleClickHandlerReturnValue {
35
+ onMouseDown?: (event: any) => void;
36
+ onClick?: (event: any) => void;
37
+ }
38
+
39
+ export function useMixedToggleClickHandler(...args: any[]): UseMixedToggleClickHandlerReturnValue {
40
+ const [user, slotArg] = splitSlot(args);
41
+ const slot = slotArg ?? S('useMixedToggleClickHandler');
42
+ const { enabled = true, mouseDownAction, open } = user[0] as UseMixedToggleClickHandlerParameters;
43
+
44
+ const ignoreClickRef = useRef(false, subSlot(slot, 'ignore'));
45
+
46
+ return useMemo(
47
+ () => {
48
+ if (!enabled) {
49
+ return EMPTY_OBJECT;
50
+ }
51
+
52
+ return {
53
+ onMouseDown: (event: any) => {
54
+ if ((mouseDownAction === 'open' && !open) || (mouseDownAction === 'close' && open)) {
55
+ ignoreClickRef.current = true;
56
+
57
+ ownerDocument(event.currentTarget as Element).addEventListener(
58
+ 'click',
59
+ () => {
60
+ ignoreClickRef.current = false;
61
+ },
62
+ { once: true },
63
+ );
64
+ }
65
+ },
66
+ onClick: (event: any) => {
67
+ if (ignoreClickRef.current) {
68
+ ignoreClickRef.current = false;
69
+ event.preventBaseUIHandler();
70
+ }
71
+ },
72
+ };
73
+ },
74
+ [enabled, mouseDownAction, open],
75
+ subSlot(slot, 'm'),
76
+ );
77
+ }
@@ -1,11 +1,12 @@
1
1
  // Ported from .base-ui/packages/react/src/utils/useOpenInteractionType.ts (v1.6.0), octane-adapted
2
2
  // (slot-threaded; native events). Records the interaction type (mouse/touch/pen/keyboard) that
3
3
  // opened a popup, so trigger-owned focus can behave correctly.
4
- import { useMemo } from 'octane';
4
+ import { useMemo, useState } from 'octane';
5
5
 
6
6
  import { subSlot } from '../internal';
7
7
  import { useStableCallback } from './useStableCallback';
8
8
  import { useEnhancedClickHandler, type InteractionType } from './useEnhancedClickHandler';
9
+ import { useValueChanged } from './useValueChanged';
9
10
  import { platform } from './platform';
10
11
 
11
12
  export function useOpenMethodTriggerProps(
@@ -30,3 +31,38 @@ export function useOpenMethodTriggerProps(
30
31
 
31
32
  return useMemo(() => ({ onClick, onPointerDown }), [onClick, onPointerDown], subSlot(slot, 'm'));
32
33
  }
34
+
35
+ /**
36
+ * Determines the interaction type (keyboard, mouse, touch, etc.) that opened the component, and
37
+ * returns it alongside the trigger props that record it. Resets to `null` once the popup closes.
38
+ *
39
+ * Used by roots that own the open method locally (Menu) rather than delegating it to each trigger
40
+ * (Popover, which calls `useOpenMethodTriggerProps` directly and stores the result in its store).
41
+ */
42
+ export function useOpenInteractionType(
43
+ open: boolean,
44
+ slot: symbol | undefined,
45
+ ): {
46
+ openMethod: InteractionType | null;
47
+ triggerProps: { onClick: (event: any) => void; onPointerDown: (event: any) => void };
48
+ } {
49
+ const [openMethod, setOpenMethod] = useState<InteractionType | null>(null, subSlot(slot, 's'));
50
+
51
+ const triggerProps = useOpenMethodTriggerProps(open, setOpenMethod, subSlot(slot, 'tp'));
52
+
53
+ useValueChanged(
54
+ open,
55
+ (previousOpen: boolean) => {
56
+ if (previousOpen && !open) {
57
+ setOpenMethod(null);
58
+ }
59
+ },
60
+ subSlot(slot, 'vc'),
61
+ );
62
+
63
+ return useMemo(
64
+ () => ({ openMethod, triggerProps }),
65
+ [openMethod, triggerProps],
66
+ subSlot(slot, 'r'),
67
+ );
68
+ }
@@ -0,0 +1,250 @@
1
+ // Ported from .base-ui/packages/react/src/utils/usePopupAutoResize.ts (v1.6.0). Lets a popup resize
2
+ // to its content while keeping the change animatable: it measures the new content at `max-content`,
3
+ // pins the popup to its previous size, then animates to the new one on the next frame.
4
+ //
5
+ // octane adaptations: `useIsoLayoutEffect` → `useLayoutEffect`; every composed hook threads an
6
+ // explicit slot.
7
+ //
8
+ // SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
9
+ import { useLayoutEffect, useMemo, useRef } from 'octane';
10
+
11
+ import { S, splitSlot, subSlot } from '../internal';
12
+ import { NOOP, EMPTY_OBJECT } from './empty';
13
+ import { useAnimationFrame } from './useAnimationFrame';
14
+ import { useAnimationsFinished } from './useAnimationsFinished';
15
+ import { useStableCallback } from './useStableCallback';
16
+ import { getCssDimensions } from './getCssDimensions';
17
+ import type { Dimensions } from './getCssDimensions';
18
+ import type { Side } from './useAnchorPositioning';
19
+
20
+ export interface UsePopupAutoResizeParameters {
21
+ /** Element to resize. */
22
+ popupElement: HTMLElement | null;
23
+ /** Positioner element (parent of the popup). */
24
+ positionerElement: HTMLElement | null;
25
+ /** Whether the popup is mounted. */
26
+ mounted: boolean;
27
+ /**
28
+ * Content that may change and trigger a resize. It doesn't have to be the actual content, just
29
+ * a value that changes when a resize is needed.
30
+ */
31
+ content: unknown;
32
+ /** Fired immediately before measuring the dimensions of the new content. */
33
+ onMeasureLayout?: (() => void) | undefined;
34
+ /**
35
+ * Fired after the new dimensions have been measured.
36
+ * @param previousDimensions Dimensions before the change, or `null` on the first measurement.
37
+ * @param newDimensions Newly measured dimensions.
38
+ */
39
+ onMeasureLayoutComplete?:
40
+ ((previousDimensions: Dimensions | null, newDimensions: Dimensions) => void) | undefined;
41
+ side: Side;
42
+ direction: 'ltr' | 'rtl';
43
+ }
44
+
45
+ function overrideElementStyle(element: HTMLElement, property: string, value: string): () => void {
46
+ const originalValue = element.style.getPropertyValue(property);
47
+ element.style.setProperty(property, value);
48
+ return () => {
49
+ element.style.setProperty(property, originalValue);
50
+ };
51
+ }
52
+
53
+ function applyElementStyles(element: HTMLElement, styles: Record<string, string>): () => void {
54
+ const restorers: Array<() => void> = [];
55
+ for (const [key, value] of Object.entries(styles)) {
56
+ restorers.push(overrideElementStyle(element, key, value));
57
+ }
58
+ return restorers.length
59
+ ? () => {
60
+ restorers.forEach((restore) => restore());
61
+ }
62
+ : NOOP;
63
+ }
64
+
65
+ function setPopupCssSize(popupElement: HTMLElement, size: Dimensions | 'auto'): void {
66
+ const width = size === 'auto' ? 'auto' : `${size.width}px`;
67
+ const height = size === 'auto' ? 'auto' : `${size.height}px`;
68
+ popupElement.style.setProperty('--popup-width', width);
69
+ popupElement.style.setProperty('--popup-height', height);
70
+ }
71
+
72
+ function setPositionerCssSize(
73
+ positionerElement: HTMLElement,
74
+ size: Dimensions | 'max-content',
75
+ ): void {
76
+ const width = size === 'max-content' ? 'max-content' : `${size.width}px`;
77
+ const height = size === 'max-content' ? 'max-content' : `${size.height}px`;
78
+ positionerElement.style.setProperty('--positioner-width', width);
79
+ positionerElement.style.setProperty('--positioner-height', height);
80
+ }
81
+
82
+ export function usePopupAutoResize(...args: any[]): void {
83
+ const [user, slotArg] = splitSlot(args);
84
+ const slot = slotArg ?? S('usePopupAutoResize');
85
+ const parameters = user[0] as UsePopupAutoResizeParameters;
86
+
87
+ const {
88
+ popupElement,
89
+ positionerElement,
90
+ content,
91
+ mounted,
92
+ onMeasureLayout: onMeasureLayoutParam,
93
+ onMeasureLayoutComplete: onMeasureLayoutCompleteParam,
94
+ side,
95
+ direction,
96
+ } = parameters;
97
+
98
+ const runOnceAnimationsFinish = useAnimationsFinished(
99
+ popupElement,
100
+ true,
101
+ false,
102
+ subSlot(slot, 'anim'),
103
+ );
104
+
105
+ const animationFrame = useAnimationFrame(subSlot(slot, 'frame'));
106
+
107
+ const committedDimensionsRef = useRef<Dimensions | null>(null, subSlot(slot, 'dims'));
108
+ const isInitialRenderRef = useRef(true, subSlot(slot, 'init'));
109
+
110
+ const restoreAnchoringStylesRef = useRef<() => void>(NOOP, subSlot(slot, 'restore'));
111
+
112
+ const onMeasureLayout = useStableCallback(onMeasureLayoutParam, subSlot(slot, 'oml'));
113
+ const onMeasureLayoutComplete = useStableCallback(
114
+ onMeasureLayoutCompleteParam,
115
+ subSlot(slot, 'omlc'),
116
+ );
117
+
118
+ const anchoringStyles = useMemo(
119
+ () => {
120
+ // Ensure the popup size transitions correctly when anchored to `bottom` (side=top) or
121
+ // `right` (side=left).
122
+ let isOriginSide = side === 'top';
123
+ let isPhysicalLeft = side === 'left';
124
+ if (direction === 'rtl') {
125
+ isOriginSide = isOriginSide || side === 'inline-end';
126
+ isPhysicalLeft = isPhysicalLeft || side === 'inline-end';
127
+ } else {
128
+ isOriginSide = isOriginSide || side === 'inline-start';
129
+ isPhysicalLeft = isPhysicalLeft || side === 'inline-start';
130
+ }
131
+
132
+ return isOriginSide
133
+ ? ({
134
+ position: 'absolute',
135
+ [side === 'top' ? 'bottom' : 'top']: '0',
136
+ [isPhysicalLeft ? 'right' : 'left']: '0',
137
+ } as Record<string, string>)
138
+ : (EMPTY_OBJECT as unknown as Record<string, string>);
139
+ },
140
+ [side, direction],
141
+ subSlot(slot, 'anchoring'),
142
+ );
143
+
144
+ useLayoutEffect(
145
+ () => {
146
+ // Reset the state when the popup is closed.
147
+ if (!mounted) {
148
+ restoreAnchoringStylesRef.current = NOOP;
149
+ isInitialRenderRef.current = true;
150
+ committedDimensionsRef.current = null;
151
+ return undefined;
152
+ }
153
+
154
+ if (!popupElement || !positionerElement) {
155
+ return undefined;
156
+ }
157
+
158
+ restoreAnchoringStylesRef.current = applyElementStyles(popupElement, anchoringStyles);
159
+
160
+ // Measure the rendered size to enable transitions.
161
+ setPopupCssSize(popupElement, 'auto');
162
+
163
+ const restorePopupPosition = overrideElementStyle(popupElement, 'position', 'static');
164
+ const restorePopupTransform = overrideElementStyle(popupElement, 'transform', 'none');
165
+ const restorePopupScale = overrideElementStyle(popupElement, 'scale', '1');
166
+ const restorePositionerAvailableSize = applyElementStyles(positionerElement, {
167
+ '--available-width': 'max-content',
168
+ '--available-height': 'max-content',
169
+ });
170
+
171
+ function restoreMeasurementOverrides() {
172
+ restorePopupPosition();
173
+ restorePopupTransform();
174
+ restorePositionerAvailableSize();
175
+ }
176
+
177
+ function restoreMeasurementOverridesIncludingScale() {
178
+ restoreMeasurementOverrides();
179
+ restorePopupScale();
180
+ }
181
+
182
+ onMeasureLayout?.();
183
+
184
+ // Initial render (for each time the popup opens).
185
+ if (isInitialRenderRef.current || committedDimensionsRef.current === null) {
186
+ setPositionerCssSize(positionerElement, 'max-content');
187
+
188
+ const dimensions = getCssDimensions(popupElement);
189
+
190
+ committedDimensionsRef.current = dimensions;
191
+
192
+ setPositionerCssSize(positionerElement, dimensions);
193
+ restoreMeasurementOverridesIncludingScale();
194
+ onMeasureLayoutComplete?.(null, dimensions);
195
+
196
+ isInitialRenderRef.current = false;
197
+
198
+ return () => {
199
+ restoreAnchoringStylesRef.current();
200
+ restoreAnchoringStylesRef.current = NOOP;
201
+ };
202
+ }
203
+
204
+ // Subsequent renders while open (when `content` changes).
205
+ setPositionerCssSize(positionerElement, 'max-content');
206
+
207
+ const previousDimensions = committedDimensionsRef.current;
208
+ const newDimensions = getCssDimensions(popupElement);
209
+
210
+ // Commit immediately so future content changes have a stable previous size.
211
+ committedDimensionsRef.current = newDimensions;
212
+
213
+ setPopupCssSize(popupElement, previousDimensions);
214
+ restoreMeasurementOverridesIncludingScale();
215
+ onMeasureLayoutComplete?.(previousDimensions, newDimensions);
216
+
217
+ setPositionerCssSize(positionerElement, newDimensions);
218
+
219
+ const abortController = new AbortController();
220
+
221
+ animationFrame.request(() => {
222
+ setPopupCssSize(popupElement, newDimensions);
223
+
224
+ runOnceAnimationsFinish(() => {
225
+ popupElement.style.setProperty('--popup-width', 'auto');
226
+ popupElement.style.setProperty('--popup-height', 'auto');
227
+ }, abortController.signal);
228
+ });
229
+
230
+ return () => {
231
+ abortController.abort();
232
+ animationFrame.cancel();
233
+ restoreAnchoringStylesRef.current();
234
+ restoreAnchoringStylesRef.current = NOOP;
235
+ };
236
+ },
237
+ [
238
+ content,
239
+ popupElement,
240
+ positionerElement,
241
+ runOnceAnimationsFinish,
242
+ animationFrame,
243
+ mounted,
244
+ onMeasureLayout,
245
+ onMeasureLayoutComplete,
246
+ anchoringStyles,
247
+ ],
248
+ subSlot(slot, 'l:resize'),
249
+ );
250
+ }