@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.
- package/README.md +7 -1
- package/package.json +12 -4
- package/src/alert-dialog.ts +47 -0
- package/src/checkbox.ts +7 -16
- package/src/dialog.ts +859 -0
- package/src/field.ts +4 -30
- package/src/index.ts +3 -0
- package/src/number-field.ts +8 -45
- package/src/popover.ts +1205 -0
- package/src/radio.ts +3 -24
- package/src/slider.ts +4 -5
- package/src/switch.ts +4 -30
- package/src/utils/InternalBackdrop.ts +28 -0
- package/src/utils/adaptiveOriginMiddleware.ts +82 -0
- package/src/utils/closePart.ts +61 -0
- package/src/utils/constants.ts +9 -0
- package/src/utils/createChangeEventDetails.ts +7 -0
- package/src/utils/dom.ts +9 -0
- package/src/utils/empty.ts +7 -0
- package/src/utils/floating/FloatingFocusManager.ts +833 -0
- package/src/utils/floating/FloatingPortal.ts +268 -0
- package/src/utils/floating/FloatingRootStore.ts +127 -0
- package/src/utils/floating/FloatingTree.ts +70 -0
- package/src/utils/floating/FloatingTreeStore.ts +22 -0
- package/src/utils/floating/FocusGuard.ts +34 -0
- package/src/utils/floating/composite.ts +20 -0
- package/src/utils/floating/constants.ts +8 -0
- package/src/utils/floating/createAttribute.ts +4 -0
- package/src/utils/floating/createEventEmitter.ts +20 -0
- package/src/utils/floating/element.ts +54 -0
- package/src/utils/floating/enqueueFocus.ts +38 -0
- package/src/utils/floating/event.ts +53 -0
- package/src/utils/floating/getEmptyRootContext.ts +19 -0
- package/src/utils/floating/markOthers.ts +197 -0
- package/src/utils/floating/nodes.ts +31 -0
- package/src/utils/floating/tabbable.ts +260 -0
- package/src/utils/floating/types.ts +57 -0
- package/src/utils/floating/useClick.ts +174 -0
- package/src/utils/floating/useDismiss.ts +641 -0
- package/src/utils/floating/useFloating.ts +198 -0
- package/src/utils/floating/useFloatingRootContext.ts +81 -0
- package/src/utils/floating/useHoverFloatingInteraction.ts +12 -0
- package/src/utils/floating/useHoverReferenceInteraction.ts +17 -0
- package/src/utils/floating/useSyncedFloatingRootContext.ts +93 -0
- package/src/utils/getDisabledMountTransitionStyles.ts +12 -0
- package/src/utils/hideMiddleware.ts +22 -0
- package/src/utils/inertValue.ts +5 -0
- package/src/utils/mergeCleanups.ts +13 -0
- package/src/utils/platform.ts +46 -2
- package/src/utils/popupStateMapping.ts +48 -0
- package/src/utils/popups/index.ts +4 -0
- package/src/utils/popups/popupStoreUtils.ts +484 -0
- package/src/utils/popups/popupTriggerMap.ts +62 -0
- package/src/utils/popups/store.ts +147 -0
- package/src/utils/popups/useTriggerFocusGuards.ts +73 -0
- package/src/utils/store/ReactStore.ts +168 -0
- package/src/utils/store/Store.ts +84 -0
- package/src/utils/store/createSelector.ts +66 -0
- package/src/utils/store/useStore.ts +49 -0
- package/src/utils/useAnchorPositioning.ts +581 -0
- package/src/utils/useAnchoredPopupScrollLock.ts +50 -0
- package/src/utils/useAnimationFrame.ts +28 -5
- package/src/utils/useEnhancedClickHandler.ts +47 -0
- package/src/utils/useOnFirstRender.ts +11 -0
- package/src/utils/useOpenInteractionType.ts +32 -0
- package/src/utils/usePositioner.ts +48 -0
- package/src/utils/useScrollLock.ts +253 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/utils/popups/popupStoreUtils.ts (v1.6.0), octane-adapted.
|
|
2
|
+
// The shared hooks + helpers every popup Root/Trigger/Popup routes through: store creation +
|
|
3
|
+
// floating-root sync (`usePopupStore`), trigger registration, the open-change sequence
|
|
4
|
+
// (`applyPopupOpenChange`), mounted/transition management (`useOpenStateTransitions`), and interaction-
|
|
5
|
+
// prop syncing. octane adaptations: every hook threads an explicit slot; `useIsoLayoutEffect` →
|
|
6
|
+
// `useLayoutEffect`; `ReactDOM.flushSync` → octane `flushSync`; `useId` → `useBaseUiId`.
|
|
7
|
+
import { useRef, useCallback, useLayoutEffect, flushSync, useId } from 'octane';
|
|
8
|
+
|
|
9
|
+
import { subSlot } from '../../internal';
|
|
10
|
+
import { useStableCallback } from '../useStableCallback';
|
|
11
|
+
import { useOnFirstRender } from '../useOnFirstRender';
|
|
12
|
+
import { useTransitionStatus } from '../useTransitionStatus';
|
|
13
|
+
import { useOpenChangeComplete } from '../useOpenChangeComplete';
|
|
14
|
+
import { EMPTY_OBJECT } from '../empty';
|
|
15
|
+
import { FOCUSABLE_ATTRIBUTE } from '../floating/constants';
|
|
16
|
+
import { useFloatingParentNodeId } from '../floating/FloatingTree';
|
|
17
|
+
import { useSyncedFloatingRootContext } from '../floating/useSyncedFloatingRootContext';
|
|
18
|
+
import { createChangeEventDetails, REASONS } from '../createChangeEventDetails';
|
|
19
|
+
import type { ReactStore } from '../store/ReactStore';
|
|
20
|
+
import {
|
|
21
|
+
PopupStoreState,
|
|
22
|
+
PopupStoreContext,
|
|
23
|
+
popupStoreSelectors,
|
|
24
|
+
PopupStoreSelectors,
|
|
25
|
+
} from './store';
|
|
26
|
+
|
|
27
|
+
export type InteractionType = 'mouse' | 'touch' | 'pen' | 'keyboard';
|
|
28
|
+
|
|
29
|
+
export const FOCUSABLE_POPUP_PROPS = {
|
|
30
|
+
tabIndex: -1,
|
|
31
|
+
[FOCUSABLE_ATTRIBUTE]: '',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** The default `initialFocus` resolver: focus the popup itself when opened by touch, else default. */
|
|
35
|
+
export function createDefaultInitialFocus(popupRef: { current: HTMLElement | null }) {
|
|
36
|
+
return (interactionType: InteractionType) =>
|
|
37
|
+
interactionType === 'touch' ? popupRef.current : true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type AnyPopupStore = ReactStore<any, PopupStoreContext<any>, PopupStoreSelectors> & {
|
|
41
|
+
setOpen(open: boolean, eventDetails: any): void;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function usePopupStore<Store extends AnyPopupStore>(
|
|
45
|
+
externalStore: Store | undefined,
|
|
46
|
+
createStore: (floatingId: string | undefined, nested: boolean) => Store,
|
|
47
|
+
treatPopupAsFloatingElement: boolean,
|
|
48
|
+
slot: symbol | undefined,
|
|
49
|
+
): { store: Store; internalStore: Store | null } {
|
|
50
|
+
// Raw `useId` (no `base-ui-` prefix), matching Base UI's `@base-ui/utils/useId`.
|
|
51
|
+
const floatingId = useId(subSlot(slot, 'fid'));
|
|
52
|
+
const nested = useFloatingParentNodeId() != null;
|
|
53
|
+
|
|
54
|
+
const internalStoreRef = useRef<Store | null>(null, subSlot(slot, 'store'));
|
|
55
|
+
if (externalStore === undefined && internalStoreRef.current === null) {
|
|
56
|
+
internalStoreRef.current = createStore(floatingId, nested);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const store = externalStore ?? internalStoreRef.current!;
|
|
60
|
+
|
|
61
|
+
useSyncedFloatingRootContext(
|
|
62
|
+
{
|
|
63
|
+
popupStore: store,
|
|
64
|
+
treatPopupAsFloatingElement,
|
|
65
|
+
floatingRootContext: store.state.floatingRootContext,
|
|
66
|
+
floatingId,
|
|
67
|
+
nested,
|
|
68
|
+
onOpenChange: store.setOpen,
|
|
69
|
+
},
|
|
70
|
+
subSlot(slot, 'synced'),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return { store, internalStore: internalStoreRef.current };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A callback ref that registers/unregisters the trigger element in the store. */
|
|
77
|
+
export function useTriggerRegistration<State extends PopupStoreState<unknown>>(
|
|
78
|
+
id: string | undefined,
|
|
79
|
+
store: ReactStore<State, PopupStoreContext<never>, PopupStoreSelectors>,
|
|
80
|
+
slot: symbol | undefined,
|
|
81
|
+
): (element: Element | null) => void {
|
|
82
|
+
const registeredElementIdRef = useRef<string | null>(null, subSlot(slot, 'rid'));
|
|
83
|
+
const registeredElementRef = useRef<Element | null>(null, subSlot(slot, 'rel'));
|
|
84
|
+
|
|
85
|
+
return useCallback(
|
|
86
|
+
(element: Element | null) => {
|
|
87
|
+
if (id === undefined) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let shouldSyncTriggerCount = false;
|
|
92
|
+
|
|
93
|
+
if (registeredElementIdRef.current !== null) {
|
|
94
|
+
const registeredId = registeredElementIdRef.current;
|
|
95
|
+
const registeredElement = registeredElementRef.current;
|
|
96
|
+
const currentElement = store.context.triggerElements.getById(registeredId);
|
|
97
|
+
|
|
98
|
+
if (registeredElement && currentElement === registeredElement) {
|
|
99
|
+
store.context.triggerElements.delete(registeredId);
|
|
100
|
+
shouldSyncTriggerCount = true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
registeredElementIdRef.current = null;
|
|
104
|
+
registeredElementRef.current = null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (element !== null) {
|
|
108
|
+
registeredElementIdRef.current = id;
|
|
109
|
+
registeredElementRef.current = element;
|
|
110
|
+
store.context.triggerElements.add(id, element);
|
|
111
|
+
shouldSyncTriggerCount = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (shouldSyncTriggerCount) {
|
|
115
|
+
const triggerCount = store.context.triggerElements.size;
|
|
116
|
+
if (store.select('open') && store.state.triggerCount !== triggerCount) {
|
|
117
|
+
store.set('triggerCount', triggerCount);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
[store, id],
|
|
122
|
+
subSlot(slot, 'cb'),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function setPopupOpenState(
|
|
127
|
+
state: Partial<PopupStoreState<unknown>>,
|
|
128
|
+
open: boolean,
|
|
129
|
+
trigger: Element | undefined,
|
|
130
|
+
preventUnmountOnClose = false,
|
|
131
|
+
) {
|
|
132
|
+
if (open) {
|
|
133
|
+
state.preventUnmountingOnClose = false;
|
|
134
|
+
} else if (preventUnmountOnClose) {
|
|
135
|
+
state.preventUnmountingOnClose = true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const triggerId = trigger?.id ?? null;
|
|
139
|
+
|
|
140
|
+
if (triggerId || open) {
|
|
141
|
+
state.activeTriggerId = triggerId;
|
|
142
|
+
state.activeTriggerElement = trigger ?? null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function attachPreventUnmountOnClose(eventDetails: { preventUnmountOnClose(): void }) {
|
|
147
|
+
let preventUnmountOnClose = false;
|
|
148
|
+
eventDetails.preventUnmountOnClose = () => {
|
|
149
|
+
preventUnmountOnClose = true;
|
|
150
|
+
};
|
|
151
|
+
return () => preventUnmountOnClose;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The shared open-change sequence (notify → cancel-check → dispatch → commit). Non-hook. */
|
|
155
|
+
export function applyPopupOpenChange<
|
|
156
|
+
State extends PopupStoreState<unknown> & {
|
|
157
|
+
instantType?: 'delay' | 'dismiss' | 'focus' | undefined;
|
|
158
|
+
},
|
|
159
|
+
>(
|
|
160
|
+
store: {
|
|
161
|
+
readonly context: Pick<PopupStoreContext<any>, 'onOpenChange'>;
|
|
162
|
+
readonly state: Pick<PopupStoreState<unknown>, 'floatingRootContext'>;
|
|
163
|
+
update(state: Partial<State>): void;
|
|
164
|
+
},
|
|
165
|
+
nextOpen: boolean,
|
|
166
|
+
eventDetails: any & { preventUnmountOnClose(): void },
|
|
167
|
+
options: {
|
|
168
|
+
onBeforeDispatch?: (() => void) | undefined;
|
|
169
|
+
extraState?: Partial<State> | undefined;
|
|
170
|
+
} = {},
|
|
171
|
+
): void {
|
|
172
|
+
const reason = eventDetails.reason;
|
|
173
|
+
const isHover = reason === REASONS.triggerHover;
|
|
174
|
+
const isFocusOpen = nextOpen && reason === REASONS.triggerFocus;
|
|
175
|
+
const isDismissClose =
|
|
176
|
+
!nextOpen && (reason === REASONS.triggerPress || reason === REASONS.escapeKey);
|
|
177
|
+
|
|
178
|
+
const shouldPreventUnmountOnClose = attachPreventUnmountOnClose(eventDetails);
|
|
179
|
+
|
|
180
|
+
store.context.onOpenChange?.(nextOpen, eventDetails);
|
|
181
|
+
|
|
182
|
+
if (eventDetails.isCanceled) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
options.onBeforeDispatch?.();
|
|
187
|
+
|
|
188
|
+
store.state.floatingRootContext.dispatchOpenChange(nextOpen, eventDetails);
|
|
189
|
+
|
|
190
|
+
const changeState = () => {
|
|
191
|
+
const updatedState: Partial<PopupStoreState<unknown>> & {
|
|
192
|
+
instantType?: 'delay' | 'dismiss' | 'focus' | undefined;
|
|
193
|
+
} = { ...options.extraState, open: nextOpen };
|
|
194
|
+
|
|
195
|
+
if (isFocusOpen) {
|
|
196
|
+
updatedState.instantType = 'focus';
|
|
197
|
+
} else if (isDismissClose) {
|
|
198
|
+
updatedState.instantType = 'dismiss';
|
|
199
|
+
} else if (isHover) {
|
|
200
|
+
updatedState.instantType = undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
setPopupOpenState(updatedState, nextOpen, eventDetails.trigger, shouldPreventUnmountOnClose());
|
|
204
|
+
store.update(updatedState as Partial<State>);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
if (isHover) {
|
|
208
|
+
flushSync(changeState);
|
|
209
|
+
} else {
|
|
210
|
+
changeState();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function useInitialOpenSync<State extends PopupStoreState<unknown>>(
|
|
215
|
+
store: ReactStore<State, PopupStoreContext<never>, PopupStoreSelectors>,
|
|
216
|
+
openProp: boolean | undefined,
|
|
217
|
+
defaultOpen: boolean,
|
|
218
|
+
defaultTriggerId: string | null,
|
|
219
|
+
slot: symbol | undefined,
|
|
220
|
+
) {
|
|
221
|
+
useOnFirstRender(
|
|
222
|
+
() => {
|
|
223
|
+
if (openProp === undefined && store.state.open === false && defaultOpen) {
|
|
224
|
+
store.state = {
|
|
225
|
+
...store.state,
|
|
226
|
+
open: true,
|
|
227
|
+
activeTriggerId: defaultTriggerId,
|
|
228
|
+
preventUnmountingOnClose: false,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
subSlot(slot, 'first'),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function useTriggerDataForwarding<State extends PopupStoreState<unknown>>(
|
|
237
|
+
triggerId: string | undefined,
|
|
238
|
+
triggerElementRef: { current: Element | null },
|
|
239
|
+
store: ReactStore<State, PopupStoreContext<never>, typeof popupStoreSelectors>,
|
|
240
|
+
stateUpdates: Partial<State>,
|
|
241
|
+
slot: symbol | undefined,
|
|
242
|
+
) {
|
|
243
|
+
const isMountedByThisTrigger = store.useState(
|
|
244
|
+
'isMountedByTrigger',
|
|
245
|
+
subSlot(slot, 'mbt'),
|
|
246
|
+
triggerId,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const baseRegisterTrigger = useTriggerRegistration(triggerId, store, subSlot(slot, 'reg'));
|
|
250
|
+
|
|
251
|
+
const registerTrigger = useStableCallback(
|
|
252
|
+
(element: Element | null) => {
|
|
253
|
+
baseRegisterTrigger(element);
|
|
254
|
+
|
|
255
|
+
if (!element) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const open = store.select('open');
|
|
260
|
+
const activeTriggerId = store.select('activeTriggerId');
|
|
261
|
+
|
|
262
|
+
if (activeTriggerId === triggerId) {
|
|
263
|
+
store.update({
|
|
264
|
+
activeTriggerElement: element,
|
|
265
|
+
...(open ? stateUpdates : null),
|
|
266
|
+
} as Partial<State>);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (activeTriggerId == null && open) {
|
|
271
|
+
store.update({
|
|
272
|
+
activeTriggerId: triggerId,
|
|
273
|
+
activeTriggerElement: element,
|
|
274
|
+
...stateUpdates,
|
|
275
|
+
} as Partial<State>);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
subSlot(slot, 'regcb'),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
useLayoutEffect(
|
|
282
|
+
() => {
|
|
283
|
+
if (isMountedByThisTrigger) {
|
|
284
|
+
store.update({
|
|
285
|
+
activeTriggerElement: triggerElementRef.current,
|
|
286
|
+
...stateUpdates,
|
|
287
|
+
} as Partial<State>);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
[isMountedByThisTrigger, store, triggerElementRef, ...Object.values(stateUpdates)],
|
|
291
|
+
subSlot(slot, 'e'),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
return { registerTrigger, isMountedByThisTrigger };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function useImplicitActiveTrigger<State extends PopupStoreState<unknown>>(
|
|
298
|
+
store: ReactStore<State, PopupStoreContext<never>, typeof popupStoreSelectors> & {
|
|
299
|
+
setOpen(open: boolean, eventDetails: any): void;
|
|
300
|
+
},
|
|
301
|
+
options: { closeOnActiveTriggerUnmount?: boolean | undefined },
|
|
302
|
+
slot: symbol | undefined,
|
|
303
|
+
) {
|
|
304
|
+
const { closeOnActiveTriggerUnmount = false } = options;
|
|
305
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
306
|
+
const reactiveTriggerCount = store.useState('triggerCount', subSlot(slot, 'tc'));
|
|
307
|
+
|
|
308
|
+
useLayoutEffect(
|
|
309
|
+
() => {
|
|
310
|
+
if (!open) {
|
|
311
|
+
if (store.state.triggerCount !== 0) {
|
|
312
|
+
store.set('triggerCount', 0);
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const triggerCount = store.context.triggerElements.size;
|
|
318
|
+
const stateUpdates: Partial<PopupStoreState<unknown>> = {};
|
|
319
|
+
|
|
320
|
+
if (store.state.triggerCount !== triggerCount) {
|
|
321
|
+
stateUpdates.triggerCount = triggerCount;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const activeTriggerId = store.select('activeTriggerId');
|
|
325
|
+
let lostActiveTriggerId: string | null = null;
|
|
326
|
+
|
|
327
|
+
if (activeTriggerId) {
|
|
328
|
+
const activeTriggerElement = store.context.triggerElements.getById(activeTriggerId);
|
|
329
|
+
if (!activeTriggerElement) {
|
|
330
|
+
lostActiveTriggerId = activeTriggerId;
|
|
331
|
+
} else if (activeTriggerElement !== store.state.activeTriggerElement) {
|
|
332
|
+
stateUpdates.activeTriggerElement = activeTriggerElement;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!lostActiveTriggerId && !activeTriggerId && triggerCount === 1) {
|
|
337
|
+
const iteratorResult = store.context.triggerElements.entries().next();
|
|
338
|
+
if (!iteratorResult.done) {
|
|
339
|
+
const [implicitTriggerId, implicitTriggerElement] = iteratorResult.value;
|
|
340
|
+
stateUpdates.activeTriggerId = implicitTriggerId;
|
|
341
|
+
stateUpdates.activeTriggerElement = implicitTriggerElement;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (
|
|
346
|
+
stateUpdates.triggerCount !== undefined ||
|
|
347
|
+
stateUpdates.activeTriggerId !== undefined ||
|
|
348
|
+
stateUpdates.activeTriggerElement !== undefined
|
|
349
|
+
) {
|
|
350
|
+
store.update(stateUpdates as Partial<State>);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (lostActiveTriggerId) {
|
|
354
|
+
if (closeOnActiveTriggerUnmount) {
|
|
355
|
+
queueMicrotask(() => {
|
|
356
|
+
if (
|
|
357
|
+
store.select('open') &&
|
|
358
|
+
store.select('activeTriggerId') === lostActiveTriggerId &&
|
|
359
|
+
!store.context.triggerElements.getById(lostActiveTriggerId)
|
|
360
|
+
) {
|
|
361
|
+
const eventDetails = createChangeEventDetails(REASONS.none);
|
|
362
|
+
store.setOpen(false, eventDetails);
|
|
363
|
+
if (!eventDetails.isCanceled) {
|
|
364
|
+
store.update({
|
|
365
|
+
activeTriggerId: null,
|
|
366
|
+
activeTriggerElement: null,
|
|
367
|
+
} as Partial<State>);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
[open, store, reactiveTriggerCount, closeOnActiveTriggerUnmount],
|
|
375
|
+
subSlot(slot, 'e'),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function useOpenStateTransitions<State extends PopupStoreState<unknown>>(
|
|
380
|
+
open: boolean,
|
|
381
|
+
store: ReactStore<State, PopupStoreContext<never>, typeof popupStoreSelectors>,
|
|
382
|
+
onUnmount: (() => void) | undefined,
|
|
383
|
+
slot: symbol | undefined,
|
|
384
|
+
) {
|
|
385
|
+
const { mounted, setMounted, transitionStatus } = useTransitionStatus(
|
|
386
|
+
open,
|
|
387
|
+
undefined,
|
|
388
|
+
undefined,
|
|
389
|
+
subSlot(slot, 'ts'),
|
|
390
|
+
);
|
|
391
|
+
const preventUnmountingOnClose = store.useState(
|
|
392
|
+
'preventUnmountingOnClose',
|
|
393
|
+
subSlot(slot, 'puoc'),
|
|
394
|
+
);
|
|
395
|
+
const syncedPreventUnmountingOnClose = open ? false : preventUnmountingOnClose;
|
|
396
|
+
|
|
397
|
+
store.useSyncedValues(
|
|
398
|
+
{
|
|
399
|
+
mounted,
|
|
400
|
+
transitionStatus,
|
|
401
|
+
preventUnmountingOnClose: syncedPreventUnmountingOnClose,
|
|
402
|
+
} as Partial<State>,
|
|
403
|
+
subSlot(slot, 'sync'),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const forceUnmount = useStableCallback(
|
|
407
|
+
() => {
|
|
408
|
+
setMounted(false);
|
|
409
|
+
store.update({
|
|
410
|
+
activeTriggerId: null,
|
|
411
|
+
activeTriggerElement: null,
|
|
412
|
+
mounted: false,
|
|
413
|
+
preventUnmountingOnClose: false,
|
|
414
|
+
} as Partial<State>);
|
|
415
|
+
onUnmount?.();
|
|
416
|
+
store.context.onOpenChangeComplete?.(false);
|
|
417
|
+
},
|
|
418
|
+
subSlot(slot, 'fu'),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
useOpenChangeComplete(
|
|
422
|
+
{
|
|
423
|
+
enabled: mounted && !open && !syncedPreventUnmountingOnClose,
|
|
424
|
+
open,
|
|
425
|
+
ref: store.context.popupRef,
|
|
426
|
+
onComplete() {
|
|
427
|
+
if (!open) {
|
|
428
|
+
forceUnmount();
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
subSlot(slot, 'occ'),
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
return { forceUnmount, transitionStatus };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function usePopupInteractionProps<State extends PopupStoreState<unknown>>(
|
|
439
|
+
store: ReactStore<State, PopupStoreContext<never>, typeof popupStoreSelectors>,
|
|
440
|
+
statePart: Partial<State>,
|
|
441
|
+
slot: symbol | undefined,
|
|
442
|
+
) {
|
|
443
|
+
store.useSyncedValues(statePart, subSlot(slot, 'sync'));
|
|
444
|
+
|
|
445
|
+
useLayoutEffect(
|
|
446
|
+
() => () => {
|
|
447
|
+
store.update({
|
|
448
|
+
activeTriggerProps: EMPTY_OBJECT,
|
|
449
|
+
inactiveTriggerProps: EMPTY_OBJECT,
|
|
450
|
+
popupProps: EMPTY_OBJECT,
|
|
451
|
+
} as unknown as Partial<State>);
|
|
452
|
+
},
|
|
453
|
+
[store],
|
|
454
|
+
subSlot(slot, 'e'),
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function usePopupRootSync<
|
|
459
|
+
State extends PopupStoreState<unknown> & { openMethod: InteractionType | null },
|
|
460
|
+
>(
|
|
461
|
+
store: ReactStore<State, PopupStoreContext<never>, typeof popupStoreSelectors>,
|
|
462
|
+
open: boolean,
|
|
463
|
+
slot: symbol | undefined,
|
|
464
|
+
) {
|
|
465
|
+
useLayoutEffect(
|
|
466
|
+
() => {
|
|
467
|
+
if (!open && store.state.openMethod !== null) {
|
|
468
|
+
store.set('openMethod', null);
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
[open, store],
|
|
472
|
+
subSlot(slot, 'e1'),
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
useLayoutEffect(
|
|
476
|
+
() => () => {
|
|
477
|
+
if (store.state.openMethod !== null) {
|
|
478
|
+
store.set('openMethod', null);
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
[store],
|
|
482
|
+
subSlot(slot, 'e2'),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/utils/popups/popupTriggerMap.ts (v1.6.0).
|
|
2
|
+
// Tracks a popup's trigger elements by id (a Set for membership + a Map for id lookup). Pure —
|
|
3
|
+
// the dev-only invariant check is dropped (functional outcomes only).
|
|
4
|
+
export class PopupTriggerMap {
|
|
5
|
+
private elementsSet: Set<Element>;
|
|
6
|
+
|
|
7
|
+
private idMap: Map<string, Element>;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.elementsSet = new Set();
|
|
11
|
+
this.idMap = new Map();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
add(id: string, element: Element) {
|
|
15
|
+
const existingElement = this.idMap.get(id);
|
|
16
|
+
if (existingElement === element) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (existingElement !== undefined) {
|
|
20
|
+
this.elementsSet.delete(existingElement);
|
|
21
|
+
}
|
|
22
|
+
this.elementsSet.add(element);
|
|
23
|
+
this.idMap.set(id, element);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
delete(id: string) {
|
|
27
|
+
const element = this.idMap.get(id);
|
|
28
|
+
if (element) {
|
|
29
|
+
this.elementsSet.delete(element);
|
|
30
|
+
this.idMap.delete(id);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
hasElement(element: Element): boolean {
|
|
35
|
+
return this.elementsSet.has(element);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
hasMatchingElement(predicate: (el: Element) => boolean): boolean {
|
|
39
|
+
for (const element of this.elementsSet) {
|
|
40
|
+
if (predicate(element)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getById(id: string): Element | undefined {
|
|
48
|
+
return this.idMap.get(id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
entries(): IterableIterator<[string, Element]> {
|
|
52
|
+
return this.idMap.entries();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
elements(): IterableIterator<Element> {
|
|
56
|
+
return this.elementsSet.values();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get size(): number {
|
|
60
|
+
return this.idMap.size;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/utils/popups/store.ts (v1.6.0). The state shape, initial
|
|
2
|
+
// state, floating-root-context factory, and selectors shared by EVERY popup store (Dialog/Popover/
|
|
3
|
+
// Tooltip/Menu/PreviewCard). Pure — octane adaptations are only import paths + `HTMLProps` inlined.
|
|
4
|
+
import { createSelector } from '../store/createSelector';
|
|
5
|
+
import { EMPTY_OBJECT } from '../empty';
|
|
6
|
+
import type { FloatingRootContext } from '../floating/types';
|
|
7
|
+
import { FloatingRootStore } from '../floating/FloatingRootStore';
|
|
8
|
+
import { getEmptyRootContext } from '../floating/getEmptyRootContext';
|
|
9
|
+
import type { TransitionStatus } from '../useTransitionStatus';
|
|
10
|
+
import { PopupTriggerMap } from './popupTriggerMap';
|
|
11
|
+
|
|
12
|
+
type HTMLProps = Record<string, any>;
|
|
13
|
+
|
|
14
|
+
export type PopupStoreState<Payload> = {
|
|
15
|
+
open: boolean;
|
|
16
|
+
readonly openProp: boolean | undefined;
|
|
17
|
+
mounted: boolean;
|
|
18
|
+
transitionStatus: TransitionStatus;
|
|
19
|
+
floatingRootContext: FloatingRootContext;
|
|
20
|
+
floatingId: string | undefined;
|
|
21
|
+
triggerCount: number;
|
|
22
|
+
preventUnmountingOnClose: boolean;
|
|
23
|
+
payload: Payload | undefined;
|
|
24
|
+
activeTriggerId: string | null;
|
|
25
|
+
activeTriggerElement: Element | null;
|
|
26
|
+
readonly triggerIdProp: string | null | undefined;
|
|
27
|
+
popupElement: HTMLElement | null;
|
|
28
|
+
positionerElement: HTMLElement | null;
|
|
29
|
+
activeTriggerProps: HTMLProps;
|
|
30
|
+
inactiveTriggerProps: HTMLProps;
|
|
31
|
+
popupProps: HTMLProps;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function createInitialPopupStoreState<Payload>(): PopupStoreState<Payload> {
|
|
35
|
+
return {
|
|
36
|
+
open: false,
|
|
37
|
+
openProp: undefined,
|
|
38
|
+
mounted: false,
|
|
39
|
+
transitionStatus: undefined,
|
|
40
|
+
floatingRootContext: getEmptyRootContext(),
|
|
41
|
+
floatingId: undefined,
|
|
42
|
+
triggerCount: 0,
|
|
43
|
+
preventUnmountingOnClose: false,
|
|
44
|
+
payload: undefined,
|
|
45
|
+
activeTriggerId: null,
|
|
46
|
+
activeTriggerElement: null,
|
|
47
|
+
triggerIdProp: undefined,
|
|
48
|
+
popupElement: null,
|
|
49
|
+
positionerElement: null,
|
|
50
|
+
activeTriggerProps: EMPTY_OBJECT as HTMLProps,
|
|
51
|
+
inactiveTriggerProps: EMPTY_OBJECT as HTMLProps,
|
|
52
|
+
popupProps: EMPTY_OBJECT as HTMLProps,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createPopupFloatingRootContext(
|
|
57
|
+
triggerElements: PopupTriggerMap,
|
|
58
|
+
floatingId?: string | undefined,
|
|
59
|
+
nested = false,
|
|
60
|
+
) {
|
|
61
|
+
return new FloatingRootStore({
|
|
62
|
+
open: false,
|
|
63
|
+
transitionStatus: undefined,
|
|
64
|
+
floatingElement: null,
|
|
65
|
+
referenceElement: null,
|
|
66
|
+
triggerElements,
|
|
67
|
+
floatingId,
|
|
68
|
+
syncOnly: true,
|
|
69
|
+
nested,
|
|
70
|
+
onOpenChange: undefined,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type PopupStoreContext<ChangeEventDetails> = {
|
|
75
|
+
readonly triggerElements: PopupTriggerMap;
|
|
76
|
+
readonly popupRef: { current: HTMLElement | null };
|
|
77
|
+
onOpenChange?: ((open: boolean, eventDetails: ChangeEventDetails) => void) | undefined;
|
|
78
|
+
onOpenChangeComplete: ((open: boolean) => void) | undefined;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type S = PopupStoreState<unknown>;
|
|
82
|
+
|
|
83
|
+
const activeTriggerIdSelector = createSelector(
|
|
84
|
+
(state: S) => state.triggerIdProp ?? state.activeTriggerId,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const openSelector = createSelector((state: S) => state.openProp ?? state.open);
|
|
88
|
+
|
|
89
|
+
const popupIdSelector = createSelector((state: S) => {
|
|
90
|
+
const popupId = state.popupElement?.id ?? state.floatingId;
|
|
91
|
+
return popupId || undefined;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function triggerOwnsOpenPopup(state: S, triggerId: string | undefined) {
|
|
95
|
+
return (
|
|
96
|
+
triggerId !== undefined && openSelector(state) && activeTriggerIdSelector(state) === triggerId
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function triggerOwnsOpenPopupOrIsOnlyTrigger(state: S, triggerId: string | undefined) {
|
|
101
|
+
if (triggerOwnsOpenPopup(state, triggerId)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return (
|
|
105
|
+
triggerId !== undefined &&
|
|
106
|
+
openSelector(state) &&
|
|
107
|
+
activeTriggerIdSelector(state) == null &&
|
|
108
|
+
state.triggerCount === 1
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const popupStoreSelectors = {
|
|
113
|
+
open: openSelector,
|
|
114
|
+
mounted: createSelector((state: S) => state.mounted),
|
|
115
|
+
transitionStatus: createSelector((state: S) => state.transitionStatus),
|
|
116
|
+
floatingRootContext: createSelector((state: S) => state.floatingRootContext),
|
|
117
|
+
triggerCount: createSelector((state: S) => state.triggerCount),
|
|
118
|
+
preventUnmountingOnClose: createSelector((state: S) => state.preventUnmountingOnClose),
|
|
119
|
+
payload: createSelector((state: S) => state.payload),
|
|
120
|
+
activeTriggerId: activeTriggerIdSelector,
|
|
121
|
+
activeTriggerElement: createSelector((state: S) =>
|
|
122
|
+
state.mounted ? state.activeTriggerElement : null,
|
|
123
|
+
),
|
|
124
|
+
popupId: popupIdSelector,
|
|
125
|
+
isTriggerActive: createSelector(
|
|
126
|
+
(state: S, triggerId: string | undefined) =>
|
|
127
|
+
triggerId !== undefined && activeTriggerIdSelector(state) === triggerId,
|
|
128
|
+
),
|
|
129
|
+
isOpenedByTrigger: createSelector((state: S, triggerId: string | undefined) =>
|
|
130
|
+
triggerOwnsOpenPopup(state, triggerId),
|
|
131
|
+
),
|
|
132
|
+
isMountedByTrigger: createSelector(
|
|
133
|
+
(state: S, triggerId: string | undefined) =>
|
|
134
|
+
triggerId !== undefined && activeTriggerIdSelector(state) === triggerId && state.mounted,
|
|
135
|
+
),
|
|
136
|
+
triggerProps: createSelector((state: S, isActive: boolean) =>
|
|
137
|
+
isActive ? state.activeTriggerProps : state.inactiveTriggerProps,
|
|
138
|
+
),
|
|
139
|
+
triggerPopupId: createSelector((state: S, triggerId: string | undefined) =>
|
|
140
|
+
triggerOwnsOpenPopupOrIsOnlyTrigger(state, triggerId) ? popupIdSelector(state) : undefined,
|
|
141
|
+
),
|
|
142
|
+
popupProps: createSelector((state: S) => state.popupProps),
|
|
143
|
+
popupElement: createSelector((state: S) => state.popupElement),
|
|
144
|
+
positionerElement: createSelector((state: S) => state.positionerElement),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type PopupStoreSelectors = typeof popupStoreSelectors;
|