@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/base-ui",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -41,19 +41,19 @@
41
41
  "@floating-ui/utils": "^0.2.12",
42
42
  "aria-hidden": "^1.2.6",
43
43
  "tabbable": "^6.5.0",
44
- "@octanejs/floating-ui": "0.1.15"
44
+ "@octanejs/floating-ui": "0.1.17"
45
45
  },
46
46
  "peerDependencies": {
47
- "octane": "0.1.16"
47
+ "octane": "0.1.18"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@base-ui/react": "^1.6.0",
51
- "@tsrx/react": "^0.2.50",
51
+ "@tsrx/react": "^0.2.54",
52
52
  "esbuild": "^0.28.1",
53
53
  "react": "^19.2.7",
54
54
  "react-dom": "^19.2.7",
55
55
  "vitest": "^4.1.10",
56
- "octane": "0.1.16"
56
+ "octane": "0.1.18"
57
57
  },
58
58
  "scripts": {
59
59
  "test": "vitest run"
@@ -40,8 +40,10 @@ export const AlertDialog = {
40
40
  Portal: Dialog.Portal,
41
41
  Backdrop: Dialog.Backdrop,
42
42
  Popup: Dialog.Popup,
43
+ Viewport: Dialog.Viewport,
43
44
  Title: Dialog.Title,
44
45
  Description: Dialog.Description,
45
46
  Close: Dialog.Close,
46
47
  createHandle: createAlertDialogHandle,
48
+ Handle: AlertDialogHandle,
47
49
  };
package/src/button.ts ADDED
@@ -0,0 +1,63 @@
1
+ // Ported from .base-ui/packages/react/src/button/Button.tsx (v1.6.0). A button that can be
2
+ // used to trigger actions. Renders a `<button>` element.
3
+ import { S, subSlot } from './internal';
4
+ import { useButton } from './utils/useButton';
5
+ import { useRenderElement } from './utils/useRenderElement';
6
+
7
+ export interface ButtonState {
8
+ /** Whether the button should ignore user interaction. */
9
+ disabled: boolean;
10
+ }
11
+
12
+ export interface ButtonProps {
13
+ disabled?: boolean;
14
+ /**
15
+ * Whether the button should be focusable when disabled.
16
+ * @default false
17
+ */
18
+ focusableWhenDisabled?: boolean;
19
+ /**
20
+ * Whether the component renders a native `<button>` element when replacing it via the
21
+ * `render` prop.
22
+ * @default true
23
+ */
24
+ nativeButton?: boolean;
25
+ className?: string | ((state: ButtonState) => string | undefined);
26
+ render?: import('./utils/useRenderElement').RenderProp<ButtonState>;
27
+ ref?: any;
28
+ [key: string]: any;
29
+ }
30
+
31
+ // octane: forwardRef → ref-as-prop.
32
+ export function Button(props: ButtonProps): any {
33
+ const slot = S('Button');
34
+ const {
35
+ render,
36
+ className,
37
+ disabled = false,
38
+ focusableWhenDisabled = false,
39
+ nativeButton = true,
40
+ style,
41
+ ref,
42
+ ...elementProps
43
+ } = props;
44
+
45
+ const { getButtonProps, buttonRef } = useButton(
46
+ { disabled, focusableWhenDisabled, native: nativeButton },
47
+ subSlot(slot, 'btn'),
48
+ );
49
+
50
+ const state: ButtonState = { disabled };
51
+
52
+ return useRenderElement(
53
+ 'button',
54
+ { className, render, style },
55
+ { state, ref: [ref, buttonRef], props: [elementProps, getButtonProps] },
56
+ subSlot(slot, 're'),
57
+ );
58
+ }
59
+
60
+ export namespace Button {
61
+ export type Props = ButtonProps;
62
+ export type State = ButtonState;
63
+ }
@@ -0,0 +1,317 @@
1
+ // Ported from .base-ui/packages/react/src/context-menu/ (v1.6.0) — Phase 3f STAGE 4.
2
+ //
3
+ // A `ContextMenu` is a `Menu.Root` opened by right-click or long-press at the pointer, anchored to a
4
+ // synthetic zero-size rect at the cursor instead of to a trigger element. `ContextMenu.Root`
5
+ // provides the `ContextMenuRootContext` that `Menu` has been branching on since stage 1
6
+ // (`parent.type === 'context-menu'`): a modal focus manager, `mousedown`-grace-period outside-press
7
+ // dismissal, `fixed` positioning, `start` alignment, and the item `mouseup` guards that stop the
8
+ // gesture which OPENED the menu from immediately activating an item under the cursor.
9
+ //
10
+ // `ContextMenuRootContext` itself lives in `./utils/ContextMenuRootContext` — it landed as a
11
+ // context-only module in stage 1 so `Menu`'s parent-union branches could be written verbatim ahead
12
+ // of this component.
13
+ //
14
+ // Every part other than `Root` and `Trigger` is `Menu`'s own, re-exported through this namespace
15
+ // exactly as upstream's `context-menu/index.parts.ts` does.
16
+ //
17
+ // octane adaptations: `createElement`, not JSX; `forwardRef` → ref-as-prop; per-call-site `S('…')`
18
+ // slots with `subSlot`; native events, so `event.nativeEvent` collapses to `event`; upstream's
19
+ // `React.TouchEvent`/`React.MouseEvent` are the native types here.
20
+ import { createElement, useState, useRef, useEffect, useMemo, useId } from 'octane';
21
+
22
+ import { S, subSlot } from './internal';
23
+ import { Menu, MenuRootContext, useMenuRootContext } from './menu';
24
+ import type { MenuRootContextValue } from './menu';
25
+ import { ContextMenuRootContext, useContextMenuRootContext } from './utils/ContextMenuRootContext';
26
+ import type { ContextMenuRootContextValue } from './utils/ContextMenuRootContext';
27
+ import { useRenderElement } from './utils/useRenderElement';
28
+ import { useTimeout } from './utils/useTimeout';
29
+ import { addEventListener } from './utils/addEventListener';
30
+ import { ownerDocument } from './utils/owner';
31
+ import { contains } from './utils/contains';
32
+ import { getTarget, stopEvent } from './utils/composite/list-utils';
33
+ import { createChangeEventDetails, REASONS } from './utils/createChangeEventDetails';
34
+ import { pressableTriggerOpenStateMapping } from './utils/popupStateMapping';
35
+ import { findRootOwnerId } from './utils/findRootOwnerId';
36
+
37
+ const LONG_PRESS_DELAY = 500;
38
+
39
+ // --- Root ---------------------------------------------------------------------
40
+
41
+ /**
42
+ * A component that creates a context menu activated by right clicking or long pressing.
43
+ * Doesn't render its own HTML element.
44
+ */
45
+ function ContextMenuRoot(props: any): any {
46
+ const slot = S('ContextMenuRoot');
47
+ const [anchor, setAnchor] = useState<ContextMenuRootContextValue['anchor']>(
48
+ {
49
+ getBoundingClientRect() {
50
+ return DOMRect.fromRect({ width: 0, height: 0, x: 0, y: 0 });
51
+ },
52
+ },
53
+ subSlot(slot, 'anchor'),
54
+ );
55
+
56
+ const backdropRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'backdrop'));
57
+ const internalBackdropRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'ibackdrop'));
58
+ const actionsRef = useRef<ContextMenuRootContextValue['actionsRef']['current']>(
59
+ null,
60
+ subSlot(slot, 'actions'),
61
+ );
62
+ const positionerRef = useRef<HTMLElement | null>(null, subSlot(slot, 'positioner'));
63
+ const allowMouseUpTriggerRef = useRef(true, subSlot(slot, 'amut'));
64
+ const initialCursorPointRef = useRef<{ x: number; y: number } | null>(
65
+ null,
66
+ subSlot(slot, 'cursor'),
67
+ );
68
+ // Raw `useId` (no `base-ui-` prefix), matching Base UI's `@base-ui/utils/useId`.
69
+ const id = useId(subSlot(slot, 'id'));
70
+
71
+ const contextValue: ContextMenuRootContextValue = useMemo(
72
+ () => ({
73
+ anchor,
74
+ setAnchor,
75
+ actionsRef,
76
+ backdropRef,
77
+ internalBackdropRef,
78
+ positionerRef,
79
+ allowMouseUpTriggerRef,
80
+ initialCursorPointRef,
81
+ rootId: id,
82
+ }),
83
+ [anchor, id],
84
+ subSlot(slot, 'ctx'),
85
+ );
86
+
87
+ // The inner `MenuRootContext.Provider value={undefined}` is load-bearing, not defensive: it is
88
+ // how `MenuRoot` tells "this Menu IS the context menu" apart from "this Menu is nested inside a
89
+ // ContextMenu.Trigger" (the `contextMenuContext && !parentMenuRootContext` guard).
90
+ return createElement(ContextMenuRootContext.Provider, {
91
+ value: contextValue,
92
+ children: createElement(MenuRootContext.Provider, {
93
+ value: undefined,
94
+ children: createElement(Menu.Root, props),
95
+ }),
96
+ });
97
+ }
98
+
99
+ // --- Trigger ------------------------------------------------------------------
100
+
101
+ /**
102
+ * An area that opens the menu on right click or long press.
103
+ * Renders a `<div>` element.
104
+ */
105
+ function ContextMenuTrigger(componentProps: any): any {
106
+ const slot = S('ContextMenuTrigger');
107
+ const { render, className, style, ref: forwardedRef, ...elementProps } = componentProps;
108
+
109
+ const {
110
+ setAnchor,
111
+ actionsRef,
112
+ internalBackdropRef,
113
+ backdropRef,
114
+ positionerRef,
115
+ allowMouseUpTriggerRef,
116
+ initialCursorPointRef,
117
+ rootId,
118
+ } = useContextMenuRootContext(false);
119
+
120
+ const { store } = useMenuRootContext() as MenuRootContextValue;
121
+ const open = store.useState('open', subSlot(slot, 'open'));
122
+ const disabled = store.useState('disabled', subSlot(slot, 'disabled'));
123
+
124
+ const triggerRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'trigger'));
125
+ const touchPositionRef = useRef<{ x: number; y: number } | null>(null, subSlot(slot, 'touch'));
126
+ const longPressTimeout = useTimeout(subSlot(slot, 'lp'));
127
+ const allowMouseUpTimeout = useTimeout(subSlot(slot, 'amu'));
128
+ const allowMouseUpRef = useRef(false, subSlot(slot, 'amur'));
129
+
130
+ function handleLongPress(x: number, y: number, event: MouseEvent | TouchEvent) {
131
+ const isTouchEvent = event.type.startsWith('touch');
132
+
133
+ initialCursorPointRef.current = { x, y };
134
+
135
+ setAnchor({
136
+ getBoundingClientRect() {
137
+ return DOMRect.fromRect({
138
+ width: isTouchEvent ? 10 : 0,
139
+ height: isTouchEvent ? 10 : 0,
140
+ x,
141
+ y,
142
+ });
143
+ },
144
+ });
145
+
146
+ allowMouseUpRef.current = false;
147
+ actionsRef.current?.setOpen(true, createChangeEventDetails(REASONS.triggerPress, event));
148
+
149
+ allowMouseUpTimeout.start(LONG_PRESS_DELAY, () => {
150
+ allowMouseUpRef.current = true;
151
+ });
152
+ }
153
+
154
+ function handleContextMenu(event: MouseEvent) {
155
+ if (disabled) {
156
+ return;
157
+ }
158
+ allowMouseUpTriggerRef.current = true;
159
+ stopEvent(event);
160
+ // octane dispatches NATIVE events, so upstream's `event.nativeEvent` collapses to `event`.
161
+ handleLongPress(event.clientX, event.clientY, event);
162
+ const doc = ownerDocument(triggerRef.current);
163
+
164
+ addEventListener(
165
+ doc,
166
+ 'mouseup',
167
+ (mouseEvent: Event) => {
168
+ allowMouseUpTriggerRef.current = false;
169
+
170
+ if (!allowMouseUpRef.current) {
171
+ return;
172
+ }
173
+
174
+ allowMouseUpTimeout.clear();
175
+ allowMouseUpRef.current = false;
176
+
177
+ const mouseUpTarget = getTarget(mouseEvent) as Element | null;
178
+
179
+ if (contains(positionerRef.current, mouseUpTarget)) {
180
+ return;
181
+ }
182
+
183
+ if (rootId && mouseUpTarget && findRootOwnerId(mouseUpTarget) === rootId) {
184
+ return;
185
+ }
186
+
187
+ actionsRef.current?.setOpen(
188
+ false,
189
+ createChangeEventDetails(REASONS.cancelOpen, mouseEvent),
190
+ );
191
+ },
192
+ { once: true },
193
+ );
194
+ }
195
+
196
+ function handleTouchStart(event: TouchEvent) {
197
+ if (disabled) {
198
+ return;
199
+ }
200
+ allowMouseUpTriggerRef.current = false;
201
+ if (event.touches.length === 1) {
202
+ event.stopPropagation();
203
+ const touch = event.touches[0];
204
+ touchPositionRef.current = { x: touch.clientX, y: touch.clientY };
205
+ longPressTimeout.start(LONG_PRESS_DELAY, () => {
206
+ if (touchPositionRef.current) {
207
+ handleLongPress(touchPositionRef.current.x, touchPositionRef.current.y, event);
208
+ }
209
+ });
210
+ }
211
+ }
212
+
213
+ function handleTouchMove(event: TouchEvent) {
214
+ if (longPressTimeout.isStarted() && touchPositionRef.current && event.touches.length === 1) {
215
+ const touch = event.touches[0];
216
+ const moveThreshold = 10;
217
+
218
+ const deltaX = Math.abs(touch.clientX - touchPositionRef.current.x);
219
+ const deltaY = Math.abs(touch.clientY - touchPositionRef.current.y);
220
+
221
+ if (deltaX > moveThreshold || deltaY > moveThreshold) {
222
+ longPressTimeout.clear();
223
+ }
224
+ }
225
+ }
226
+
227
+ function handleTouchEnd() {
228
+ longPressTimeout.clear();
229
+ touchPositionRef.current = null;
230
+ }
231
+
232
+ useEffect(
233
+ () => {
234
+ function handleDocumentContextMenu(event: Event) {
235
+ if (disabled) {
236
+ return;
237
+ }
238
+
239
+ const target = getTarget(event);
240
+ const targetElement = target as HTMLElement | null;
241
+ if (
242
+ contains(triggerRef.current, targetElement) ||
243
+ contains(internalBackdropRef.current, targetElement) ||
244
+ contains(backdropRef.current, targetElement)
245
+ ) {
246
+ event.preventDefault();
247
+ }
248
+ }
249
+
250
+ const doc = ownerDocument(triggerRef.current);
251
+ return addEventListener(doc, 'contextmenu', handleDocumentContextMenu);
252
+ },
253
+ [backdropRef, disabled, internalBackdropRef],
254
+ subSlot(slot, 'e:ctxmenu'),
255
+ );
256
+
257
+ const state: ContextMenuTriggerState = {
258
+ open,
259
+ };
260
+
261
+ return useRenderElement(
262
+ 'div',
263
+ componentProps,
264
+ {
265
+ state,
266
+ ref: [triggerRef, forwardedRef],
267
+ props: [
268
+ {
269
+ onContextMenu: handleContextMenu,
270
+ onTouchStart: handleTouchStart,
271
+ onTouchMove: handleTouchMove,
272
+ onTouchEnd: handleTouchEnd,
273
+ onTouchCancel: handleTouchEnd,
274
+ style: {
275
+ WebkitTouchCallout: 'none',
276
+ },
277
+ },
278
+ elementProps,
279
+ ],
280
+ stateAttributesMapping: pressableTriggerOpenStateMapping,
281
+ },
282
+ subSlot(slot, 're'),
283
+ );
284
+ }
285
+
286
+ export interface ContextMenuTriggerState {
287
+ /** Whether the context menu is currently open. */
288
+ open: boolean;
289
+ }
290
+
291
+ // --- Namespace ----------------------------------------------------------------
292
+
293
+ /**
294
+ * Mirrors upstream's `context-menu/index.parts.ts`: `Root` and `Trigger` are ContextMenu's own,
295
+ * everything else is `Menu`'s part re-exported here.
296
+ */
297
+ export const ContextMenu = {
298
+ Root: ContextMenuRoot,
299
+ Trigger: ContextMenuTrigger,
300
+ Backdrop: Menu.Backdrop,
301
+ Portal: Menu.Portal,
302
+ Positioner: Menu.Positioner,
303
+ Popup: Menu.Popup,
304
+ Arrow: Menu.Arrow,
305
+ Group: Menu.Group,
306
+ GroupLabel: Menu.GroupLabel,
307
+ Item: Menu.Item,
308
+ CheckboxItem: Menu.CheckboxItem,
309
+ CheckboxItemIndicator: Menu.CheckboxItemIndicator,
310
+ LinkItem: Menu.LinkItem,
311
+ RadioGroup: Menu.RadioGroup,
312
+ RadioItem: Menu.RadioItem,
313
+ RadioItemIndicator: Menu.RadioItemIndicator,
314
+ SubmenuRoot: Menu.SubmenuRoot,
315
+ SubmenuTrigger: Menu.SubmenuTrigger,
316
+ Separator: Menu.Separator,
317
+ };
@@ -0,0 +1,37 @@
1
+ // Ported from .base-ui/packages/react/src/csp-provider/CSPProvider.tsx (v1.6.0). Provides a
2
+ // default Content Security Policy configuration for Base UI components that require inline
3
+ // `<style>` or `<script>` tags.
4
+ import { createElement, useMemo } from 'octane';
5
+
6
+ import { S, subSlot } from './internal';
7
+ import { CSPContext } from './utils/CSPContext';
8
+ import type { CSPContextValue } from './utils/CSPContext';
9
+
10
+ export interface CSPProviderProps {
11
+ children?: any;
12
+ /** The nonce value to apply to inline `<style>` and `<script>` tags. */
13
+ nonce?: string;
14
+ /**
15
+ * Whether inline `<style>` elements created by Base UI components should not be rendered.
16
+ * Instead, components must specify the CSS styles via custom class names or other methods.
17
+ * @default false
18
+ */
19
+ disableStyleElements?: boolean;
20
+ }
21
+
22
+ export function CSPProvider(props: CSPProviderProps): any {
23
+ const slot = S('CSPProvider');
24
+ const { children, nonce, disableStyleElements } = props;
25
+
26
+ const contextValue: CSPContextValue = useMemo(
27
+ () => ({ nonce, disableStyleElements }),
28
+ [nonce, disableStyleElements],
29
+ subSlot(slot, 'ctx'),
30
+ );
31
+
32
+ return createElement(CSPContext.Provider, { value: contextValue, children });
33
+ }
34
+
35
+ export namespace CSPProvider {
36
+ export type Props = CSPProviderProps;
37
+ }
package/src/dialog.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  import {
10
10
  createContext,
11
11
  createElement,
12
+ Fragment,
12
13
  useContext,
13
14
  useMemo,
14
15
  useRef,
@@ -33,7 +34,11 @@ import { useDismiss } from './utils/floating/useDismiss';
33
34
  import { useScrollLock } from './utils/useScrollLock';
34
35
  import { useOpenChangeComplete } from './utils/useOpenChangeComplete';
35
36
  import { useOpenMethodTriggerProps } from './utils/useOpenInteractionType';
36
- import { triggerOpenStateMapping, popupStateMapping } from './utils/popupStateMapping';
37
+ import {
38
+ triggerOpenStateMapping,
39
+ popupStateMapping,
40
+ CommonPopupDataAttributes,
41
+ } from './utils/popupStateMapping';
37
42
  import { transitionStatusMapping } from './utils/useTransitionStatus';
38
43
  import type { StateAttributesMapping } from './utils/getStateAttributesProps';
39
44
  import { EMPTY_OBJECT } from './utils/empty';
@@ -404,14 +409,7 @@ function DialogInteractions(props: any): any {
404
409
  subSlot(slot, 'pip'),
405
410
  );
406
411
 
407
- // Renders the Dialog's children (see the note at the `content` call site). No DOM of its own.
408
- return props.children ?? null;
409
- }
410
-
411
- // Passthrough used when the interactions aren't mounted, to keep the Provider's children a stable
412
- // element-descriptor shape (see the note at the `content` call site).
413
- function DialogChildren(props: any): any {
414
- return props.children ?? null;
412
+ return null;
415
413
  }
416
414
 
417
415
  export function useRenderDialogRoot<Payload>(
@@ -482,19 +480,19 @@ export function useRenderDialogRoot<Payload>(
482
480
  const resolvedChildren =
483
481
  typeof children === 'function' && !isChildrenBlock(children) ? children({ payload }) : children;
484
482
 
485
- // The Provider's `children` must stay a STABLE shape (always an element descriptor) across
486
- // renders: octane's `childrenAsBody` runs a render-FUNCTION child directly in the Provider scope
487
- // (owning `scope.slots`) but routes a DESCRIPTOR child through `childSlot(scope, 0)` — alternating
488
- // the two across renders (a raw children-block when closed vs a component when open) collides those
489
- // slot namespaces and crashes octane's reconciler (see octane bug note). So a no-DOM component
490
- // wraps the children in BOTH states: `DialogInteractions` (runs the open-state hooks) when
491
- // open/mounted, else `DialogChildren` (a passthrough). Both just render `props.children`.
492
- const content = createElement(shouldRenderInteractions ? DialogInteractions : DialogChildren, {
493
- store,
494
- parentContext: parentDialogRootContext?.store.context,
495
- isDrawer,
496
- children: resolvedChildren,
497
- });
483
+ // `DialogInteractions` is a headless SIBLING of the children, exactly as Base UI renders it, so
484
+ // toggling it never disturbs them.
485
+ const content = [
486
+ shouldRenderInteractions
487
+ ? createElement(DialogInteractions, {
488
+ key: 'interactions',
489
+ store,
490
+ parentContext: parentDialogRootContext?.store.context,
491
+ isDrawer,
492
+ })
493
+ : null,
494
+ createElement(Fragment, { key: 'children', children: resolvedChildren }),
495
+ ];
498
496
 
499
497
  return createElement(IsDrawerContext.Provider, {
500
498
  value: false,
@@ -844,6 +842,73 @@ function DialogClose(componentProps: any): any {
844
842
  );
845
843
  }
846
844
 
845
+ // --- Viewport ----------------------------------------------------------------
846
+
847
+ export const DialogViewportDataAttributes = {
848
+ open: CommonPopupDataAttributes.open,
849
+ closed: CommonPopupDataAttributes.closed,
850
+ startingStyle: CommonPopupDataAttributes.startingStyle,
851
+ endingStyle: CommonPopupDataAttributes.endingStyle,
852
+ nested: 'data-nested',
853
+ nestedDialogOpen: 'data-nested-dialog-open',
854
+ } as const;
855
+
856
+ const viewportStateAttributesMapping: StateAttributesMapping<any> = {
857
+ ...(popupStateMapping as StateAttributesMapping<any>),
858
+ ...(transitionStatusMapping as StateAttributesMapping<any>),
859
+ nested(value: boolean) {
860
+ return value ? { [DialogViewportDataAttributes.nested]: '' } : null;
861
+ },
862
+ nestedDialogOpen(value: boolean) {
863
+ return value ? { [DialogViewportDataAttributes.nestedDialogOpen]: '' } : null;
864
+ },
865
+ };
866
+
867
+ // A positioning container for the dialog popup that can be made scrollable.
868
+ function DialogViewport(componentProps: any): any {
869
+ const slot = S('DialogViewport');
870
+ const { render, className, style, children, ref, ...elementProps } = componentProps;
871
+
872
+ const keepMounted = useDialogPortalContext();
873
+ const { store } = useDialogRootContext() as DialogRootContextValue;
874
+
875
+ const open = store.useState('open', subSlot(slot, 'open'));
876
+ const nested = store.useState('nested', subSlot(slot, 'nested'));
877
+ const transitionStatus = store.useState('transitionStatus', subSlot(slot, 'ts'));
878
+ const nestedOpenDialogCount = store.useState('nestedOpenDialogCount', subSlot(slot, 'nodc'));
879
+ const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
880
+
881
+ const setViewportElement = store.useStateSetter('viewportElement', subSlot(slot, 'sve'));
882
+
883
+ const state = {
884
+ open,
885
+ nested,
886
+ transitionStatus,
887
+ nestedDialogOpen: nestedOpenDialogCount > 0,
888
+ };
889
+
890
+ return useRenderElement(
891
+ 'div',
892
+ { render, className, style },
893
+ {
894
+ enabled: keepMounted || mounted,
895
+ state,
896
+ ref: [ref, setViewportElement],
897
+ stateAttributesMapping: viewportStateAttributesMapping,
898
+ props: [
899
+ {
900
+ role: 'presentation',
901
+ hidden: !mounted,
902
+ style: { pointerEvents: !open ? 'none' : undefined },
903
+ children,
904
+ },
905
+ elementProps,
906
+ ],
907
+ },
908
+ subSlot(slot, 're'),
909
+ );
910
+ }
911
+
847
912
  // --- Namespace ---------------------------------------------------------------
848
913
 
849
914
  export const Dialog = {
@@ -852,8 +917,10 @@ export const Dialog = {
852
917
  Portal: DialogPortal,
853
918
  Backdrop: DialogBackdrop,
854
919
  Popup: DialogPopup,
920
+ Viewport: DialogViewport,
855
921
  Title: DialogTitle,
856
922
  Description: DialogDescription,
857
923
  Close: DialogClose,
858
924
  createHandle: createDialogHandle,
925
+ Handle: DialogHandle,
859
926
  };
@@ -0,0 +1,32 @@
1
+ // Ported from .base-ui/packages/react/src/direction-provider/DirectionProvider.tsx (v1.6.0).
2
+ // Enables RTL behavior for Base UI components.
3
+ //
4
+ // octane adaptation: `utils/DirectionContext` carries the `TextDirection` string DIRECTLY
5
+ // rather than Base UI's `{ direction }` wrapper object, so the provider needs no `useMemo`
6
+ // to stay referentially stable. The context value is never observable in the DOM, and every
7
+ // consumer reads it through `useDirection()`.
8
+ import { createElement } from 'octane';
9
+
10
+ import { DirectionContext, useDirection } from './utils/DirectionContext';
11
+ import type { TextDirection } from './utils/DirectionContext';
12
+
13
+ export type { TextDirection };
14
+ export { useDirection };
15
+
16
+ export interface DirectionProviderProps {
17
+ children?: any;
18
+ /**
19
+ * The reading direction of the text.
20
+ * @default 'ltr'
21
+ */
22
+ direction?: TextDirection;
23
+ }
24
+
25
+ export function DirectionProvider(props: DirectionProviderProps): any {
26
+ const { direction = 'ltr' } = props;
27
+ return createElement(DirectionContext.Provider, { value: direction, children: props.children });
28
+ }
29
+
30
+ export namespace DirectionProvider {
31
+ export type Props = DirectionProviderProps;
32
+ }
package/src/index.ts CHANGED
@@ -5,19 +5,29 @@
5
5
  export { Separator } from './separator';
6
6
  export { AlertDialog } from './alert-dialog';
7
7
  export { Avatar } from './avatar';
8
+ export { Button } from './button';
9
+ export { CSPProvider } from './csp-provider';
10
+ export { DirectionProvider, useDirection } from './direction-provider';
11
+ export type { TextDirection } from './direction-provider';
12
+ export { useMediaQuery } from './unstable-use-media-query';
8
13
  export { Checkbox } from './checkbox';
14
+ export { ContextMenu } from './context-menu';
9
15
  export { CheckboxGroup } from './checkbox-group';
10
16
  export { Dialog } from './dialog';
11
17
  export { Field } from './field';
12
18
  export { Fieldset } from './fieldset';
13
19
  export { Form } from './form';
14
20
  export { Input } from './input';
21
+ export { Menu } from './menu';
22
+ export { Menubar } from './menubar';
15
23
  export { Meter } from './meter';
16
24
  export { NumberField } from './number-field';
17
25
  export { Progress } from './progress';
18
26
  export { Radio } from './radio';
19
27
  export { RadioGroup } from './radio-group';
20
28
  export { Popover } from './popover';
29
+ export { PreviewCard } from './preview-card';
30
+ export { Tooltip } from './tooltip';
21
31
  export { Slider } from './slider';
22
32
  export { Switch } from './switch';
23
33
  export { Toggle } from './toggle';