@octanejs/base-ui 0.1.14 → 0.1.15
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/package.json +4 -4
- package/src/alert-dialog.ts +2 -0
- package/src/button.ts +63 -0
- package/src/csp-provider.ts +37 -0
- package/src/dialog.ts +89 -22
- package/src/direction-provider.ts +32 -0
- package/src/index.ts +7 -0
- package/src/popover.ts +92 -22
- package/src/preview-card.ts +812 -0
- package/src/tooltip.ts +1053 -0
- package/src/unstable-use-media-query.ts +101 -0
- package/src/utils/CSPContext.ts +21 -0
- package/src/utils/FloatingPortalLite.ts +29 -0
- package/src/utils/createChangeEventDetails.ts +1 -0
- package/src/utils/floating/FloatingDelayGroup.ts +280 -0
- package/src/utils/floating/element.ts +36 -2
- package/src/utils/floating/safePolygon.ts +448 -0
- package/src/utils/floating/useClientPoint.ts +276 -0
- package/src/utils/floating/useFocus.ts +248 -0
- package/src/utils/floating/useHoverFloatingInteraction.ts +301 -11
- package/src/utils/floating/useHoverInteractionSharedState.ts +139 -0
- package/src/utils/floating/useHoverReferenceInteraction.ts +507 -14
- package/src/utils/floating/useHoverShared.ts +78 -0
- package/src/utils/getCssDimensions.ts +28 -0
- package/src/utils/platform.ts +10 -0
- package/src/utils/popups/index.ts +6 -0
- package/src/utils/popups/inlineRect.ts +289 -0
- package/src/utils/usePopupAutoResize.ts +250 -0
- package/src/utils/usePopupViewport.ts +375 -0
- package/src/utils/usePreviousValue.ts +25 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/preview-card/ (v1.6.0): store (PreviewCardStore/
|
|
2
|
+
// PreviewCardHandle), root (+ PreviewCardInteractions), trigger, portal, positioner, popup,
|
|
3
|
+
// backdrop, arrow, viewport. Reuses the Store + floating stack from Dialog/Popover, the Phase-3b
|
|
4
|
+
// hover/focus layer, and the Phase-3c viewport.
|
|
5
|
+
//
|
|
6
|
+
// The distinctive piece is the inline-rect anchoring: the trigger is a link that may wrap across
|
|
7
|
+
// several lines, so the card anchors to the specific line the cursor was on
|
|
8
|
+
// (`utils/popups/inlineRect`).
|
|
9
|
+
//
|
|
10
|
+
// octane adaptations: forwardRef → ref-as-prop; native events; every Store hook-method + hook
|
|
11
|
+
// threads an explicit slot; `React.createContext` → octane `createContext`.
|
|
12
|
+
import {
|
|
13
|
+
createContext,
|
|
14
|
+
createElement,
|
|
15
|
+
Fragment,
|
|
16
|
+
useContext,
|
|
17
|
+
useMemo,
|
|
18
|
+
useRef,
|
|
19
|
+
useLayoutEffect,
|
|
20
|
+
useCallback,
|
|
21
|
+
useImperativeHandle,
|
|
22
|
+
isChildrenBlock,
|
|
23
|
+
} from 'octane';
|
|
24
|
+
|
|
25
|
+
import { S, subSlot } from './internal';
|
|
26
|
+
import { useRenderElement } from './utils/useRenderElement';
|
|
27
|
+
import { mergeProps } from './utils/mergeProps';
|
|
28
|
+
import { useBaseUiId } from './utils/useBaseUiId';
|
|
29
|
+
import { createChangeEventDetails, REASONS } from './utils/createChangeEventDetails';
|
|
30
|
+
import { EMPTY_OBJECT } from './utils/empty';
|
|
31
|
+
import { ReactStore } from './utils/store/ReactStore';
|
|
32
|
+
import { createSelector } from './utils/store/createSelector';
|
|
33
|
+
import { useStableCallback } from './utils/useStableCallback';
|
|
34
|
+
import { useDismiss } from './utils/floating/useDismiss';
|
|
35
|
+
import { useFocus } from './utils/floating/useFocus';
|
|
36
|
+
import {
|
|
37
|
+
safePolygon,
|
|
38
|
+
useHoverReferenceInteraction,
|
|
39
|
+
} from './utils/floating/useHoverReferenceInteraction';
|
|
40
|
+
import { useHoverFloatingInteraction } from './utils/floating/useHoverFloatingInteraction';
|
|
41
|
+
import { FloatingTree, FloatingNode, useFloatingNodeId } from './utils/floating/FloatingTree';
|
|
42
|
+
import { FloatingPortalLite } from './utils/FloatingPortalLite';
|
|
43
|
+
import {
|
|
44
|
+
useAnchorPositioning,
|
|
45
|
+
type Side,
|
|
46
|
+
type Align,
|
|
47
|
+
type UseAnchorPositioningSharedParameters,
|
|
48
|
+
} from './utils/useAnchorPositioning';
|
|
49
|
+
import { usePositioner } from './utils/usePositioner';
|
|
50
|
+
import { usePopupViewport } from './utils/usePopupViewport';
|
|
51
|
+
import { adaptiveOrigin } from './utils/adaptiveOriginMiddleware';
|
|
52
|
+
import { useOpenChangeComplete } from './utils/useOpenChangeComplete';
|
|
53
|
+
import { getDisabledMountTransitionStyles } from './utils/getDisabledMountTransitionStyles';
|
|
54
|
+
import { POPUP_COLLISION_AVOIDANCE } from './utils/constants';
|
|
55
|
+
import { popupStateMapping, triggerOpenStateMapping } from './utils/popupStateMapping';
|
|
56
|
+
import { transitionStatusMapping } from './utils/useTransitionStatus';
|
|
57
|
+
import type { StateAttributesMapping } from './utils/getStateAttributesProps';
|
|
58
|
+
import {
|
|
59
|
+
createInitialPopupStoreState,
|
|
60
|
+
createPopupFloatingRootContext,
|
|
61
|
+
popupStoreSelectors,
|
|
62
|
+
applyPopupOpenChange,
|
|
63
|
+
usePopupStore,
|
|
64
|
+
useInitialOpenSync,
|
|
65
|
+
useImplicitActiveTrigger,
|
|
66
|
+
useOpenStateTransitions,
|
|
67
|
+
usePopupInteractionProps,
|
|
68
|
+
useTriggerDataForwarding,
|
|
69
|
+
FOCUSABLE_POPUP_PROPS,
|
|
70
|
+
createInlineMiddleware,
|
|
71
|
+
getInlineRectTriggerProps,
|
|
72
|
+
updateInlineRectCoords,
|
|
73
|
+
type InlineRectCoords,
|
|
74
|
+
type PopupStoreState,
|
|
75
|
+
type PopupStoreContext,
|
|
76
|
+
} from './utils/popups';
|
|
77
|
+
import { PopupTriggerMap } from './utils/popups/popupTriggerMap';
|
|
78
|
+
|
|
79
|
+
export const OPEN_DELAY = 600;
|
|
80
|
+
export const CLOSE_DELAY = 300;
|
|
81
|
+
|
|
82
|
+
export const PreviewCardViewportCssVars = {
|
|
83
|
+
popupWidth: '--popup-width',
|
|
84
|
+
popupHeight: '--popup-height',
|
|
85
|
+
} as const;
|
|
86
|
+
|
|
87
|
+
// --- Store -------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
export type PreviewCardState<Payload> = PopupStoreState<Payload> & {
|
|
90
|
+
instantType: 'dismiss' | 'focus' | undefined;
|
|
91
|
+
hasViewport: boolean;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type PreviewCardContext = PopupStoreContext<any> & {
|
|
95
|
+
readonly popupRef: { current: HTMLElement | null };
|
|
96
|
+
readonly closeDelayRef: { current: number };
|
|
97
|
+
readonly inlineRectCoordsRef: { current: InlineRectCoords | undefined };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const previewCardSelectors = {
|
|
101
|
+
...popupStoreSelectors,
|
|
102
|
+
instantType: createSelector((state: PreviewCardState<unknown>) => state.instantType),
|
|
103
|
+
hasViewport: createSelector((state: PreviewCardState<unknown>) => state.hasViewport),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function createInitialPreviewCardState<Payload>(): PreviewCardState<Payload> {
|
|
107
|
+
return {
|
|
108
|
+
...createInitialPopupStoreState<Payload>(),
|
|
109
|
+
instantType: undefined,
|
|
110
|
+
hasViewport: false,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class PreviewCardStore<Payload> extends ReactStore<
|
|
115
|
+
Readonly<PreviewCardState<Payload>>,
|
|
116
|
+
PreviewCardContext,
|
|
117
|
+
typeof previewCardSelectors
|
|
118
|
+
> {
|
|
119
|
+
constructor(
|
|
120
|
+
initialState?: Partial<PreviewCardState<Payload>>,
|
|
121
|
+
floatingId?: string | undefined,
|
|
122
|
+
nested = false,
|
|
123
|
+
) {
|
|
124
|
+
const triggerElements = new PopupTriggerMap();
|
|
125
|
+
const state = { ...createInitialPreviewCardState<Payload>(), ...initialState };
|
|
126
|
+
state.floatingRootContext = createPopupFloatingRootContext(triggerElements, floatingId, nested);
|
|
127
|
+
|
|
128
|
+
super(
|
|
129
|
+
state,
|
|
130
|
+
{
|
|
131
|
+
popupRef: { current: null },
|
|
132
|
+
onOpenChange: undefined,
|
|
133
|
+
onOpenChangeComplete: undefined,
|
|
134
|
+
triggerElements,
|
|
135
|
+
closeDelayRef: { current: CLOSE_DELAY },
|
|
136
|
+
inlineRectCoordsRef: { current: undefined },
|
|
137
|
+
} as PreviewCardContext,
|
|
138
|
+
previewCardSelectors,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setOpen = (nextOpen: boolean, eventDetails: any) => {
|
|
143
|
+
const { inlineRectCoordsRef } = this.context;
|
|
144
|
+
|
|
145
|
+
applyPopupOpenChange(this as any, nextOpen, eventDetails, {
|
|
146
|
+
onBeforeDispatch() {
|
|
147
|
+
// Capture the hovered inline-rect coordinates so the card anchors to the exact point
|
|
148
|
+
// on the link that was hovered.
|
|
149
|
+
const event = eventDetails.event;
|
|
150
|
+
if (
|
|
151
|
+
nextOpen &&
|
|
152
|
+
eventDetails.reason === REASONS.triggerHover &&
|
|
153
|
+
eventDetails.trigger &&
|
|
154
|
+
event &&
|
|
155
|
+
'clientX' in event &&
|
|
156
|
+
'clientY' in event &&
|
|
157
|
+
inlineRectCoordsRef.current?.element !== eventDetails.trigger
|
|
158
|
+
) {
|
|
159
|
+
updateInlineRectCoords(
|
|
160
|
+
inlineRectCoordsRef,
|
|
161
|
+
eventDetails.trigger,
|
|
162
|
+
event.clientX,
|
|
163
|
+
event.clientY,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
} as any);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
static useStore<Payload>(
|
|
171
|
+
externalStore: PreviewCardStore<Payload> | undefined,
|
|
172
|
+
initialState: Partial<PreviewCardState<Payload>>,
|
|
173
|
+
slot: symbol | undefined,
|
|
174
|
+
): PreviewCardStore<Payload> {
|
|
175
|
+
return usePopupStore(
|
|
176
|
+
externalStore as any,
|
|
177
|
+
(floatingId, nested) =>
|
|
178
|
+
new PreviewCardStore<Payload>(initialState, floatingId, nested) as any,
|
|
179
|
+
true,
|
|
180
|
+
slot,
|
|
181
|
+
).store as unknown as PreviewCardStore<Payload>;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- Handle ------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
export class PreviewCardHandle<Payload> {
|
|
188
|
+
readonly store: PreviewCardStore<Payload>;
|
|
189
|
+
|
|
190
|
+
constructor(store?: PreviewCardStore<Payload>) {
|
|
191
|
+
this.store = store ?? new PreviewCardStore<Payload>();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
open(triggerId: string): void {
|
|
195
|
+
const triggerElement = triggerId
|
|
196
|
+
? (this.store.context.triggerElements.getById(triggerId) as HTMLElement | undefined)
|
|
197
|
+
: undefined;
|
|
198
|
+
|
|
199
|
+
if (triggerId && !triggerElement) {
|
|
200
|
+
throw new Error(`Base UI: PreviewCardHandle.open: No trigger found with id "${triggerId}".`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.store.setOpen(
|
|
204
|
+
true,
|
|
205
|
+
createChangeEventDetails(REASONS.imperativeAction, undefined, triggerElement),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
close(): void {
|
|
210
|
+
this.store.setOpen(
|
|
211
|
+
false,
|
|
212
|
+
createChangeEventDetails(REASONS.imperativeAction, undefined, undefined),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
get isOpen(): boolean {
|
|
217
|
+
return this.store.select('open');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function createPreviewCardHandle<Payload = unknown>(): PreviewCardHandle<Payload> {
|
|
222
|
+
return new PreviewCardHandle<Payload>();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- Contexts ----------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
const PreviewCardRootContext = createContext<PreviewCardStore<any> | undefined>(undefined);
|
|
228
|
+
|
|
229
|
+
export function usePreviewCardRootContext(optional?: boolean): PreviewCardStore<any> | undefined {
|
|
230
|
+
const context = useContext(PreviewCardRootContext);
|
|
231
|
+
if (context === undefined && !optional) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
'Base UI: PreviewCardRootContext is missing. PreviewCard parts must be placed within <PreviewCard.Root>.',
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return context;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const PreviewCardPortalContext = createContext<boolean | undefined>(undefined);
|
|
240
|
+
|
|
241
|
+
function usePreviewCardPortalContext(): boolean {
|
|
242
|
+
const value = useContext(PreviewCardPortalContext);
|
|
243
|
+
if (value === undefined) {
|
|
244
|
+
throw new Error('Base UI: <PreviewCard.Portal> is missing.');
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface PreviewCardPositionerContextValue {
|
|
250
|
+
side: Side;
|
|
251
|
+
align: Align;
|
|
252
|
+
arrowRef: { current: Element | null };
|
|
253
|
+
arrowUncentered: boolean;
|
|
254
|
+
arrowStyles: Record<string, any>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const PreviewCardPositionerContext = createContext<PreviewCardPositionerContextValue | undefined>(
|
|
258
|
+
undefined,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
export function usePreviewCardPositionerContext(): PreviewCardPositionerContextValue {
|
|
262
|
+
const context = useContext(PreviewCardPositionerContext);
|
|
263
|
+
if (!context) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
'Base UI: PreviewCardPositionerContext is missing. PreviewCardPositioner parts must be placed within <PreviewCard.Positioner>.',
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return context;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- Interactions ------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
function PreviewCardInteractions(props: any): any {
|
|
274
|
+
const slot = S('PreviewCardInteractions');
|
|
275
|
+
const { store } = props;
|
|
276
|
+
|
|
277
|
+
const floatingRootContext = store.useState('floatingRootContext', subSlot(slot, 'frc'));
|
|
278
|
+
|
|
279
|
+
const dismiss = useDismiss(floatingRootContext, {}, subSlot(slot, 'dismiss'));
|
|
280
|
+
const activeTriggerProps = dismiss.reference ?? EMPTY_OBJECT;
|
|
281
|
+
const inactiveTriggerProps = dismiss.trigger ?? EMPTY_OBJECT;
|
|
282
|
+
const popupProps = useMemo(
|
|
283
|
+
() => mergeProps(FOCUSABLE_POPUP_PROPS, dismiss.floating),
|
|
284
|
+
[dismiss.floating],
|
|
285
|
+
subSlot(slot, 'pp'),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
usePopupInteractionProps(
|
|
289
|
+
store,
|
|
290
|
+
{ activeTriggerProps, inactiveTriggerProps, popupProps } as any,
|
|
291
|
+
subSlot(slot, 'pip'),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- Root --------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
function PreviewCardRootComponent<Payload>(props: any): any {
|
|
300
|
+
const slot = S('PreviewCardRoot');
|
|
301
|
+
const {
|
|
302
|
+
open: openProp,
|
|
303
|
+
defaultOpen = false,
|
|
304
|
+
onOpenChange,
|
|
305
|
+
onOpenChangeComplete,
|
|
306
|
+
actionsRef,
|
|
307
|
+
handle,
|
|
308
|
+
triggerId: triggerIdProp,
|
|
309
|
+
defaultTriggerId: defaultTriggerIdProp = null,
|
|
310
|
+
children,
|
|
311
|
+
} = props;
|
|
312
|
+
|
|
313
|
+
const store = PreviewCardStore.useStore<Payload>(
|
|
314
|
+
handle?.store,
|
|
315
|
+
{
|
|
316
|
+
open: defaultOpen,
|
|
317
|
+
openProp,
|
|
318
|
+
activeTriggerId: defaultTriggerIdProp,
|
|
319
|
+
triggerIdProp,
|
|
320
|
+
} as any,
|
|
321
|
+
subSlot(slot, 'store'),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
useInitialOpenSync(store, openProp, defaultOpen, defaultTriggerIdProp, subSlot(slot, 'ios'));
|
|
325
|
+
|
|
326
|
+
store.useControlledProp('openProp', openProp, subSlot(slot, 'cp:open'));
|
|
327
|
+
store.useControlledProp('triggerIdProp', triggerIdProp, subSlot(slot, 'cp:trigger'));
|
|
328
|
+
|
|
329
|
+
store.useContextCallback('onOpenChange', onOpenChange, subSlot(slot, 'cb:open'));
|
|
330
|
+
store.useContextCallback(
|
|
331
|
+
'onOpenChangeComplete',
|
|
332
|
+
onOpenChangeComplete,
|
|
333
|
+
subSlot(slot, 'cb:complete'),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
337
|
+
const activeTriggerId = store.useState('activeTriggerId', subSlot(slot, 'atid'));
|
|
338
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
339
|
+
const payload = store.useState('payload', subSlot(slot, 'payload'));
|
|
340
|
+
|
|
341
|
+
useImplicitActiveTrigger(store, { closeOnActiveTriggerUnmount: true }, subSlot(slot, 'implicit'));
|
|
342
|
+
const { forceUnmount } = useOpenStateTransitions(
|
|
343
|
+
open,
|
|
344
|
+
store as any,
|
|
345
|
+
() => {
|
|
346
|
+
store.context.inlineRectCoordsRef.current = undefined;
|
|
347
|
+
},
|
|
348
|
+
subSlot(slot, 'transitions'),
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
useLayoutEffect(
|
|
352
|
+
() => {
|
|
353
|
+
if (open && activeTriggerId == null) {
|
|
354
|
+
store.set('payload', undefined);
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
[store, activeTriggerId, open],
|
|
358
|
+
subSlot(slot, 'l:payload'),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const handleImperativeClose = useCallback(
|
|
362
|
+
() => {
|
|
363
|
+
store.setOpen(false, createChangeEventDetails(REASONS.imperativeAction));
|
|
364
|
+
},
|
|
365
|
+
[store],
|
|
366
|
+
subSlot(slot, 'close'),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
useImperativeHandle(
|
|
370
|
+
actionsRef,
|
|
371
|
+
() => ({ unmount: forceUnmount, close: handleImperativeClose }),
|
|
372
|
+
[forceUnmount, handleImperativeClose],
|
|
373
|
+
subSlot(slot, 'imp'),
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const shouldRenderInteractions = open || mounted;
|
|
377
|
+
|
|
378
|
+
const resolvedChildren =
|
|
379
|
+
typeof children === 'function' && !isChildrenBlock(children) ? children({ payload }) : children;
|
|
380
|
+
|
|
381
|
+
// `PreviewCardInteractions` is a headless SIBLING of the children, exactly as Base UI renders it.
|
|
382
|
+
const content = [
|
|
383
|
+
shouldRenderInteractions
|
|
384
|
+
? createElement(PreviewCardInteractions, { key: 'interactions', store })
|
|
385
|
+
: null,
|
|
386
|
+
createElement(Fragment, { key: 'children', children: resolvedChildren }),
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
return createElement(PreviewCardRootContext.Provider, { value: store, children: content });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function PreviewCardRoot<Payload = unknown>(props: any): any {
|
|
393
|
+
// Top-level preview cards establish a FloatingTree; nested ones reuse the parent's.
|
|
394
|
+
if (usePreviewCardRootContext(true)) {
|
|
395
|
+
return createElement(PreviewCardRootComponent, props);
|
|
396
|
+
}
|
|
397
|
+
return createElement(FloatingTree, {
|
|
398
|
+
children: createElement(PreviewCardRootComponent, props),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// --- Trigger -----------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
function PreviewCardTrigger(componentProps: any): any {
|
|
405
|
+
const slot = S('PreviewCardTrigger');
|
|
406
|
+
const {
|
|
407
|
+
render,
|
|
408
|
+
className,
|
|
409
|
+
delay,
|
|
410
|
+
closeDelay,
|
|
411
|
+
id: idProp,
|
|
412
|
+
payload,
|
|
413
|
+
handle,
|
|
414
|
+
style,
|
|
415
|
+
ref,
|
|
416
|
+
...elementProps
|
|
417
|
+
} = componentProps;
|
|
418
|
+
|
|
419
|
+
const rootContext = usePreviewCardRootContext(true);
|
|
420
|
+
const store = (handle?.store ?? rootContext) as PreviewCardStore<any> | undefined;
|
|
421
|
+
if (!store) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
'Base UI: <PreviewCard.Trigger> must be either used within a <PreviewCard.Root> component or provided with a handle.',
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const thisTriggerId = useBaseUiId(idProp, subSlot(slot, 'id'));
|
|
428
|
+
const isTriggerActive = store.useState('isTriggerActive', subSlot(slot, 'ita'), thisTriggerId);
|
|
429
|
+
const isOpenedByThisTrigger = store.useState(
|
|
430
|
+
'isOpenedByTrigger',
|
|
431
|
+
subSlot(slot, 'obt'),
|
|
432
|
+
thisTriggerId,
|
|
433
|
+
);
|
|
434
|
+
const floatingRootContext = store.useState('floatingRootContext', subSlot(slot, 'frc'));
|
|
435
|
+
const inlineRectCoordsRef = store.context.inlineRectCoordsRef;
|
|
436
|
+
|
|
437
|
+
const triggerElementRef = useRef<Element | null>(null, subSlot(slot, 'trigEl'));
|
|
438
|
+
|
|
439
|
+
const delayWithDefault = delay ?? OPEN_DELAY;
|
|
440
|
+
const closeDelayWithDefault = closeDelay ?? CLOSE_DELAY;
|
|
441
|
+
|
|
442
|
+
const { registerTrigger, isMountedByThisTrigger } = useTriggerDataForwarding(
|
|
443
|
+
thisTriggerId,
|
|
444
|
+
triggerElementRef,
|
|
445
|
+
store,
|
|
446
|
+
{ payload },
|
|
447
|
+
subSlot(slot, 'tdf'),
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
useLayoutEffect(
|
|
451
|
+
() => {
|
|
452
|
+
if (isMountedByThisTrigger) {
|
|
453
|
+
store.context.closeDelayRef.current = closeDelayWithDefault;
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
[store, isMountedByThisTrigger, closeDelayWithDefault],
|
|
457
|
+
subSlot(slot, 'l:closeDelay'),
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
const hoverProps = useHoverReferenceInteraction(
|
|
461
|
+
floatingRootContext,
|
|
462
|
+
{
|
|
463
|
+
mouseOnly: true,
|
|
464
|
+
move: false,
|
|
465
|
+
handleClose: safePolygon(),
|
|
466
|
+
delay: () => ({ open: delayWithDefault, close: closeDelayWithDefault }),
|
|
467
|
+
triggerElementRef,
|
|
468
|
+
isActiveTrigger: isTriggerActive,
|
|
469
|
+
isClosing: () => store.select('transitionStatus') === 'ending',
|
|
470
|
+
},
|
|
471
|
+
subSlot(slot, 'hover'),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const focusProps = useFocus(
|
|
475
|
+
floatingRootContext,
|
|
476
|
+
{ delay: delayWithDefault },
|
|
477
|
+
subSlot(slot, 'focus'),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const state = { open: isOpenedByThisTrigger };
|
|
481
|
+
|
|
482
|
+
const rootTriggerProps = store.useState(
|
|
483
|
+
'triggerProps',
|
|
484
|
+
subSlot(slot, 'tp'),
|
|
485
|
+
isMountedByThisTrigger,
|
|
486
|
+
);
|
|
487
|
+
const inlineRectTriggerProps = getInlineRectTriggerProps(
|
|
488
|
+
inlineRectCoordsRef,
|
|
489
|
+
isOpenedByThisTrigger,
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
return useRenderElement(
|
|
493
|
+
'a',
|
|
494
|
+
{ render, className, style },
|
|
495
|
+
{
|
|
496
|
+
state,
|
|
497
|
+
ref: [ref, registerTrigger, triggerElementRef],
|
|
498
|
+
props: [
|
|
499
|
+
hoverProps,
|
|
500
|
+
focusProps.reference,
|
|
501
|
+
rootTriggerProps,
|
|
502
|
+
inlineRectTriggerProps,
|
|
503
|
+
{ id: thisTriggerId },
|
|
504
|
+
elementProps,
|
|
505
|
+
],
|
|
506
|
+
stateAttributesMapping: triggerOpenStateMapping,
|
|
507
|
+
},
|
|
508
|
+
subSlot(slot, 're'),
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// --- Portal ------------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
function PreviewCardPortal(props: any): any {
|
|
515
|
+
const slot = S('PreviewCardPortal');
|
|
516
|
+
const { keepMounted = false, ref, ...portalProps } = props;
|
|
517
|
+
|
|
518
|
+
const store = usePreviewCardRootContext() as PreviewCardStore<any>;
|
|
519
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
520
|
+
|
|
521
|
+
if (!(mounted || keepMounted)) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return createElement(PreviewCardPortalContext.Provider, {
|
|
526
|
+
value: keepMounted,
|
|
527
|
+
children: createElement(FloatingPortalLite, { ref, ...portalProps }),
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// --- Positioner --------------------------------------------------------------
|
|
532
|
+
|
|
533
|
+
function PreviewCardPositioner(componentProps: any): any {
|
|
534
|
+
const slot = S('PreviewCardPositioner');
|
|
535
|
+
const {
|
|
536
|
+
render,
|
|
537
|
+
className,
|
|
538
|
+
style,
|
|
539
|
+
anchor,
|
|
540
|
+
positionMethod = 'absolute',
|
|
541
|
+
side = 'bottom',
|
|
542
|
+
align = 'center',
|
|
543
|
+
sideOffset = 0,
|
|
544
|
+
alignOffset = 0,
|
|
545
|
+
collisionBoundary = 'clipping-ancestors',
|
|
546
|
+
collisionPadding = 5,
|
|
547
|
+
arrowPadding = 5,
|
|
548
|
+
sticky = false,
|
|
549
|
+
disableAnchorTracking = false,
|
|
550
|
+
collisionAvoidance = POPUP_COLLISION_AVOIDANCE,
|
|
551
|
+
ref,
|
|
552
|
+
...elementProps
|
|
553
|
+
} = componentProps;
|
|
554
|
+
|
|
555
|
+
const store = usePreviewCardRootContext() as PreviewCardStore<any>;
|
|
556
|
+
const keepMounted = usePreviewCardPortalContext();
|
|
557
|
+
const nodeId = useFloatingNodeId(undefined, subSlot(slot, 'nodeId'));
|
|
558
|
+
|
|
559
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
560
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
561
|
+
const floatingRootContext = store.useState('floatingRootContext', subSlot(slot, 'frc'));
|
|
562
|
+
const instantType = store.useState('instantType', subSlot(slot, 'instant'));
|
|
563
|
+
const transitionStatus = store.useState('transitionStatus', subSlot(slot, 'trans'));
|
|
564
|
+
const hasViewport = store.useState('hasViewport', subSlot(slot, 'viewport'));
|
|
565
|
+
const inlineRectCoordsRef = store.context.inlineRectCoordsRef;
|
|
566
|
+
|
|
567
|
+
const inlineMiddleware = useMemo(
|
|
568
|
+
() => createInlineMiddleware(inlineRectCoordsRef),
|
|
569
|
+
[inlineRectCoordsRef],
|
|
570
|
+
subSlot(slot, 'inline'),
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
const positioning = useAnchorPositioning(
|
|
574
|
+
{
|
|
575
|
+
anchor,
|
|
576
|
+
floatingRootContext,
|
|
577
|
+
positionMethod,
|
|
578
|
+
mounted,
|
|
579
|
+
side,
|
|
580
|
+
sideOffset,
|
|
581
|
+
align,
|
|
582
|
+
alignOffset,
|
|
583
|
+
arrowPadding,
|
|
584
|
+
collisionBoundary,
|
|
585
|
+
collisionPadding,
|
|
586
|
+
sticky,
|
|
587
|
+
disableAnchorTracking,
|
|
588
|
+
keepMounted,
|
|
589
|
+
nodeId,
|
|
590
|
+
collisionAvoidance,
|
|
591
|
+
adaptiveOrigin: hasViewport ? adaptiveOrigin : undefined,
|
|
592
|
+
inline: inlineMiddleware,
|
|
593
|
+
},
|
|
594
|
+
subSlot(slot, 'pos'),
|
|
595
|
+
);
|
|
596
|
+
const updatePosition = positioning.update;
|
|
597
|
+
|
|
598
|
+
useLayoutEffect(
|
|
599
|
+
() => {
|
|
600
|
+
if (open && mounted) {
|
|
601
|
+
updatePosition();
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
[open, mounted, updatePosition],
|
|
605
|
+
subSlot(slot, 'l:update'),
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
const state = {
|
|
609
|
+
open,
|
|
610
|
+
side: positioning.side,
|
|
611
|
+
align: positioning.align,
|
|
612
|
+
anchorHidden: positioning.anchorHidden,
|
|
613
|
+
instant: instantType,
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const setPositionerElement = store.useStateSetter('positionerElement', subSlot(slot, 'spe'));
|
|
617
|
+
|
|
618
|
+
const element = usePositioner(
|
|
619
|
+
{ render, className, style },
|
|
620
|
+
state,
|
|
621
|
+
{
|
|
622
|
+
styles: positioning.positionerStyles,
|
|
623
|
+
transitionStatus,
|
|
624
|
+
props: elementProps,
|
|
625
|
+
refs: [ref, setPositionerElement],
|
|
626
|
+
hidden: !mounted,
|
|
627
|
+
inert: !open,
|
|
628
|
+
},
|
|
629
|
+
subSlot(slot, 'positioner'),
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
return createElement(PreviewCardPositionerContext.Provider, {
|
|
633
|
+
value: positioning as PreviewCardPositionerContextValue,
|
|
634
|
+
children: createElement(FloatingNode, { id: nodeId, children: element }),
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// --- Popup -------------------------------------------------------------------
|
|
639
|
+
|
|
640
|
+
const popupStateAttributesMapping: StateAttributesMapping<any> = {
|
|
641
|
+
...(popupStateMapping as StateAttributesMapping<any>),
|
|
642
|
+
...(transitionStatusMapping as StateAttributesMapping<any>),
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
function PreviewCardPopup(componentProps: any): any {
|
|
646
|
+
const slot = S('PreviewCardPopup');
|
|
647
|
+
const { className, render, style, ref, ...elementProps } = componentProps;
|
|
648
|
+
|
|
649
|
+
const store = usePreviewCardRootContext() as PreviewCardStore<any>;
|
|
650
|
+
const { side, align } = usePreviewCardPositionerContext();
|
|
651
|
+
|
|
652
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
653
|
+
const instantType = store.useState('instantType', subSlot(slot, 'instant'));
|
|
654
|
+
const transitionStatus = store.useState('transitionStatus', subSlot(slot, 'trans'));
|
|
655
|
+
const popupProps = store.useState('popupProps', subSlot(slot, 'pp'));
|
|
656
|
+
const floatingContext = store.useState('floatingRootContext', subSlot(slot, 'frc'));
|
|
657
|
+
|
|
658
|
+
useOpenChangeComplete(
|
|
659
|
+
{
|
|
660
|
+
open,
|
|
661
|
+
ref: store.context.popupRef,
|
|
662
|
+
onComplete() {
|
|
663
|
+
if (open) {
|
|
664
|
+
store.context.onOpenChangeComplete?.(true);
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
subSlot(slot, 'occ'),
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
// The close delay lives on the store so the active trigger owns it.
|
|
672
|
+
const getCloseDelay = useStableCallback(
|
|
673
|
+
() => store.context.closeDelayRef.current,
|
|
674
|
+
subSlot(slot, 'gcd'),
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
useHoverFloatingInteraction(
|
|
678
|
+
floatingContext,
|
|
679
|
+
{ closeDelay: getCloseDelay },
|
|
680
|
+
subSlot(slot, 'hoverPopup'),
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
const setPopupElement = store.useStateSetter('popupElement', subSlot(slot, 'spe'));
|
|
684
|
+
|
|
685
|
+
const state = { open, side, align, instant: instantType, transitionStatus };
|
|
686
|
+
|
|
687
|
+
return useRenderElement(
|
|
688
|
+
'div',
|
|
689
|
+
{ render, className, style },
|
|
690
|
+
{
|
|
691
|
+
state,
|
|
692
|
+
ref: [ref, store.context.popupRef, setPopupElement],
|
|
693
|
+
props: [popupProps, getDisabledMountTransitionStyles(transitionStatus), elementProps],
|
|
694
|
+
stateAttributesMapping: popupStateAttributesMapping,
|
|
695
|
+
},
|
|
696
|
+
subSlot(slot, 're'),
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// --- Backdrop ----------------------------------------------------------------
|
|
701
|
+
|
|
702
|
+
function PreviewCardBackdrop(componentProps: any): any {
|
|
703
|
+
const slot = S('PreviewCardBackdrop');
|
|
704
|
+
const { render, className, style, ref, ...elementProps } = componentProps;
|
|
705
|
+
|
|
706
|
+
const store = usePreviewCardRootContext() as PreviewCardStore<any>;
|
|
707
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
708
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
709
|
+
const transitionStatus = store.useState('transitionStatus', subSlot(slot, 'trans'));
|
|
710
|
+
|
|
711
|
+
const state = { open, transitionStatus };
|
|
712
|
+
|
|
713
|
+
return useRenderElement(
|
|
714
|
+
'div',
|
|
715
|
+
{ render, className, style },
|
|
716
|
+
{
|
|
717
|
+
state,
|
|
718
|
+
ref: [ref],
|
|
719
|
+
props: [
|
|
720
|
+
{
|
|
721
|
+
role: 'presentation',
|
|
722
|
+
hidden: !mounted,
|
|
723
|
+
style: { pointerEvents: 'none', userSelect: 'none', WebkitUserSelect: 'none' },
|
|
724
|
+
},
|
|
725
|
+
elementProps,
|
|
726
|
+
],
|
|
727
|
+
stateAttributesMapping: popupStateAttributesMapping,
|
|
728
|
+
},
|
|
729
|
+
subSlot(slot, 're'),
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// --- Arrow -------------------------------------------------------------------
|
|
734
|
+
|
|
735
|
+
function PreviewCardArrow(componentProps: any): any {
|
|
736
|
+
const slot = S('PreviewCardArrow');
|
|
737
|
+
const { render, className, style, ref, ...elementProps } = componentProps;
|
|
738
|
+
|
|
739
|
+
const store = usePreviewCardRootContext() as PreviewCardStore<any>;
|
|
740
|
+
const { arrowRef, side, align, arrowUncentered, arrowStyles } = usePreviewCardPositionerContext();
|
|
741
|
+
|
|
742
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
743
|
+
|
|
744
|
+
const state = { open, side, align, uncentered: arrowUncentered };
|
|
745
|
+
|
|
746
|
+
return useRenderElement(
|
|
747
|
+
'div',
|
|
748
|
+
{ render, className, style },
|
|
749
|
+
{
|
|
750
|
+
state,
|
|
751
|
+
ref: [arrowRef, ref],
|
|
752
|
+
props: [{ style: arrowStyles, 'aria-hidden': true }, elementProps],
|
|
753
|
+
stateAttributesMapping: popupStateMapping as StateAttributesMapping<any>,
|
|
754
|
+
},
|
|
755
|
+
subSlot(slot, 're'),
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// --- Viewport ----------------------------------------------------------------
|
|
760
|
+
|
|
761
|
+
const viewportStateAttributesMapping: StateAttributesMapping<any> = {
|
|
762
|
+
activationDirection: (value: string | undefined) =>
|
|
763
|
+
value ? { 'data-activation-direction': value } : null,
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
function PreviewCardViewport(componentProps: any): any {
|
|
767
|
+
const slot = S('PreviewCardViewport');
|
|
768
|
+
const { render, className, style, children, ref, ...elementProps } = componentProps;
|
|
769
|
+
|
|
770
|
+
const store = usePreviewCardRootContext() as PreviewCardStore<any>;
|
|
771
|
+
const positioner = usePreviewCardPositionerContext();
|
|
772
|
+
|
|
773
|
+
const instantType = store.useState('instantType', subSlot(slot, 'instant'));
|
|
774
|
+
|
|
775
|
+
const { children: childrenToRender, state: viewportState } = usePopupViewport(
|
|
776
|
+
{ store, side: positioner.side, cssVars: PreviewCardViewportCssVars, children },
|
|
777
|
+
subSlot(slot, 'vp'),
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
const state = {
|
|
781
|
+
activationDirection: viewportState.activationDirection,
|
|
782
|
+
transitioning: viewportState.transitioning,
|
|
783
|
+
instant: instantType,
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
return useRenderElement(
|
|
787
|
+
'div',
|
|
788
|
+
{ render, className, style },
|
|
789
|
+
{
|
|
790
|
+
state,
|
|
791
|
+
ref,
|
|
792
|
+
props: [elementProps, { children: childrenToRender }],
|
|
793
|
+
stateAttributesMapping: viewportStateAttributesMapping,
|
|
794
|
+
},
|
|
795
|
+
subSlot(slot, 're'),
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// --- Namespace ---------------------------------------------------------------
|
|
800
|
+
|
|
801
|
+
export const PreviewCard = {
|
|
802
|
+
Root: PreviewCardRoot,
|
|
803
|
+
Trigger: PreviewCardTrigger,
|
|
804
|
+
Portal: PreviewCardPortal,
|
|
805
|
+
Positioner: PreviewCardPositioner,
|
|
806
|
+
Popup: PreviewCardPopup,
|
|
807
|
+
Backdrop: PreviewCardBackdrop,
|
|
808
|
+
Arrow: PreviewCardArrow,
|
|
809
|
+
Viewport: PreviewCardViewport,
|
|
810
|
+
createHandle: createPreviewCardHandle,
|
|
811
|
+
Handle: PreviewCardHandle,
|
|
812
|
+
};
|