@octanejs/base-ui 0.1.1 → 0.1.3

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 (67) hide show
  1. package/README.md +7 -1
  2. package/package.json +12 -4
  3. package/src/alert-dialog.ts +47 -0
  4. package/src/checkbox.ts +7 -16
  5. package/src/dialog.ts +859 -0
  6. package/src/field.ts +4 -30
  7. package/src/index.ts +3 -0
  8. package/src/number-field.ts +8 -45
  9. package/src/popover.ts +1205 -0
  10. package/src/radio.ts +3 -24
  11. package/src/slider.ts +4 -5
  12. package/src/switch.ts +4 -30
  13. package/src/utils/InternalBackdrop.ts +28 -0
  14. package/src/utils/adaptiveOriginMiddleware.ts +82 -0
  15. package/src/utils/closePart.ts +61 -0
  16. package/src/utils/constants.ts +9 -0
  17. package/src/utils/createChangeEventDetails.ts +7 -0
  18. package/src/utils/dom.ts +9 -0
  19. package/src/utils/empty.ts +7 -0
  20. package/src/utils/floating/FloatingFocusManager.ts +833 -0
  21. package/src/utils/floating/FloatingPortal.ts +268 -0
  22. package/src/utils/floating/FloatingRootStore.ts +127 -0
  23. package/src/utils/floating/FloatingTree.ts +70 -0
  24. package/src/utils/floating/FloatingTreeStore.ts +22 -0
  25. package/src/utils/floating/FocusGuard.ts +34 -0
  26. package/src/utils/floating/composite.ts +20 -0
  27. package/src/utils/floating/constants.ts +8 -0
  28. package/src/utils/floating/createAttribute.ts +4 -0
  29. package/src/utils/floating/createEventEmitter.ts +20 -0
  30. package/src/utils/floating/element.ts +54 -0
  31. package/src/utils/floating/enqueueFocus.ts +38 -0
  32. package/src/utils/floating/event.ts +53 -0
  33. package/src/utils/floating/getEmptyRootContext.ts +19 -0
  34. package/src/utils/floating/markOthers.ts +197 -0
  35. package/src/utils/floating/nodes.ts +31 -0
  36. package/src/utils/floating/tabbable.ts +260 -0
  37. package/src/utils/floating/types.ts +57 -0
  38. package/src/utils/floating/useClick.ts +174 -0
  39. package/src/utils/floating/useDismiss.ts +641 -0
  40. package/src/utils/floating/useFloating.ts +198 -0
  41. package/src/utils/floating/useFloatingRootContext.ts +81 -0
  42. package/src/utils/floating/useHoverFloatingInteraction.ts +12 -0
  43. package/src/utils/floating/useHoverReferenceInteraction.ts +17 -0
  44. package/src/utils/floating/useSyncedFloatingRootContext.ts +93 -0
  45. package/src/utils/getDisabledMountTransitionStyles.ts +12 -0
  46. package/src/utils/hideMiddleware.ts +22 -0
  47. package/src/utils/inertValue.ts +5 -0
  48. package/src/utils/mergeCleanups.ts +13 -0
  49. package/src/utils/platform.ts +46 -2
  50. package/src/utils/popupStateMapping.ts +48 -0
  51. package/src/utils/popups/index.ts +4 -0
  52. package/src/utils/popups/popupStoreUtils.ts +484 -0
  53. package/src/utils/popups/popupTriggerMap.ts +62 -0
  54. package/src/utils/popups/store.ts +147 -0
  55. package/src/utils/popups/useTriggerFocusGuards.ts +73 -0
  56. package/src/utils/store/ReactStore.ts +168 -0
  57. package/src/utils/store/Store.ts +84 -0
  58. package/src/utils/store/createSelector.ts +66 -0
  59. package/src/utils/store/useStore.ts +49 -0
  60. package/src/utils/useAnchorPositioning.ts +581 -0
  61. package/src/utils/useAnchoredPopupScrollLock.ts +50 -0
  62. package/src/utils/useAnimationFrame.ts +28 -5
  63. package/src/utils/useEnhancedClickHandler.ts +47 -0
  64. package/src/utils/useOnFirstRender.ts +11 -0
  65. package/src/utils/useOpenInteractionType.ts +32 -0
  66. package/src/utils/usePositioner.ts +48 -0
  67. package/src/utils/useScrollLock.ts +253 -0
package/src/dialog.ts ADDED
@@ -0,0 +1,859 @@
1
+ // Ported from .base-ui/packages/react/src/dialog/ (v1.6.0): store/DialogStore + store/DialogHandle,
2
+ // root (context/useDialogRoot/useRenderDialogRoot/DialogRoot + DialogInteractions), trigger, portal,
3
+ // backdrop, popup, title, description, close. octane adaptations: forwardRef → ref-as-prop; native
4
+ // events; every Store hook-method + hook threads an explicit slot; `React.createContext` →
5
+ // octane `createContext`.
6
+ //
7
+ // `DialogPortal`/`DialogPopup` use Base UI's own FloatingPortal + FloatingFocusManager (ported to
8
+ // `utils/floating/`, emitting `data-base-ui-*`) fed the FloatingRootStore directly as `context`.
9
+ import {
10
+ createContext,
11
+ createElement,
12
+ useContext,
13
+ useMemo,
14
+ useRef,
15
+ useState,
16
+ useEffect,
17
+ useCallback,
18
+ useImperativeHandle,
19
+ isChildrenBlock,
20
+ } from 'octane';
21
+ import { FloatingFocusManager } from './utils/floating/FloatingFocusManager';
22
+ import { FloatingPortal } from './utils/floating/FloatingPortal';
23
+
24
+ import { S, subSlot } from './internal';
25
+ import { useRenderElement } from './utils/useRenderElement';
26
+ import { useButton } from './utils/useButton';
27
+ import { useBaseUiId } from './utils/useBaseUiId';
28
+ import { createChangeEventDetails, REASONS } from './utils/createChangeEventDetails';
29
+ import { ReactStore } from './utils/store/ReactStore';
30
+ import { createSelector } from './utils/store/createSelector';
31
+ import { useClick } from './utils/floating/useClick';
32
+ import { useDismiss } from './utils/floating/useDismiss';
33
+ import { useScrollLock } from './utils/useScrollLock';
34
+ import { useOpenChangeComplete } from './utils/useOpenChangeComplete';
35
+ import { useOpenMethodTriggerProps } from './utils/useOpenInteractionType';
36
+ import { triggerOpenStateMapping, popupStateMapping } from './utils/popupStateMapping';
37
+ import { transitionStatusMapping } from './utils/useTransitionStatus';
38
+ import type { StateAttributesMapping } from './utils/getStateAttributesProps';
39
+ import { EMPTY_OBJECT } from './utils/empty';
40
+ import { inertValue } from './utils/inertValue';
41
+ import { InternalBackdrop } from './utils/InternalBackdrop';
42
+ import { contains, getTarget } from './utils/floating/element';
43
+ import { COMPOSITE_KEYS } from './utils/composite/keys';
44
+ import {
45
+ createInitialPopupStoreState,
46
+ createPopupFloatingRootContext,
47
+ popupStoreSelectors,
48
+ setPopupOpenState,
49
+ usePopupStore,
50
+ useImplicitActiveTrigger,
51
+ useOpenStateTransitions,
52
+ usePopupRootSync,
53
+ usePopupInteractionProps,
54
+ useTriggerDataForwarding,
55
+ createDefaultInitialFocus,
56
+ FOCUSABLE_POPUP_PROPS,
57
+ type PopupStoreState,
58
+ type PopupStoreContext,
59
+ } from './utils/popups';
60
+ import { PopupTriggerMap } from './utils/popups/popupTriggerMap';
61
+ import type { InteractionType } from './utils/useEnhancedClickHandler';
62
+
63
+ const CLICK_TRIGGER_IDENTIFIER = 'data-base-ui-click-trigger';
64
+
65
+ const popupStateAttributesMapping: StateAttributesMapping<any> = {
66
+ ...(popupStateMapping as StateAttributesMapping<any>),
67
+ ...(transitionStatusMapping as StateAttributesMapping<any>),
68
+ };
69
+
70
+ // --- Store -------------------------------------------------------------------
71
+
72
+ export type DialogState<Payload> = PopupStoreState<Payload> & {
73
+ modal: boolean | 'trap-focus';
74
+ disablePointerDismissal: boolean;
75
+ openMethod: InteractionType | null;
76
+ nested: boolean;
77
+ nestedOpenDialogCount: number;
78
+ nestedOpenDrawerCount: number;
79
+ titleElementId: string | undefined;
80
+ descriptionElementId: string | undefined;
81
+ viewportElement: HTMLElement | null;
82
+ role: 'dialog' | 'alertdialog';
83
+ };
84
+
85
+ type DialogContext = PopupStoreContext<any> & {
86
+ readonly popupRef: { current: HTMLElement | null };
87
+ readonly backdropRef: { current: HTMLDivElement | null };
88
+ readonly internalBackdropRef: { current: HTMLDivElement | null };
89
+ readonly outsidePressEnabledRef: { current: boolean };
90
+ onNestedDialogOpen?: ((dialogCount: number, drawerCount: number) => void) | undefined;
91
+ onNestedDialogClose?: (() => void) | undefined;
92
+ };
93
+
94
+ const dialogSelectors = {
95
+ ...popupStoreSelectors,
96
+ modal: createSelector((state: DialogState<unknown>) => state.modal),
97
+ nested: createSelector((state: DialogState<unknown>) => state.nested),
98
+ nestedOpenDialogCount: createSelector(
99
+ (state: DialogState<unknown>) => state.nestedOpenDialogCount,
100
+ ),
101
+ nestedOpenDrawerCount: createSelector(
102
+ (state: DialogState<unknown>) => state.nestedOpenDrawerCount,
103
+ ),
104
+ disablePointerDismissal: createSelector(
105
+ (state: DialogState<unknown>) => state.disablePointerDismissal,
106
+ ),
107
+ openMethod: createSelector((state: DialogState<unknown>) => state.openMethod),
108
+ descriptionElementId: createSelector((state: DialogState<unknown>) => state.descriptionElementId),
109
+ titleElementId: createSelector((state: DialogState<unknown>) => state.titleElementId),
110
+ viewportElement: createSelector((state: DialogState<unknown>) => state.viewportElement),
111
+ role: createSelector((state: DialogState<unknown>) => state.role),
112
+ };
113
+
114
+ function createInitialDialogState<Payload>(
115
+ initialState: Partial<DialogState<Payload>> = {},
116
+ ): DialogState<Payload> {
117
+ return {
118
+ ...createInitialPopupStoreState<Payload>(),
119
+ modal: true,
120
+ disablePointerDismissal: false,
121
+ popupElement: null,
122
+ viewportElement: null,
123
+ descriptionElementId: undefined,
124
+ titleElementId: undefined,
125
+ openMethod: null,
126
+ nested: false,
127
+ nestedOpenDialogCount: 0,
128
+ nestedOpenDrawerCount: 0,
129
+ role: 'dialog',
130
+ ...initialState,
131
+ };
132
+ }
133
+
134
+ export class DialogStore<Payload> extends ReactStore<
135
+ Readonly<DialogState<Payload>>,
136
+ DialogContext,
137
+ typeof dialogSelectors
138
+ > {
139
+ constructor(
140
+ initialState?: Partial<DialogState<Payload>>,
141
+ floatingId?: string | undefined,
142
+ nested = false,
143
+ ) {
144
+ const triggerElements = new PopupTriggerMap();
145
+ const state = createInitialDialogState<Payload>(initialState);
146
+ state.floatingRootContext = createPopupFloatingRootContext(triggerElements, floatingId, nested);
147
+
148
+ super(
149
+ state,
150
+ {
151
+ popupRef: { current: null },
152
+ backdropRef: { current: null },
153
+ internalBackdropRef: { current: null },
154
+ outsidePressEnabledRef: { current: true },
155
+ triggerElements,
156
+ onOpenChange: undefined,
157
+ onOpenChangeComplete: undefined,
158
+ } as DialogContext,
159
+ dialogSelectors,
160
+ );
161
+ }
162
+
163
+ setOpen = (nextOpen: boolean, eventDetails: any) => {
164
+ eventDetails.preventUnmountOnClose = () => {
165
+ this.set('preventUnmountingOnClose', true);
166
+ };
167
+
168
+ if (!nextOpen && eventDetails.trigger == null && this.state.activeTriggerId != null) {
169
+ eventDetails.trigger = this.state.activeTriggerElement ?? undefined;
170
+ }
171
+
172
+ this.context.onOpenChange?.(nextOpen, eventDetails);
173
+
174
+ if (eventDetails.isCanceled) {
175
+ return;
176
+ }
177
+
178
+ (this.state.floatingRootContext as any).dispatchOpenChange(nextOpen, eventDetails);
179
+
180
+ const updatedState: Partial<DialogState<Payload>> = { open: nextOpen };
181
+ setPopupOpenState(updatedState as any, nextOpen, eventDetails.trigger);
182
+ this.update(updatedState);
183
+ };
184
+
185
+ static useStore<Payload>(
186
+ externalStore: DialogStore<Payload> | undefined,
187
+ initialState: Partial<DialogState<Payload>>,
188
+ slot: symbol | undefined,
189
+ ): DialogStore<Payload> {
190
+ return usePopupStore(
191
+ externalStore as any,
192
+ (floatingId, nested) => new DialogStore<Payload>(initialState, floatingId, nested) as any,
193
+ true,
194
+ slot,
195
+ ).store as unknown as DialogStore<Payload>;
196
+ }
197
+ }
198
+
199
+ // --- Handle ------------------------------------------------------------------
200
+
201
+ export class DialogHandle<Payload> {
202
+ readonly store: DialogStore<Payload>;
203
+
204
+ constructor(store?: DialogStore<Payload>) {
205
+ this.store = store ?? new DialogStore<Payload>();
206
+ }
207
+
208
+ open(triggerId: string | null) {
209
+ const triggerElement = triggerId
210
+ ? (this.store.context.triggerElements.getById(triggerId) as HTMLElement | undefined)
211
+ : undefined;
212
+ this.store.setOpen(
213
+ true,
214
+ createChangeEventDetails(REASONS.imperativeAction, undefined, triggerElement),
215
+ );
216
+ }
217
+
218
+ openWithPayload(payload: Payload) {
219
+ this.store.set('payload', payload);
220
+ this.store.setOpen(
221
+ true,
222
+ createChangeEventDetails(REASONS.imperativeAction, undefined, undefined),
223
+ );
224
+ }
225
+
226
+ close() {
227
+ this.store.setOpen(
228
+ false,
229
+ createChangeEventDetails(REASONS.imperativeAction, undefined, undefined),
230
+ );
231
+ }
232
+
233
+ get isOpen() {
234
+ return this.store.select('open');
235
+ }
236
+ }
237
+
238
+ export function createDialogHandle<Payload>(): DialogHandle<Payload> {
239
+ return new DialogHandle<Payload>();
240
+ }
241
+
242
+ // --- Context -----------------------------------------------------------------
243
+
244
+ export interface DialogRootContextValue<Payload = unknown> {
245
+ store: DialogStore<Payload>;
246
+ }
247
+
248
+ const IsDrawerContext = createContext(false);
249
+ const DialogRootContext = createContext<DialogRootContextValue | undefined>(undefined);
250
+
251
+ export function useDialogRootContext(optional?: boolean): DialogRootContextValue | undefined {
252
+ const dialogRootContext = useContext(DialogRootContext);
253
+ if (optional === false && dialogRootContext === undefined) {
254
+ throw new Error(
255
+ 'Base UI: DialogRootContext is missing. Dialog parts must be placed within <Dialog.Root>.',
256
+ );
257
+ }
258
+ return dialogRootContext;
259
+ }
260
+
261
+ // --- Root --------------------------------------------------------------------
262
+
263
+ function useDialogRoot(
264
+ params: { store: DialogStore<any>; actionsRef?: any },
265
+ slot: symbol | undefined,
266
+ ) {
267
+ const { store, actionsRef } = params;
268
+
269
+ const open = store.useState('open', subSlot(slot, 'open'));
270
+ usePopupRootSync(store as any, open, subSlot(slot, 'rootSync'));
271
+ useImplicitActiveTrigger(store as any, {}, subSlot(slot, 'iat'));
272
+ const { forceUnmount } = useOpenStateTransitions(
273
+ open,
274
+ store as any,
275
+ undefined,
276
+ subSlot(slot, 'ost'),
277
+ );
278
+
279
+ const handleImperativeClose = useCallback(
280
+ () => {
281
+ store.setOpen(false, createChangeEventDetails(REASONS.imperativeAction));
282
+ },
283
+ [store],
284
+ subSlot(slot, 'close'),
285
+ );
286
+
287
+ useImperativeHandle(
288
+ actionsRef,
289
+ () => ({ unmount: forceUnmount, close: handleImperativeClose }),
290
+ [forceUnmount, handleImperativeClose],
291
+ subSlot(slot, 'imp'),
292
+ );
293
+ }
294
+
295
+ // Runs the open-state floating interactions (dismiss + scroll lock + nested-dialog bookkeeping),
296
+ // and syncs the resulting prop bags into the store. Rendered only when `open || mounted`.
297
+ function DialogInteractions(props: any): any {
298
+ const slot = S('DialogInteractions');
299
+ const { store, parentContext, isDrawer } = props;
300
+
301
+ const open = store.useState('open', subSlot(slot, 'open'));
302
+ const disablePointerDismissal = store.useState('disablePointerDismissal', subSlot(slot, 'dpd'));
303
+ const modal = store.useState('modal', subSlot(slot, 'modal'));
304
+ const popupElement = store.useState('popupElement', subSlot(slot, 'pel'));
305
+ const floatingRootContext = store.useState('floatingRootContext', subSlot(slot, 'frc'));
306
+
307
+ const [ownNestedOpenDialogs, setOwnNestedOpenDialogs] = useState(0, subSlot(slot, 'nod'));
308
+ const [ownNestedOpenDrawers, setOwnNestedOpenDrawers] = useState(0, subSlot(slot, 'ndr'));
309
+ const isTopmost = ownNestedOpenDialogs === 0;
310
+
311
+ const dismiss = useDismiss(
312
+ floatingRootContext,
313
+ {
314
+ outsidePressEvent() {
315
+ if (store.context.internalBackdropRef.current || store.context.backdropRef.current) {
316
+ return 'intentional';
317
+ }
318
+ return {
319
+ mouse: modal === 'trap-focus' ? 'sloppy' : 'intentional',
320
+ touch: 'sloppy',
321
+ };
322
+ },
323
+ outsidePress(event: any) {
324
+ if (!store.context.outsidePressEnabledRef.current) {
325
+ return false;
326
+ }
327
+ if ('button' in event && event.button !== 0) {
328
+ return false;
329
+ }
330
+ if ('touches' in event && event.touches.length !== 1) {
331
+ return false;
332
+ }
333
+ const target = getTarget(event) as Element | null;
334
+ if (isTopmost && !disablePointerDismissal) {
335
+ if (modal) {
336
+ return store.context.internalBackdropRef.current || store.context.backdropRef.current
337
+ ? store.context.internalBackdropRef.current === target ||
338
+ store.context.backdropRef.current === target ||
339
+ (contains(target, popupElement) && !target?.hasAttribute('data-base-ui-portal'))
340
+ : true;
341
+ }
342
+ return true;
343
+ }
344
+ return false;
345
+ },
346
+ escapeKey: isTopmost,
347
+ },
348
+ subSlot(slot, 'dismiss'),
349
+ );
350
+
351
+ useScrollLock(open && modal === true, popupElement, subSlot(slot, 'scroll'));
352
+
353
+ store.useContextCallback(
354
+ 'onNestedDialogOpen',
355
+ (dialogCount: number, drawerCount: number) => {
356
+ setOwnNestedOpenDialogs(dialogCount);
357
+ setOwnNestedOpenDrawers(drawerCount);
358
+ },
359
+ subSlot(slot, 'cc-open'),
360
+ );
361
+ store.useContextCallback(
362
+ 'onNestedDialogClose',
363
+ () => {
364
+ setOwnNestedOpenDialogs(0);
365
+ setOwnNestedOpenDrawers(0);
366
+ },
367
+ subSlot(slot, 'cc-close'),
368
+ );
369
+
370
+ useEffect(
371
+ () => {
372
+ if (parentContext?.onNestedDialogOpen && open) {
373
+ parentContext.onNestedDialogOpen(
374
+ ownNestedOpenDialogs + 1,
375
+ ownNestedOpenDrawers + (isDrawer ? 1 : 0),
376
+ );
377
+ }
378
+ if (parentContext?.onNestedDialogClose && !open) {
379
+ parentContext.onNestedDialogClose();
380
+ }
381
+ return () => {
382
+ if (parentContext?.onNestedDialogClose && open) {
383
+ parentContext.onNestedDialogClose();
384
+ }
385
+ };
386
+ },
387
+ [isDrawer, open, ownNestedOpenDialogs, ownNestedOpenDrawers, parentContext],
388
+ subSlot(slot, 'e:parent'),
389
+ );
390
+
391
+ const activeTriggerProps = dismiss.reference ?? EMPTY_OBJECT;
392
+ const inactiveTriggerProps = dismiss.trigger ?? EMPTY_OBJECT;
393
+ const popupProps = dismiss.floating ?? EMPTY_OBJECT;
394
+
395
+ usePopupInteractionProps(
396
+ store,
397
+ {
398
+ activeTriggerProps,
399
+ inactiveTriggerProps,
400
+ popupProps,
401
+ nestedOpenDialogCount: ownNestedOpenDialogs,
402
+ nestedOpenDrawerCount: ownNestedOpenDrawers,
403
+ } as any,
404
+ subSlot(slot, 'pip'),
405
+ );
406
+
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;
415
+ }
416
+
417
+ export function useRenderDialogRoot<Payload>(
418
+ props: any,
419
+ mode: 'dialog' | 'drawer' | 'alert-dialog',
420
+ ) {
421
+ const slot = S('DialogRoot');
422
+ const {
423
+ children,
424
+ open: openProp,
425
+ defaultOpen = false,
426
+ onOpenChange,
427
+ onOpenChangeComplete,
428
+ disablePointerDismissal: disablePointerDismissalProp = false,
429
+ modal: modalProp = true,
430
+ actionsRef,
431
+ handle,
432
+ triggerId: triggerIdProp,
433
+ defaultTriggerId: defaultTriggerIdProp = null,
434
+ } = props;
435
+
436
+ const isDrawer = mode === 'drawer';
437
+ const isAlertDialog = mode === 'alert-dialog';
438
+ const modal = isAlertDialog ? true : modalProp;
439
+ const disablePointerDismissal = isAlertDialog || disablePointerDismissalProp;
440
+ const role: 'dialog' | 'alertdialog' = isAlertDialog ? 'alertdialog' : 'dialog';
441
+
442
+ const parentDialogRootContext = useDialogRootContext(true) as DialogRootContextValue | undefined;
443
+ const nested = Boolean(parentDialogRootContext);
444
+ const rootState = { modal, disablePointerDismissal, nested, role };
445
+
446
+ const store = DialogStore.useStore<Payload>(
447
+ handle?.store,
448
+ {
449
+ open: defaultOpen,
450
+ openProp,
451
+ activeTriggerId: defaultTriggerIdProp,
452
+ triggerIdProp,
453
+ ...rootState,
454
+ } as Partial<DialogState<Payload>>,
455
+ subSlot(slot, 'store'),
456
+ );
457
+
458
+ store.useControlledProp('openProp', openProp, subSlot(slot, 'cp-open'));
459
+ store.useControlledProp('triggerIdProp', triggerIdProp, subSlot(slot, 'cp-tid'));
460
+
461
+ store.useSyncedValues(rootState as any, subSlot(slot, 'sv-root'));
462
+ store.useContextCallback('onOpenChange', onOpenChange, subSlot(slot, 'cc-change'));
463
+ store.useContextCallback(
464
+ 'onOpenChangeComplete',
465
+ onOpenChangeComplete,
466
+ subSlot(slot, 'cc-complete'),
467
+ );
468
+
469
+ const open = store.useState('open', subSlot(slot, 'open'));
470
+ const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
471
+ const payload = store.useState('payload', subSlot(slot, 'payload')) as Payload | undefined;
472
+
473
+ useDialogRoot({ store, actionsRef }, subSlot(slot, 'root'));
474
+
475
+ const shouldRenderInteractions = open || mounted;
476
+
477
+ const contextValue = useMemo(() => ({ store }), [store], subSlot(slot, 'ctx'));
478
+
479
+ // Base UI's `children` may be a payload render function (`{({ payload }) => …}`). octane passes
480
+ // a render-prop child RAW but compiles element/text children to a `markChildrenBlock`-tagged
481
+ // render fn — both are `typeof === 'function'`, so `isChildrenBlock` excludes the latter.
482
+ const resolvedChildren =
483
+ typeof children === 'function' && !isChildrenBlock(children) ? children({ payload }) : children;
484
+
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
+ });
498
+
499
+ return createElement(IsDrawerContext.Provider, {
500
+ value: false,
501
+ children: createElement(DialogRootContext.Provider, {
502
+ value: contextValue as DialogRootContextValue,
503
+ children: content,
504
+ }),
505
+ });
506
+ }
507
+
508
+ function DialogRoot<Payload>(props: any): any {
509
+ const mode = useContext(IsDrawerContext) ? 'drawer' : 'dialog';
510
+ return useRenderDialogRoot<Payload>(props, mode);
511
+ }
512
+
513
+ // --- Trigger -----------------------------------------------------------------
514
+
515
+ function DialogTrigger(componentProps: any): any {
516
+ const slot = S('DialogTrigger');
517
+ const {
518
+ render,
519
+ className,
520
+ style,
521
+ disabled = false,
522
+ nativeButton = true,
523
+ id: idProp,
524
+ payload,
525
+ handle,
526
+ ref,
527
+ ...elementProps
528
+ } = componentProps;
529
+
530
+ const dialogRootContext = useDialogRootContext(true);
531
+ const store = (handle?.store ?? dialogRootContext?.store) as DialogStore<any> | undefined;
532
+ if (!store) {
533
+ throw new Error(
534
+ 'Base UI: <Dialog.Trigger> must be used within <Dialog.Root> or provided with a handle.',
535
+ );
536
+ }
537
+
538
+ const thisTriggerId = useBaseUiId(idProp, subSlot(slot, 'id'));
539
+ const floatingContext = store.useState('floatingRootContext', subSlot(slot, 'fc'));
540
+ const isOpenedByThisTrigger = store.useState(
541
+ 'isOpenedByTrigger',
542
+ subSlot(slot, 'obt'),
543
+ thisTriggerId,
544
+ );
545
+ const popupId = store.useState('triggerPopupId', subSlot(slot, 'tpid'), thisTriggerId);
546
+
547
+ const triggerElementRef = useRef<HTMLElement | null>(null, subSlot(slot, 'ter'));
548
+
549
+ const { registerTrigger, isMountedByThisTrigger } = useTriggerDataForwarding(
550
+ thisTriggerId,
551
+ triggerElementRef,
552
+ store as any,
553
+ { payload } as any,
554
+ subSlot(slot, 'tdf'),
555
+ );
556
+
557
+ const { getButtonProps, buttonRef } = useButton(
558
+ { disabled, native: nativeButton },
559
+ subSlot(slot, 'btn'),
560
+ );
561
+
562
+ const click = useClick(
563
+ floatingContext,
564
+ { enabled: floatingContext != null },
565
+ subSlot(slot, 'click'),
566
+ );
567
+ const interactionTypeProps = useOpenMethodTriggerProps(
568
+ () => store.select('open'),
569
+ (interactionType) => {
570
+ store.set('openMethod', interactionType);
571
+ },
572
+ subSlot(slot, 'omt'),
573
+ );
574
+
575
+ const state = { disabled, open: isOpenedByThisTrigger };
576
+
577
+ const rootTriggerProps = store.useState(
578
+ 'triggerProps',
579
+ subSlot(slot, 'tp'),
580
+ isMountedByThisTrigger,
581
+ );
582
+
583
+ return useRenderElement(
584
+ 'button',
585
+ { render, className, style },
586
+ {
587
+ state,
588
+ ref: [buttonRef, ref, registerTrigger, triggerElementRef],
589
+ props: [
590
+ click.reference,
591
+ rootTriggerProps,
592
+ interactionTypeProps,
593
+ {
594
+ [CLICK_TRIGGER_IDENTIFIER]: '',
595
+ id: thisTriggerId,
596
+ 'aria-haspopup': 'dialog',
597
+ 'aria-expanded': isOpenedByThisTrigger,
598
+ 'aria-controls': popupId,
599
+ },
600
+ elementProps,
601
+ getButtonProps,
602
+ ],
603
+ stateAttributesMapping: triggerOpenStateMapping,
604
+ },
605
+ subSlot(slot, 're'),
606
+ );
607
+ }
608
+
609
+ // --- Portal ------------------------------------------------------------------
610
+
611
+ const DialogPortalContext = createContext<boolean | undefined>(undefined);
612
+
613
+ function useDialogPortalContext(): boolean {
614
+ const value = useContext(DialogPortalContext);
615
+ if (value === undefined) {
616
+ throw new Error('Base UI: <Dialog.Portal> is missing.');
617
+ }
618
+ return value;
619
+ }
620
+
621
+ function DialogPortal(props: any): any {
622
+ const slot = S('DialogPortal');
623
+ const { keepMounted = false, container, children, ...portalProps } = props;
624
+
625
+ const { store } = useDialogRootContext() as DialogRootContextValue;
626
+ const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
627
+ const modal = store.useState('modal', subSlot(slot, 'modal'));
628
+ const open = store.useState('open', subSlot(slot, 'open'));
629
+
630
+ const shouldRender = mounted || keepMounted;
631
+ if (!shouldRender) {
632
+ return null;
633
+ }
634
+
635
+ return createElement(DialogPortalContext.Provider, {
636
+ value: keepMounted,
637
+ children: createElement(FloatingPortal, {
638
+ container,
639
+ children: [
640
+ mounted && modal === true
641
+ ? createElement(InternalBackdrop, {
642
+ ref: store.context.internalBackdropRef,
643
+ inert: inertValue(!open),
644
+ })
645
+ : null,
646
+ children,
647
+ ],
648
+ }),
649
+ });
650
+ }
651
+
652
+ // --- Backdrop ----------------------------------------------------------------
653
+
654
+ function DialogBackdrop(componentProps: any): any {
655
+ const slot = S('DialogBackdrop');
656
+ const { render, className, style, forceRender = false, ref, ...elementProps } = componentProps;
657
+ const { store } = useDialogRootContext() as DialogRootContextValue;
658
+
659
+ const open = store.useState('open', subSlot(slot, 'open'));
660
+ const nested = store.useState('nested', subSlot(slot, 'nested'));
661
+ const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
662
+ const transitionStatus = store.useState('transitionStatus', subSlot(slot, 'ts'));
663
+
664
+ const state = { open, transitionStatus };
665
+
666
+ return useRenderElement(
667
+ 'div',
668
+ { render, className, style },
669
+ {
670
+ state,
671
+ ref: [store.context.backdropRef, ref],
672
+ stateAttributesMapping: popupStateAttributesMapping,
673
+ props: [
674
+ {
675
+ role: 'presentation',
676
+ hidden: !mounted,
677
+ style: { userSelect: 'none', WebkitUserSelect: 'none' },
678
+ },
679
+ elementProps,
680
+ ],
681
+ enabled: forceRender || !nested,
682
+ },
683
+ subSlot(slot, 're'),
684
+ );
685
+ }
686
+
687
+ // --- Popup -------------------------------------------------------------------
688
+
689
+ function DialogPopup(componentProps: any): any {
690
+ const slot = S('DialogPopup');
691
+ const { render, className, style, finalFocus, initialFocus, ref, ...elementProps } =
692
+ componentProps;
693
+
694
+ const { store } = useDialogRootContext() as DialogRootContextValue;
695
+
696
+ const descriptionElementId = store.useState('descriptionElementId', subSlot(slot, 'did'));
697
+ const disablePointerDismissal = store.useState('disablePointerDismissal', subSlot(slot, 'dpd'));
698
+ const floatingRootContext = store.useState('floatingRootContext', subSlot(slot, 'frc'));
699
+ const rootPopupProps = store.useState('popupProps', subSlot(slot, 'pp'));
700
+ const modal = store.useState('modal', subSlot(slot, 'modal'));
701
+ const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
702
+ const nested = store.useState('nested', subSlot(slot, 'nested'));
703
+ const nestedOpenDialogCount = store.useState('nestedOpenDialogCount', subSlot(slot, 'nodc'));
704
+ const open = store.useState('open', subSlot(slot, 'open'));
705
+ const openMethod = store.useState('openMethod', subSlot(slot, 'om'));
706
+ const titleElementId = store.useState('titleElementId', subSlot(slot, 'tid'));
707
+ const transitionStatus = store.useState('transitionStatus', subSlot(slot, 'ts'));
708
+ const role = store.useState('role', subSlot(slot, 'role'));
709
+ const floatingId = floatingRootContext.useState('floatingId', subSlot(slot, 'fid'));
710
+
711
+ const popupId = elementProps.id ?? floatingId;
712
+
713
+ useDialogPortalContext();
714
+
715
+ useOpenChangeComplete(
716
+ {
717
+ open,
718
+ ref: store.context.popupRef,
719
+ onComplete() {
720
+ if (open) {
721
+ store.context.onOpenChangeComplete?.(true);
722
+ }
723
+ },
724
+ },
725
+ subSlot(slot, 'occ'),
726
+ );
727
+
728
+ const resolvedInitialFocus =
729
+ initialFocus === undefined ? createDefaultInitialFocus(store.context.popupRef) : initialFocus;
730
+
731
+ const nestedDialogOpen = nestedOpenDialogCount > 0;
732
+ const setPopupElement = store.useStateSetter('popupElement', subSlot(slot, 'spe'));
733
+
734
+ const state = { open, nested, transitionStatus, nestedDialogOpen };
735
+
736
+ const element = useRenderElement(
737
+ 'div',
738
+ { render, className, style },
739
+ {
740
+ state,
741
+ props: [
742
+ rootPopupProps,
743
+ {
744
+ id: popupId,
745
+ 'aria-labelledby': titleElementId ?? undefined,
746
+ 'aria-describedby': descriptionElementId ?? undefined,
747
+ role,
748
+ ...FOCUSABLE_POPUP_PROPS,
749
+ hidden: !mounted,
750
+ onKeyDown(event: any) {
751
+ if (COMPOSITE_KEYS.has(event.key)) {
752
+ event.stopPropagation();
753
+ }
754
+ },
755
+ style: { ['--nested-dialogs']: nestedOpenDialogCount },
756
+ },
757
+ elementProps,
758
+ ],
759
+ ref: [ref, store.context.popupRef, setPopupElement],
760
+ stateAttributesMapping: popupStateAttributesMapping,
761
+ },
762
+ subSlot(slot, 're'),
763
+ );
764
+
765
+ return createElement(FloatingFocusManager, {
766
+ context: floatingRootContext,
767
+ openInteractionType: openMethod,
768
+ disabled: !mounted,
769
+ closeOnFocusOut: !disablePointerDismissal,
770
+ initialFocus: resolvedInitialFocus,
771
+ returnFocus: finalFocus,
772
+ modal: modal !== false,
773
+ restoreFocus: 'popup',
774
+ children: element,
775
+ });
776
+ }
777
+
778
+ // --- Title / Description / Close ---------------------------------------------
779
+
780
+ function DialogTitle(componentProps: any): any {
781
+ const slot = S('DialogTitle');
782
+ const { render, className, style, id: idProp, ref, ...elementProps } = componentProps;
783
+ const { store } = useDialogRootContext() as DialogRootContextValue;
784
+ const id = useBaseUiId(idProp, subSlot(slot, 'id'));
785
+ store.useSyncedValueWithCleanup('titleElementId', id, subSlot(slot, 'sync'));
786
+ return useRenderElement(
787
+ 'h2',
788
+ { render, className, style },
789
+ { ref, props: [{ id }, elementProps] },
790
+ subSlot(slot, 're'),
791
+ );
792
+ }
793
+
794
+ function DialogDescription(componentProps: any): any {
795
+ const slot = S('DialogDescription');
796
+ const { render, className, style, id: idProp, ref, ...elementProps } = componentProps;
797
+ const { store } = useDialogRootContext() as DialogRootContextValue;
798
+ const id = useBaseUiId(idProp, subSlot(slot, 'id'));
799
+ store.useSyncedValueWithCleanup('descriptionElementId', id, subSlot(slot, 'sync'));
800
+ return useRenderElement(
801
+ 'p',
802
+ { render, className, style },
803
+ { ref, props: [{ id }, elementProps] },
804
+ subSlot(slot, 're'),
805
+ );
806
+ }
807
+
808
+ function DialogClose(componentProps: any): any {
809
+ const slot = S('DialogClose');
810
+ const {
811
+ render,
812
+ className,
813
+ style,
814
+ disabled = false,
815
+ nativeButton = true,
816
+ ref,
817
+ ...elementProps
818
+ } = componentProps;
819
+ const { store } = useDialogRootContext() as DialogRootContextValue;
820
+ const open = store.useState('open', subSlot(slot, 'open'));
821
+
822
+ const { getButtonProps, buttonRef } = useButton(
823
+ { disabled, native: nativeButton },
824
+ subSlot(slot, 'btn'),
825
+ );
826
+
827
+ const state = { disabled };
828
+
829
+ function handleClick(event: any) {
830
+ if (open) {
831
+ store.setOpen(false, createChangeEventDetails(REASONS.closePress, event));
832
+ }
833
+ }
834
+
835
+ return useRenderElement(
836
+ 'button',
837
+ { render, className, style },
838
+ {
839
+ state,
840
+ ref: [ref, buttonRef],
841
+ props: [{ onClick: handleClick }, elementProps, getButtonProps],
842
+ },
843
+ subSlot(slot, 're'),
844
+ );
845
+ }
846
+
847
+ // --- Namespace ---------------------------------------------------------------
848
+
849
+ export const Dialog = {
850
+ Root: DialogRoot,
851
+ Trigger: DialogTrigger,
852
+ Portal: DialogPortal,
853
+ Backdrop: DialogBackdrop,
854
+ Popup: DialogPopup,
855
+ Title: DialogTitle,
856
+ Description: DialogDescription,
857
+ Close: DialogClose,
858
+ createHandle: createDialogHandle,
859
+ };