@octanejs/base-ui 0.1.1 → 0.1.2
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
package/src/popover.ts
ADDED
|
@@ -0,0 +1,1205 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/popover/ (v1.6.0): store (PopoverStore/PopoverHandle),
|
|
2
|
+
// root (context/PopoverRoot/PopoverInteractions), trigger. The CLOSED-state path (Root + Trigger),
|
|
3
|
+
// reusing the Store + floating stack from Dialog. octane adaptations: forwardRef → ref-as-prop;
|
|
4
|
+
// native events; every Store hook-method + hook threads an explicit slot; `React.createContext` →
|
|
5
|
+
// octane `createContext`; `ReactDOM.flushSync` → octane `flushSync`.
|
|
6
|
+
//
|
|
7
|
+
// STUBBED/DEFERRED (behind off-by-default features): `useHoverReferenceInteraction` (openOnHover);
|
|
8
|
+
// the Positioner + Popup + Arrow + Backdrop + Title/Description/Close parts (the open path) land next
|
|
9
|
+
// with `useAnchorPositioning`. Interactions are only mounted when `open || mounted`, and — as with
|
|
10
|
+
// Dialog — a stable no-DOM descriptor wraps the children (octane Provider children shape-flip bug).
|
|
11
|
+
import {
|
|
12
|
+
createContext,
|
|
13
|
+
createElement,
|
|
14
|
+
useContext,
|
|
15
|
+
useMemo,
|
|
16
|
+
useRef,
|
|
17
|
+
useEffect,
|
|
18
|
+
useLayoutEffect,
|
|
19
|
+
useCallback,
|
|
20
|
+
useImperativeHandle,
|
|
21
|
+
isChildrenBlock,
|
|
22
|
+
flushSync,
|
|
23
|
+
} from 'octane';
|
|
24
|
+
|
|
25
|
+
import { S, subSlot } from './internal';
|
|
26
|
+
import { useRenderElement } from './utils/useRenderElement';
|
|
27
|
+
import { mergeProps } from './utils/mergeProps';
|
|
28
|
+
import { useButton } from './utils/useButton';
|
|
29
|
+
import { useBaseUiId } from './utils/useBaseUiId';
|
|
30
|
+
import { createChangeEventDetails, REASONS } from './utils/createChangeEventDetails';
|
|
31
|
+
import { EMPTY_OBJECT } from './utils/empty';
|
|
32
|
+
import { ReactStore } from './utils/store/ReactStore';
|
|
33
|
+
import { createSelector } from './utils/store/createSelector';
|
|
34
|
+
import { Timeout } from './utils/useTimeout';
|
|
35
|
+
import { useClick } from './utils/floating/useClick';
|
|
36
|
+
import { useDismiss } from './utils/floating/useDismiss';
|
|
37
|
+
import { FocusGuard } from './utils/floating/FocusGuard';
|
|
38
|
+
import {
|
|
39
|
+
FloatingTree,
|
|
40
|
+
FloatingNode,
|
|
41
|
+
useFloatingParentNodeId,
|
|
42
|
+
useFloatingNodeId,
|
|
43
|
+
} from './utils/floating/FloatingTree';
|
|
44
|
+
import { FloatingFocusManager } from './utils/floating/FloatingFocusManager';
|
|
45
|
+
import { FloatingPortal } from './utils/floating/FloatingPortal';
|
|
46
|
+
import { useHoverFloatingInteraction } from './utils/floating/useHoverFloatingInteraction';
|
|
47
|
+
import {
|
|
48
|
+
useAnchorPositioning,
|
|
49
|
+
type Side,
|
|
50
|
+
type Align,
|
|
51
|
+
type UseAnchorPositioningSharedParameters,
|
|
52
|
+
} from './utils/useAnchorPositioning';
|
|
53
|
+
import { usePositioner } from './utils/usePositioner';
|
|
54
|
+
import { useAnchoredPopupScrollLock } from './utils/useAnchoredPopupScrollLock';
|
|
55
|
+
import { adaptiveOrigin } from './utils/adaptiveOriginMiddleware';
|
|
56
|
+
import { InternalBackdrop } from './utils/InternalBackdrop';
|
|
57
|
+
import { useAnimationsFinished } from './utils/useAnimationsFinished';
|
|
58
|
+
import { useOpenChangeComplete } from './utils/useOpenChangeComplete';
|
|
59
|
+
import { getDisabledMountTransitionStyles } from './utils/getDisabledMountTransitionStyles';
|
|
60
|
+
import { POPUP_COLLISION_AVOIDANCE } from './utils/constants';
|
|
61
|
+
import { ClosePartProvider, useClosePartCount, useClosePartRegistration } from './utils/closePart';
|
|
62
|
+
import { popupStateMapping } from './utils/popupStateMapping';
|
|
63
|
+
import { transitionStatusMapping } from './utils/useTransitionStatus';
|
|
64
|
+
import { COMPOSITE_KEYS } from './utils/composite/keys';
|
|
65
|
+
import { inertValue } from './utils/inertValue';
|
|
66
|
+
import { isHTMLElement } from './utils/dom';
|
|
67
|
+
import {
|
|
68
|
+
safePolygon,
|
|
69
|
+
useHoverReferenceInteraction,
|
|
70
|
+
} from './utils/floating/useHoverReferenceInteraction';
|
|
71
|
+
import { useOpenMethodTriggerProps } from './utils/useOpenInteractionType';
|
|
72
|
+
import {
|
|
73
|
+
triggerOpenStateMapping,
|
|
74
|
+
pressableTriggerOpenStateMapping,
|
|
75
|
+
} from './utils/popupStateMapping';
|
|
76
|
+
import type { StateAttributesMapping } from './utils/getStateAttributesProps';
|
|
77
|
+
import {
|
|
78
|
+
createInitialPopupStoreState,
|
|
79
|
+
createPopupFloatingRootContext,
|
|
80
|
+
popupStoreSelectors,
|
|
81
|
+
setPopupOpenState,
|
|
82
|
+
attachPreventUnmountOnClose,
|
|
83
|
+
usePopupStore,
|
|
84
|
+
useInitialOpenSync,
|
|
85
|
+
useImplicitActiveTrigger,
|
|
86
|
+
useOpenStateTransitions,
|
|
87
|
+
usePopupRootSync,
|
|
88
|
+
usePopupInteractionProps,
|
|
89
|
+
useTriggerDataForwarding,
|
|
90
|
+
FOCUSABLE_POPUP_PROPS,
|
|
91
|
+
createDefaultInitialFocus,
|
|
92
|
+
type PopupStoreState,
|
|
93
|
+
type PopupStoreContext,
|
|
94
|
+
} from './utils/popups';
|
|
95
|
+
import { useTriggerFocusGuards } from './utils/popups/useTriggerFocusGuards';
|
|
96
|
+
import { PopupTriggerMap } from './utils/popups/popupTriggerMap';
|
|
97
|
+
import type { InteractionType } from './utils/useEnhancedClickHandler';
|
|
98
|
+
|
|
99
|
+
const OPEN_DELAY = 300;
|
|
100
|
+
const PATIENT_CLICK_THRESHOLD = 500;
|
|
101
|
+
const CLICK_TRIGGER_IDENTIFIER = 'data-base-ui-click-trigger';
|
|
102
|
+
|
|
103
|
+
// --- Store -------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
export type PopoverState<Payload> = PopupStoreState<Payload> & {
|
|
106
|
+
disabled: boolean;
|
|
107
|
+
instantType: 'dismiss' | 'click' | 'focus' | 'trigger-change' | undefined;
|
|
108
|
+
modal: boolean | 'trap-focus';
|
|
109
|
+
focusManagerModal: boolean;
|
|
110
|
+
openMethod: InteractionType | null;
|
|
111
|
+
openChangeReason: string | null;
|
|
112
|
+
stickIfOpen: boolean;
|
|
113
|
+
nested: boolean;
|
|
114
|
+
titleElementId: string | undefined;
|
|
115
|
+
descriptionElementId: string | undefined;
|
|
116
|
+
openOnHover: boolean;
|
|
117
|
+
closeDelay: number;
|
|
118
|
+
hasViewport: boolean;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type PopoverContext = PopupStoreContext<any> & {
|
|
122
|
+
readonly popupRef: { current: HTMLElement | null };
|
|
123
|
+
readonly backdropRef: { current: HTMLDivElement | null };
|
|
124
|
+
readonly internalBackdropRef: { current: HTMLDivElement | null };
|
|
125
|
+
readonly triggerFocusTargetRef: { current: HTMLElement | null };
|
|
126
|
+
readonly beforeContentFocusGuardRef: { current: HTMLElement | null };
|
|
127
|
+
readonly stickIfOpenTimeout: Timeout;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function createInitialPopoverState<Payload>(): PopoverState<Payload> {
|
|
131
|
+
return {
|
|
132
|
+
...createInitialPopupStoreState<Payload>(),
|
|
133
|
+
disabled: false,
|
|
134
|
+
modal: false,
|
|
135
|
+
focusManagerModal: false,
|
|
136
|
+
instantType: undefined,
|
|
137
|
+
openMethod: null,
|
|
138
|
+
openChangeReason: null,
|
|
139
|
+
titleElementId: undefined,
|
|
140
|
+
descriptionElementId: undefined,
|
|
141
|
+
stickIfOpen: true,
|
|
142
|
+
nested: false,
|
|
143
|
+
openOnHover: false,
|
|
144
|
+
closeDelay: 0,
|
|
145
|
+
hasViewport: false,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const popoverSelectors = {
|
|
150
|
+
...popupStoreSelectors,
|
|
151
|
+
disabled: createSelector((state: PopoverState<unknown>) => state.disabled),
|
|
152
|
+
instantType: createSelector((state: PopoverState<unknown>) => state.instantType),
|
|
153
|
+
openMethod: createSelector((state: PopoverState<unknown>) => state.openMethod),
|
|
154
|
+
openChangeReason: createSelector((state: PopoverState<unknown>) => state.openChangeReason),
|
|
155
|
+
modal: createSelector((state: PopoverState<unknown>) => state.modal),
|
|
156
|
+
focusManagerModal: createSelector((state: PopoverState<unknown>) => state.focusManagerModal),
|
|
157
|
+
stickIfOpen: createSelector((state: PopoverState<unknown>) => state.stickIfOpen),
|
|
158
|
+
titleElementId: createSelector((state: PopoverState<unknown>) => state.titleElementId),
|
|
159
|
+
descriptionElementId: createSelector(
|
|
160
|
+
(state: PopoverState<unknown>) => state.descriptionElementId,
|
|
161
|
+
),
|
|
162
|
+
openOnHover: createSelector((state: PopoverState<unknown>) => state.openOnHover),
|
|
163
|
+
closeDelay: createSelector((state: PopoverState<unknown>) => state.closeDelay),
|
|
164
|
+
hasViewport: createSelector((state: PopoverState<unknown>) => state.hasViewport),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export class PopoverStore<Payload> extends ReactStore<
|
|
168
|
+
Readonly<PopoverState<Payload>>,
|
|
169
|
+
PopoverContext,
|
|
170
|
+
typeof popoverSelectors
|
|
171
|
+
> {
|
|
172
|
+
constructor(
|
|
173
|
+
initialState?: Partial<PopoverState<Payload>>,
|
|
174
|
+
floatingId?: string | undefined,
|
|
175
|
+
nested = false,
|
|
176
|
+
) {
|
|
177
|
+
const initial = { ...createInitialPopoverState<Payload>(), ...initialState };
|
|
178
|
+
const triggerElements = new PopupTriggerMap();
|
|
179
|
+
if (initial.open && initialState?.mounted === undefined) {
|
|
180
|
+
initial.mounted = true;
|
|
181
|
+
}
|
|
182
|
+
initial.floatingRootContext = createPopupFloatingRootContext(
|
|
183
|
+
triggerElements,
|
|
184
|
+
floatingId,
|
|
185
|
+
nested,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
super(
|
|
189
|
+
initial,
|
|
190
|
+
{
|
|
191
|
+
popupRef: { current: null },
|
|
192
|
+
backdropRef: { current: null },
|
|
193
|
+
internalBackdropRef: { current: null },
|
|
194
|
+
onOpenChange: undefined,
|
|
195
|
+
onOpenChangeComplete: undefined,
|
|
196
|
+
triggerFocusTargetRef: { current: null },
|
|
197
|
+
beforeContentFocusGuardRef: { current: null },
|
|
198
|
+
stickIfOpenTimeout: new Timeout(),
|
|
199
|
+
triggerElements,
|
|
200
|
+
} as PopoverContext,
|
|
201
|
+
popoverSelectors,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
setOpen = (nextOpen: boolean, eventDetails: any) => {
|
|
206
|
+
const isHover = eventDetails.reason === REASONS.triggerHover;
|
|
207
|
+
const isKeyboardClick =
|
|
208
|
+
eventDetails.reason === REASONS.triggerPress &&
|
|
209
|
+
(eventDetails.event as MouseEvent).detail === 0;
|
|
210
|
+
const isDismissClose =
|
|
211
|
+
!nextOpen && (eventDetails.reason === REASONS.escapeKey || eventDetails.reason == null);
|
|
212
|
+
|
|
213
|
+
const shouldPreventUnmountOnClose = attachPreventUnmountOnClose(eventDetails);
|
|
214
|
+
|
|
215
|
+
const activeTriggerId = this.select('activeTriggerId');
|
|
216
|
+
if (
|
|
217
|
+
!nextOpen &&
|
|
218
|
+
eventDetails.reason === REASONS.closePress &&
|
|
219
|
+
eventDetails.trigger == null &&
|
|
220
|
+
activeTriggerId != null
|
|
221
|
+
) {
|
|
222
|
+
eventDetails.trigger =
|
|
223
|
+
this.context.triggerElements.getById(activeTriggerId) ??
|
|
224
|
+
this.select('activeTriggerElement') ??
|
|
225
|
+
undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.context.onOpenChange?.(nextOpen, eventDetails);
|
|
229
|
+
if (eventDetails.isCanceled) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
(this.state.floatingRootContext as any).dispatchOpenChange(nextOpen, eventDetails);
|
|
234
|
+
|
|
235
|
+
const changeState = () => {
|
|
236
|
+
const updatedState: Partial<PopoverState<Payload>> = {
|
|
237
|
+
open: nextOpen,
|
|
238
|
+
openChangeReason: eventDetails.reason,
|
|
239
|
+
};
|
|
240
|
+
setPopupOpenState(
|
|
241
|
+
updatedState as any,
|
|
242
|
+
nextOpen,
|
|
243
|
+
eventDetails.trigger,
|
|
244
|
+
shouldPreventUnmountOnClose(),
|
|
245
|
+
);
|
|
246
|
+
this.update(updatedState);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (isHover) {
|
|
250
|
+
this.set('stickIfOpen', true);
|
|
251
|
+
this.context.stickIfOpenTimeout.start(PATIENT_CLICK_THRESHOLD, () => {
|
|
252
|
+
this.set('stickIfOpen', false);
|
|
253
|
+
});
|
|
254
|
+
flushSync(changeState);
|
|
255
|
+
} else {
|
|
256
|
+
changeState();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (isKeyboardClick || isDismissClose) {
|
|
260
|
+
this.set('instantType', isKeyboardClick ? 'click' : 'dismiss');
|
|
261
|
+
} else if (eventDetails.reason === REASONS.focusOut) {
|
|
262
|
+
this.set('instantType', 'focus');
|
|
263
|
+
} else {
|
|
264
|
+
this.set('instantType', undefined);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
disposeEffect = () => {
|
|
269
|
+
return this.context.stickIfOpenTimeout.disposeEffect();
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
static useStore<Payload>(
|
|
273
|
+
externalStore: PopoverStore<Payload> | undefined,
|
|
274
|
+
initialState: Partial<PopoverState<Payload>>,
|
|
275
|
+
slot: symbol | undefined,
|
|
276
|
+
): PopoverStore<Payload> {
|
|
277
|
+
const { store, internalStore } = usePopupStore(
|
|
278
|
+
externalStore as any,
|
|
279
|
+
(floatingId, nested) => new PopoverStore<Payload>(initialState, floatingId, nested) as any,
|
|
280
|
+
false,
|
|
281
|
+
subSlot(slot, 'pop'),
|
|
282
|
+
);
|
|
283
|
+
useEffect(
|
|
284
|
+
() => (internalStore as any)?.disposeEffect(),
|
|
285
|
+
[internalStore],
|
|
286
|
+
subSlot(slot, 'dispose'),
|
|
287
|
+
);
|
|
288
|
+
return store as unknown as PopoverStore<Payload>;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// --- Handle ------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
export class PopoverHandle<Payload> {
|
|
295
|
+
readonly store: PopoverStore<Payload>;
|
|
296
|
+
|
|
297
|
+
constructor() {
|
|
298
|
+
this.store = new PopoverStore<Payload>();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
open(triggerId: string) {
|
|
302
|
+
const triggerElement = triggerId
|
|
303
|
+
? (this.store.context.triggerElements.getById(triggerId) ?? undefined)
|
|
304
|
+
: undefined;
|
|
305
|
+
if (triggerId && !triggerElement) {
|
|
306
|
+
throw new Error(`Base UI: PopoverHandle.open: No trigger found with id "${triggerId}".`);
|
|
307
|
+
}
|
|
308
|
+
this.store.setOpen(
|
|
309
|
+
true,
|
|
310
|
+
createChangeEventDetails(
|
|
311
|
+
REASONS.imperativeAction,
|
|
312
|
+
undefined,
|
|
313
|
+
triggerElement as HTMLElement | undefined,
|
|
314
|
+
),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
close() {
|
|
319
|
+
this.store.setOpen(
|
|
320
|
+
false,
|
|
321
|
+
createChangeEventDetails(REASONS.imperativeAction, undefined, undefined),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
get isOpen() {
|
|
326
|
+
return this.store.select('open');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function createPopoverHandle<Payload>(): PopoverHandle<Payload> {
|
|
331
|
+
return new PopoverHandle<Payload>();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// --- Contexts ----------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
export interface PopoverRootContextValue<Payload = unknown> {
|
|
337
|
+
store: PopoverStore<Payload>;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const PopoverRootContext = createContext<PopoverRootContextValue | undefined>(undefined);
|
|
341
|
+
|
|
342
|
+
export function usePopoverRootContext(optional?: boolean): PopoverRootContextValue | undefined {
|
|
343
|
+
const context = useContext(PopoverRootContext);
|
|
344
|
+
if (context === undefined && !optional) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
'Base UI: PopoverRootContext is missing. Popover parts must be placed within <Popover.Root>.',
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return context;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const PopoverPortalContext = createContext<boolean | undefined>(undefined);
|
|
353
|
+
|
|
354
|
+
export function usePopoverPortalContext(): boolean {
|
|
355
|
+
const value = useContext(PopoverPortalContext);
|
|
356
|
+
if (value === undefined) {
|
|
357
|
+
throw new Error('Base UI: <Popover.Portal> is missing.');
|
|
358
|
+
}
|
|
359
|
+
return value;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export interface PopoverPositionerContextValue {
|
|
363
|
+
side: Side;
|
|
364
|
+
align: Align;
|
|
365
|
+
arrowRef: { current: Element | null };
|
|
366
|
+
arrowUncentered: boolean;
|
|
367
|
+
arrowStyles: Record<string, any>;
|
|
368
|
+
context: any;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const PopoverPositionerContext = createContext<PopoverPositionerContextValue | undefined>(
|
|
372
|
+
undefined,
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
export function usePopoverPositionerContext(): PopoverPositionerContextValue {
|
|
376
|
+
const context = useContext(PopoverPositionerContext);
|
|
377
|
+
if (!context) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
'Base UI: PopoverPositionerContext is missing. PopoverPositioner parts must be placed within <Popover.Positioner>.',
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return context;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// --- Interactions (+ passthrough for the stable-descriptor Provider children) ---
|
|
386
|
+
|
|
387
|
+
function PopoverInteractions(props: any): any {
|
|
388
|
+
const slot = S('PopoverInteractions');
|
|
389
|
+
const { store, modal } = props;
|
|
390
|
+
|
|
391
|
+
const floatingRootContext = store.useState('floatingRootContext', subSlot(slot, 'frc'));
|
|
392
|
+
|
|
393
|
+
const dismiss = useDismiss(
|
|
394
|
+
floatingRootContext,
|
|
395
|
+
{
|
|
396
|
+
outsidePressEvent: {
|
|
397
|
+
mouse: modal === 'trap-focus' ? 'sloppy' : 'intentional',
|
|
398
|
+
touch: 'sloppy',
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
subSlot(slot, 'dismiss'),
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const activeTriggerProps = dismiss.reference ?? EMPTY_OBJECT;
|
|
405
|
+
const inactiveTriggerProps = dismiss.trigger ?? EMPTY_OBJECT;
|
|
406
|
+
const popupProps = useMemo(
|
|
407
|
+
() => mergeProps(FOCUSABLE_POPUP_PROPS, dismiss.floating),
|
|
408
|
+
[dismiss.floating],
|
|
409
|
+
subSlot(slot, 'pp'),
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
usePopupInteractionProps(
|
|
413
|
+
store,
|
|
414
|
+
{ activeTriggerProps, inactiveTriggerProps, popupProps } as any,
|
|
415
|
+
subSlot(slot, 'pip'),
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
return props.children ?? null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function PopoverChildren(props: any): any {
|
|
422
|
+
return props.children ?? null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// --- Root --------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
function PopoverRootComponent<Payload>(props: any): any {
|
|
428
|
+
const slot = S('PopoverRoot');
|
|
429
|
+
const {
|
|
430
|
+
children,
|
|
431
|
+
open: openProp,
|
|
432
|
+
defaultOpen = false,
|
|
433
|
+
onOpenChange,
|
|
434
|
+
onOpenChangeComplete,
|
|
435
|
+
modal = false,
|
|
436
|
+
handle,
|
|
437
|
+
actionsRef,
|
|
438
|
+
triggerId: triggerIdProp,
|
|
439
|
+
defaultTriggerId: defaultTriggerIdProp = null,
|
|
440
|
+
} = props;
|
|
441
|
+
|
|
442
|
+
const store = PopoverStore.useStore<Payload>(
|
|
443
|
+
handle?.store,
|
|
444
|
+
{
|
|
445
|
+
modal,
|
|
446
|
+
open: defaultOpen,
|
|
447
|
+
openProp,
|
|
448
|
+
activeTriggerId: defaultTriggerIdProp,
|
|
449
|
+
triggerIdProp,
|
|
450
|
+
} as Partial<PopoverState<Payload>>,
|
|
451
|
+
subSlot(slot, 'store'),
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
useInitialOpenSync(
|
|
455
|
+
store as any,
|
|
456
|
+
openProp,
|
|
457
|
+
defaultOpen,
|
|
458
|
+
defaultTriggerIdProp,
|
|
459
|
+
subSlot(slot, 'ios'),
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
store.useControlledProp('openProp', openProp, subSlot(slot, 'cp-open'));
|
|
463
|
+
store.useControlledProp('triggerIdProp', triggerIdProp, subSlot(slot, 'cp-tid'));
|
|
464
|
+
|
|
465
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
466
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
467
|
+
const payload = store.useState('payload', subSlot(slot, 'payload')) as Payload | undefined;
|
|
468
|
+
const nested = useFloatingParentNodeId() != null;
|
|
469
|
+
|
|
470
|
+
store.useContextCallback('onOpenChange', onOpenChange, subSlot(slot, 'cc-change'));
|
|
471
|
+
store.useContextCallback(
|
|
472
|
+
'onOpenChangeComplete',
|
|
473
|
+
onOpenChangeComplete,
|
|
474
|
+
subSlot(slot, 'cc-complete'),
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
usePopupRootSync(store as any, open, subSlot(slot, 'rootSync'));
|
|
478
|
+
useImplicitActiveTrigger(store as any, {}, subSlot(slot, 'iat'));
|
|
479
|
+
const { forceUnmount } = useOpenStateTransitions(
|
|
480
|
+
open,
|
|
481
|
+
store as any,
|
|
482
|
+
() => {
|
|
483
|
+
store.update({ stickIfOpen: true, openChangeReason: null });
|
|
484
|
+
},
|
|
485
|
+
subSlot(slot, 'ost'),
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
store.useSyncedValues({ modal, nested } as any, subSlot(slot, 'sv'));
|
|
489
|
+
|
|
490
|
+
useEffect(
|
|
491
|
+
() => {
|
|
492
|
+
if (!open) {
|
|
493
|
+
store.context.stickIfOpenTimeout.clear();
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
[store, open],
|
|
497
|
+
subSlot(slot, 'e:stick'),
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
const handleImperativeClose = useCallback(
|
|
501
|
+
() => {
|
|
502
|
+
store.setOpen(false, createChangeEventDetails(REASONS.imperativeAction));
|
|
503
|
+
},
|
|
504
|
+
[store],
|
|
505
|
+
subSlot(slot, 'close'),
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
useImperativeHandle(
|
|
509
|
+
actionsRef,
|
|
510
|
+
() => ({ unmount: forceUnmount, close: handleImperativeClose }),
|
|
511
|
+
[forceUnmount, handleImperativeClose],
|
|
512
|
+
subSlot(slot, 'imp'),
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
const shouldRenderInteractions = open || mounted;
|
|
516
|
+
|
|
517
|
+
const contextValue = useMemo(() => ({ store }), [store], subSlot(slot, 'ctx'));
|
|
518
|
+
|
|
519
|
+
const resolvedChildren =
|
|
520
|
+
typeof children === 'function' && !isChildrenBlock(children) ? children({ payload }) : children;
|
|
521
|
+
|
|
522
|
+
// Stable descriptor shape (see the Dialog note / octane Provider children shape-flip bug).
|
|
523
|
+
const content = createElement(shouldRenderInteractions ? PopoverInteractions : PopoverChildren, {
|
|
524
|
+
store,
|
|
525
|
+
modal,
|
|
526
|
+
children: resolvedChildren,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
return createElement(PopoverRootContext.Provider, {
|
|
530
|
+
value: contextValue as PopoverRootContextValue,
|
|
531
|
+
children: content,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function PopoverRoot<Payload = unknown>(props: any): any {
|
|
536
|
+
const slot = S('PopoverRootWrapper');
|
|
537
|
+
// Top-level popovers establish a FloatingTree; nested ones reuse the parent's.
|
|
538
|
+
if (usePopoverRootContext(true)) {
|
|
539
|
+
return createElement(PopoverRootComponent, props);
|
|
540
|
+
}
|
|
541
|
+
return createElement(FloatingTree, {
|
|
542
|
+
children: createElement(PopoverRootComponent, props),
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// --- Trigger -----------------------------------------------------------------
|
|
547
|
+
|
|
548
|
+
function PopoverTrigger(componentProps: any): any {
|
|
549
|
+
const slot = S('PopoverTrigger');
|
|
550
|
+
const {
|
|
551
|
+
render,
|
|
552
|
+
className,
|
|
553
|
+
style,
|
|
554
|
+
disabled = false,
|
|
555
|
+
nativeButton = true,
|
|
556
|
+
handle,
|
|
557
|
+
payload,
|
|
558
|
+
openOnHover = false,
|
|
559
|
+
delay = OPEN_DELAY,
|
|
560
|
+
closeDelay = 0,
|
|
561
|
+
id: idProp,
|
|
562
|
+
ref,
|
|
563
|
+
...elementProps
|
|
564
|
+
} = componentProps;
|
|
565
|
+
|
|
566
|
+
const rootContext = usePopoverRootContext(true);
|
|
567
|
+
const store = (handle?.store ?? rootContext?.store) as PopoverStore<any> | undefined;
|
|
568
|
+
if (!store) {
|
|
569
|
+
throw new Error(
|
|
570
|
+
'Base UI: <Popover.Trigger> must be either used within a <Popover.Root> component or provided with a handle.',
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const thisTriggerId = useBaseUiId(idProp, subSlot(slot, 'id'));
|
|
575
|
+
const isTriggerActive = store.useState('isTriggerActive', subSlot(slot, 'ita'), thisTriggerId);
|
|
576
|
+
const floatingContext = store.useState('floatingRootContext', subSlot(slot, 'fc'));
|
|
577
|
+
const isOpenedByThisTrigger = store.useState(
|
|
578
|
+
'isOpenedByTrigger',
|
|
579
|
+
subSlot(slot, 'obt'),
|
|
580
|
+
thisTriggerId,
|
|
581
|
+
);
|
|
582
|
+
const popupId = store.useState('triggerPopupId', subSlot(slot, 'tpid'), thisTriggerId);
|
|
583
|
+
|
|
584
|
+
const triggerElementRef = useRef<HTMLElement | null>(null, subSlot(slot, 'ter'));
|
|
585
|
+
|
|
586
|
+
const { registerTrigger, isMountedByThisTrigger } = useTriggerDataForwarding(
|
|
587
|
+
thisTriggerId,
|
|
588
|
+
triggerElementRef,
|
|
589
|
+
store as any,
|
|
590
|
+
{ payload, disabled, openOnHover, closeDelay } as any,
|
|
591
|
+
subSlot(slot, 'tdf'),
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
const openReason = store.useState('openChangeReason', subSlot(slot, 'ocr'));
|
|
595
|
+
const stickIfOpen = store.useState('stickIfOpen', subSlot(slot, 'sio'));
|
|
596
|
+
const openMethod = store.useState('openMethod', subSlot(slot, 'om'));
|
|
597
|
+
const focusManagerModal = store.useState('focusManagerModal', subSlot(slot, 'fmm'));
|
|
598
|
+
|
|
599
|
+
const hoverProps = useHoverReferenceInteraction(
|
|
600
|
+
floatingContext,
|
|
601
|
+
{
|
|
602
|
+
enabled:
|
|
603
|
+
!disabled &&
|
|
604
|
+
floatingContext != null &&
|
|
605
|
+
openOnHover &&
|
|
606
|
+
(openMethod !== 'touch' || openReason !== REASONS.triggerPress),
|
|
607
|
+
mouseOnly: true,
|
|
608
|
+
move: false,
|
|
609
|
+
handleClose: safePolygon(),
|
|
610
|
+
restMs: delay,
|
|
611
|
+
delay: { close: closeDelay },
|
|
612
|
+
triggerElementRef,
|
|
613
|
+
isActiveTrigger: isTriggerActive,
|
|
614
|
+
isClosing: () => store.select('transitionStatus') === 'ending',
|
|
615
|
+
},
|
|
616
|
+
subSlot(slot, 'hover'),
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
const click = useClick(
|
|
620
|
+
floatingContext,
|
|
621
|
+
{ enabled: floatingContext != null, stickIfOpen },
|
|
622
|
+
subSlot(slot, 'click'),
|
|
623
|
+
);
|
|
624
|
+
const interactionTypeProps = useOpenMethodTriggerProps(
|
|
625
|
+
() => store.select('open'),
|
|
626
|
+
(interactionType) => {
|
|
627
|
+
store.set('openMethod', interactionType);
|
|
628
|
+
},
|
|
629
|
+
subSlot(slot, 'omt'),
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
const rootTriggerProps = store.useState(
|
|
633
|
+
'triggerProps',
|
|
634
|
+
subSlot(slot, 'tp'),
|
|
635
|
+
isMountedByThisTrigger,
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
const { getButtonProps, buttonRef } = useButton(
|
|
639
|
+
{ disabled, native: nativeButton },
|
|
640
|
+
subSlot(slot, 'btn'),
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
const stateAttributesMapping: StateAttributesMapping<{ open: boolean }> = {
|
|
644
|
+
open(value: boolean) {
|
|
645
|
+
if (value && openReason === REASONS.triggerPress) {
|
|
646
|
+
return pressableTriggerOpenStateMapping.open!(value);
|
|
647
|
+
}
|
|
648
|
+
return triggerOpenStateMapping.open!(value);
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const { preFocusGuardRef, handlePreFocusGuardFocus, handleFocusTargetFocus } =
|
|
653
|
+
useTriggerFocusGuards(store as any, triggerElementRef, subSlot(slot, 'tfg'));
|
|
654
|
+
|
|
655
|
+
const state = { disabled, open: isOpenedByThisTrigger };
|
|
656
|
+
|
|
657
|
+
const element = useRenderElement(
|
|
658
|
+
'button',
|
|
659
|
+
{ render, className, style },
|
|
660
|
+
{
|
|
661
|
+
state,
|
|
662
|
+
ref: [buttonRef, ref, registerTrigger, triggerElementRef],
|
|
663
|
+
props: [
|
|
664
|
+
click.reference,
|
|
665
|
+
hoverProps,
|
|
666
|
+
rootTriggerProps,
|
|
667
|
+
interactionTypeProps,
|
|
668
|
+
{
|
|
669
|
+
[CLICK_TRIGGER_IDENTIFIER]: '',
|
|
670
|
+
id: thisTriggerId,
|
|
671
|
+
'aria-haspopup': 'dialog',
|
|
672
|
+
'aria-expanded': isOpenedByThisTrigger,
|
|
673
|
+
'aria-controls': popupId,
|
|
674
|
+
},
|
|
675
|
+
elementProps,
|
|
676
|
+
getButtonProps,
|
|
677
|
+
],
|
|
678
|
+
stateAttributesMapping,
|
|
679
|
+
},
|
|
680
|
+
subSlot(slot, 're'),
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
if (isMountedByThisTrigger && !focusManagerModal) {
|
|
684
|
+
return [
|
|
685
|
+
createElement(FocusGuard, { ref: preFocusGuardRef, onFocus: handlePreFocusGuardFocus }),
|
|
686
|
+
element,
|
|
687
|
+
createElement(FocusGuard, {
|
|
688
|
+
ref: store.context.triggerFocusTargetRef,
|
|
689
|
+
onFocus: handleFocusTargetFocus,
|
|
690
|
+
}),
|
|
691
|
+
];
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return element;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// --- Portal ------------------------------------------------------------------
|
|
698
|
+
|
|
699
|
+
function PopoverPortal(props: any): any {
|
|
700
|
+
const slot = S('PopoverPortal');
|
|
701
|
+
const { keepMounted = false, ref, ...portalProps } = props;
|
|
702
|
+
|
|
703
|
+
const { store } = usePopoverRootContext() as PopoverRootContextValue;
|
|
704
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
705
|
+
|
|
706
|
+
const shouldRender = mounted || keepMounted;
|
|
707
|
+
if (!shouldRender) {
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return createElement(PopoverPortalContext.Provider, {
|
|
712
|
+
value: keepMounted,
|
|
713
|
+
children: createElement(FloatingPortal, { ref, ...portalProps }),
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// --- Positioner --------------------------------------------------------------
|
|
718
|
+
|
|
719
|
+
function PopoverPositioner(componentProps: any): any {
|
|
720
|
+
const slot = S('PopoverPositioner');
|
|
721
|
+
const {
|
|
722
|
+
render,
|
|
723
|
+
className,
|
|
724
|
+
style,
|
|
725
|
+
anchor,
|
|
726
|
+
positionMethod = 'absolute',
|
|
727
|
+
side = 'bottom',
|
|
728
|
+
align = 'center',
|
|
729
|
+
sideOffset = 0,
|
|
730
|
+
alignOffset = 0,
|
|
731
|
+
collisionBoundary = 'clipping-ancestors',
|
|
732
|
+
collisionPadding = 5,
|
|
733
|
+
arrowPadding = 5,
|
|
734
|
+
sticky = false,
|
|
735
|
+
disableAnchorTracking = false,
|
|
736
|
+
collisionAvoidance = POPUP_COLLISION_AVOIDANCE,
|
|
737
|
+
ref,
|
|
738
|
+
...elementProps
|
|
739
|
+
} = componentProps;
|
|
740
|
+
|
|
741
|
+
const { store } = usePopoverRootContext() as PopoverRootContextValue;
|
|
742
|
+
const keepMounted = usePopoverPortalContext();
|
|
743
|
+
const nodeId = useFloatingNodeId(undefined, subSlot(slot, 'nodeId'));
|
|
744
|
+
|
|
745
|
+
const floatingRootContext = store.useState('floatingRootContext', subSlot(slot, 'frc'));
|
|
746
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
747
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
748
|
+
const openReason = store.useState('openChangeReason', subSlot(slot, 'reason'));
|
|
749
|
+
const triggerElement = store.useState('activeTriggerElement', subSlot(slot, 'trigger'));
|
|
750
|
+
const modal = store.useState('modal', subSlot(slot, 'modal'));
|
|
751
|
+
const openMethod = store.useState('openMethod', subSlot(slot, 'method'));
|
|
752
|
+
const positionerElement = store.useState('positionerElement', subSlot(slot, 'posEl'));
|
|
753
|
+
const instantType = store.useState('instantType', subSlot(slot, 'instant'));
|
|
754
|
+
const transitionStatus = store.useState('transitionStatus', subSlot(slot, 'trans'));
|
|
755
|
+
const hasViewport = store.useState('hasViewport', subSlot(slot, 'viewport'));
|
|
756
|
+
|
|
757
|
+
const prevTriggerElementRef = useRef<Element | null>(null, subSlot(slot, 'prevTrigger'));
|
|
758
|
+
|
|
759
|
+
const runOnceAnimationsFinish = useAnimationsFinished(
|
|
760
|
+
positionerElement,
|
|
761
|
+
false,
|
|
762
|
+
false,
|
|
763
|
+
subSlot(slot, 'anim'),
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
const positioning = useAnchorPositioning(
|
|
767
|
+
{
|
|
768
|
+
anchor,
|
|
769
|
+
floatingRootContext,
|
|
770
|
+
positionMethod,
|
|
771
|
+
mounted,
|
|
772
|
+
side,
|
|
773
|
+
sideOffset,
|
|
774
|
+
align,
|
|
775
|
+
alignOffset,
|
|
776
|
+
arrowPadding,
|
|
777
|
+
collisionBoundary,
|
|
778
|
+
collisionPadding,
|
|
779
|
+
sticky,
|
|
780
|
+
disableAnchorTracking,
|
|
781
|
+
keepMounted,
|
|
782
|
+
nodeId,
|
|
783
|
+
collisionAvoidance,
|
|
784
|
+
adaptiveOrigin: hasViewport ? adaptiveOrigin : undefined,
|
|
785
|
+
},
|
|
786
|
+
subSlot(slot, 'positioning'),
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
const domReference = floatingRootContext.useState('domReferenceElement', subSlot(slot, 'domref'));
|
|
790
|
+
|
|
791
|
+
// When the current trigger element changes, enable transitions on the positioner temporarily.
|
|
792
|
+
useLayoutEffect(
|
|
793
|
+
() => {
|
|
794
|
+
const currentTriggerElement = domReference;
|
|
795
|
+
const prevTriggerElement = prevTriggerElementRef.current;
|
|
796
|
+
|
|
797
|
+
if (currentTriggerElement) {
|
|
798
|
+
prevTriggerElementRef.current = currentTriggerElement;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (
|
|
802
|
+
prevTriggerElement &&
|
|
803
|
+
currentTriggerElement &&
|
|
804
|
+
currentTriggerElement !== prevTriggerElement
|
|
805
|
+
) {
|
|
806
|
+
store.set('instantType', undefined);
|
|
807
|
+
const ac = new AbortController();
|
|
808
|
+
runOnceAnimationsFinish(() => {
|
|
809
|
+
store.set('instantType', 'trigger-change');
|
|
810
|
+
}, ac.signal);
|
|
811
|
+
|
|
812
|
+
return () => {
|
|
813
|
+
ac.abort();
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return undefined;
|
|
818
|
+
},
|
|
819
|
+
[domReference, runOnceAnimationsFinish, store],
|
|
820
|
+
subSlot(slot, 'e:trigger'),
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
useAnchoredPopupScrollLock(
|
|
824
|
+
open && modal === true && openReason !== REASONS.triggerHover,
|
|
825
|
+
openMethod === 'touch',
|
|
826
|
+
positionerElement,
|
|
827
|
+
triggerElement,
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
const setPositionerElement = useCallback(
|
|
831
|
+
(element: HTMLElement | null) => {
|
|
832
|
+
store.set('positionerElement', element);
|
|
833
|
+
},
|
|
834
|
+
[store],
|
|
835
|
+
subSlot(slot, 'setPosEl'),
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
const state: PopoverPositionerState = {
|
|
839
|
+
open,
|
|
840
|
+
side: positioning.side,
|
|
841
|
+
align: positioning.align,
|
|
842
|
+
anchorHidden: positioning.anchorHidden,
|
|
843
|
+
instant: instantType,
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const element = usePositioner(
|
|
847
|
+
componentProps,
|
|
848
|
+
state,
|
|
849
|
+
{
|
|
850
|
+
styles: positioning.positionerStyles,
|
|
851
|
+
transitionStatus,
|
|
852
|
+
props: elementProps,
|
|
853
|
+
refs: [ref, setPositionerElement],
|
|
854
|
+
hidden: !mounted,
|
|
855
|
+
inert: !open,
|
|
856
|
+
},
|
|
857
|
+
subSlot(slot, 'positioner'),
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
return createElement(PopoverPositionerContext.Provider, {
|
|
861
|
+
value: positioning as unknown as PopoverPositionerContextValue,
|
|
862
|
+
children: [
|
|
863
|
+
mounted && modal === true && openReason !== REASONS.triggerHover
|
|
864
|
+
? createElement(InternalBackdrop, {
|
|
865
|
+
ref: store.context.internalBackdropRef,
|
|
866
|
+
inert: inertValue(!open),
|
|
867
|
+
cutout: triggerElement,
|
|
868
|
+
})
|
|
869
|
+
: null,
|
|
870
|
+
createElement(FloatingNode, { id: nodeId, children: element }),
|
|
871
|
+
],
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
interface PopoverPositionerState {
|
|
876
|
+
open: boolean;
|
|
877
|
+
side: Side;
|
|
878
|
+
align: Align;
|
|
879
|
+
anchorHidden: boolean;
|
|
880
|
+
instant: string | undefined;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// --- Popup -------------------------------------------------------------------
|
|
884
|
+
|
|
885
|
+
const popoverPopupStateAttributesMapping = {
|
|
886
|
+
...popupStateMapping,
|
|
887
|
+
...transitionStatusMapping,
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
function PopoverPopup(componentProps: any): any {
|
|
891
|
+
const slot = S('PopoverPopup');
|
|
892
|
+
const { render, className, style, initialFocus, finalFocus, ref, ...elementProps } =
|
|
893
|
+
componentProps;
|
|
894
|
+
|
|
895
|
+
const { store } = usePopoverRootContext() as PopoverRootContextValue;
|
|
896
|
+
|
|
897
|
+
const positioner = usePopoverPositionerContext();
|
|
898
|
+
// No Toolbar context in this phase; composite key-stop is toolbar-only.
|
|
899
|
+
const insideToolbar = false;
|
|
900
|
+
const { context: closePartContext, hasClosePart } = useClosePartCount();
|
|
901
|
+
|
|
902
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
903
|
+
const openMethod = store.useState('openMethod', subSlot(slot, 'method'));
|
|
904
|
+
const instantType = store.useState('instantType', subSlot(slot, 'instant'));
|
|
905
|
+
const transitionStatus = store.useState('transitionStatus', subSlot(slot, 'trans'));
|
|
906
|
+
const popupProps = store.useState('popupProps', subSlot(slot, 'popupProps'));
|
|
907
|
+
const titleId = store.useState('titleElementId', subSlot(slot, 'titleId'));
|
|
908
|
+
const descriptionId = store.useState('descriptionElementId', subSlot(slot, 'descId'));
|
|
909
|
+
const modal = store.useState('modal', subSlot(slot, 'modal'));
|
|
910
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
911
|
+
const openReason = store.useState('openChangeReason', subSlot(slot, 'reason'));
|
|
912
|
+
const activeTriggerElement = store.useState('activeTriggerElement', subSlot(slot, 'trigger'));
|
|
913
|
+
const floatingContext = store.useState('floatingRootContext', subSlot(slot, 'frc'));
|
|
914
|
+
const floatingId = floatingContext.useState('floatingId', subSlot(slot, 'fid'));
|
|
915
|
+
const disabled = store.useState('disabled', subSlot(slot, 'disabled'));
|
|
916
|
+
const openOnHover = store.useState('openOnHover', subSlot(slot, 'hover'));
|
|
917
|
+
const closeDelay = store.useState('closeDelay', subSlot(slot, 'closeDelay'));
|
|
918
|
+
|
|
919
|
+
const popupId = elementProps.id ?? floatingId;
|
|
920
|
+
|
|
921
|
+
useOpenChangeComplete(
|
|
922
|
+
{
|
|
923
|
+
open,
|
|
924
|
+
ref: store.context.popupRef,
|
|
925
|
+
onComplete() {
|
|
926
|
+
if (open) {
|
|
927
|
+
store.context.onOpenChangeComplete?.(true);
|
|
928
|
+
}
|
|
929
|
+
},
|
|
930
|
+
},
|
|
931
|
+
subSlot(slot, 'occ'),
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
useHoverFloatingInteraction(
|
|
935
|
+
floatingContext,
|
|
936
|
+
{ enabled: openOnHover && !disabled, closeDelay },
|
|
937
|
+
subSlot(slot, 'hoverInteraction'),
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
const resolvedInitialFocus =
|
|
941
|
+
initialFocus === undefined ? createDefaultInitialFocus(store.context.popupRef) : initialFocus;
|
|
942
|
+
|
|
943
|
+
const focusManagerModal = modal !== false && hasClosePart;
|
|
944
|
+
store.useSyncedValue('focusManagerModal', focusManagerModal, subSlot(slot, 'fmm'));
|
|
945
|
+
|
|
946
|
+
const setPopupElement = useCallback(
|
|
947
|
+
(element: HTMLElement | null) => {
|
|
948
|
+
store.set('popupElement', element);
|
|
949
|
+
},
|
|
950
|
+
[store],
|
|
951
|
+
subSlot(slot, 'setPopupEl'),
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
const state: PopoverPopupState = {
|
|
955
|
+
open,
|
|
956
|
+
side: positioner.side,
|
|
957
|
+
align: positioner.align,
|
|
958
|
+
instant: instantType,
|
|
959
|
+
transitionStatus,
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
const element = useRenderElement(
|
|
963
|
+
'div',
|
|
964
|
+
componentProps,
|
|
965
|
+
{
|
|
966
|
+
state,
|
|
967
|
+
ref: [ref, store.context.popupRef, setPopupElement],
|
|
968
|
+
props: [
|
|
969
|
+
popupProps,
|
|
970
|
+
{
|
|
971
|
+
id: popupId,
|
|
972
|
+
role: 'dialog',
|
|
973
|
+
...FOCUSABLE_POPUP_PROPS,
|
|
974
|
+
'aria-labelledby': titleId,
|
|
975
|
+
'aria-describedby': descriptionId,
|
|
976
|
+
onKeyDown(event: any) {
|
|
977
|
+
if (insideToolbar && COMPOSITE_KEYS.has(event.key)) {
|
|
978
|
+
event.stopPropagation();
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
getDisabledMountTransitionStyles(transitionStatus),
|
|
983
|
+
elementProps,
|
|
984
|
+
],
|
|
985
|
+
stateAttributesMapping: popoverPopupStateAttributesMapping,
|
|
986
|
+
},
|
|
987
|
+
subSlot(slot, 're'),
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
return createElement(FloatingFocusManager, {
|
|
991
|
+
context: floatingContext,
|
|
992
|
+
openInteractionType: openMethod,
|
|
993
|
+
modal: focusManagerModal,
|
|
994
|
+
disabled: !mounted || openReason === REASONS.triggerHover,
|
|
995
|
+
initialFocus: resolvedInitialFocus,
|
|
996
|
+
returnFocus: finalFocus,
|
|
997
|
+
restoreFocus: 'popup',
|
|
998
|
+
previousFocusableElement: isHTMLElement(activeTriggerElement)
|
|
999
|
+
? activeTriggerElement
|
|
1000
|
+
: undefined,
|
|
1001
|
+
nextFocusableElement: store.context.triggerFocusTargetRef,
|
|
1002
|
+
beforeContentFocusGuardRef: store.context.beforeContentFocusGuardRef,
|
|
1003
|
+
children: createElement(ClosePartProvider, { value: closePartContext, children: element }),
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
interface PopoverPopupState {
|
|
1008
|
+
open: boolean;
|
|
1009
|
+
side: Side;
|
|
1010
|
+
align: Align;
|
|
1011
|
+
transitionStatus: any;
|
|
1012
|
+
instant: 'dismiss' | 'click' | 'focus' | 'trigger-change' | undefined;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// --- Arrow -------------------------------------------------------------------
|
|
1016
|
+
|
|
1017
|
+
function PopoverArrow(componentProps: any): any {
|
|
1018
|
+
const slot = S('PopoverArrow');
|
|
1019
|
+
const { render, className, style, ref, ...elementProps } = componentProps;
|
|
1020
|
+
|
|
1021
|
+
const { store } = usePopoverRootContext() as PopoverRootContextValue;
|
|
1022
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
1023
|
+
const { arrowRef, side, align, arrowUncentered, arrowStyles } = usePopoverPositionerContext();
|
|
1024
|
+
|
|
1025
|
+
const state: PopoverArrowState = {
|
|
1026
|
+
open,
|
|
1027
|
+
side,
|
|
1028
|
+
align,
|
|
1029
|
+
uncentered: arrowUncentered,
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
return useRenderElement(
|
|
1033
|
+
'div',
|
|
1034
|
+
componentProps,
|
|
1035
|
+
{
|
|
1036
|
+
state,
|
|
1037
|
+
ref: [ref, arrowRef],
|
|
1038
|
+
props: [{ style: arrowStyles, 'aria-hidden': true }, elementProps],
|
|
1039
|
+
stateAttributesMapping: popupStateMapping,
|
|
1040
|
+
},
|
|
1041
|
+
subSlot(slot, 're'),
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
interface PopoverArrowState {
|
|
1046
|
+
open: boolean;
|
|
1047
|
+
side: Side;
|
|
1048
|
+
align: Align;
|
|
1049
|
+
uncentered: boolean;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// --- Backdrop ----------------------------------------------------------------
|
|
1053
|
+
|
|
1054
|
+
const popoverBackdropStateAttributesMapping = {
|
|
1055
|
+
...popupStateMapping,
|
|
1056
|
+
...transitionStatusMapping,
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
function PopoverBackdrop(props: any): any {
|
|
1060
|
+
const slot = S('PopoverBackdrop');
|
|
1061
|
+
const { render, className, style, ref, ...elementProps } = props;
|
|
1062
|
+
|
|
1063
|
+
const { store } = usePopoverRootContext() as PopoverRootContextValue;
|
|
1064
|
+
|
|
1065
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
1066
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
1067
|
+
const transitionStatus = store.useState('transitionStatus', subSlot(slot, 'trans'));
|
|
1068
|
+
const openReason = store.useState('openChangeReason', subSlot(slot, 'reason'));
|
|
1069
|
+
|
|
1070
|
+
const state: PopoverBackdropState = {
|
|
1071
|
+
open,
|
|
1072
|
+
transitionStatus,
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
return useRenderElement(
|
|
1076
|
+
'div',
|
|
1077
|
+
props,
|
|
1078
|
+
{
|
|
1079
|
+
state,
|
|
1080
|
+
ref: [store.context.backdropRef, ref],
|
|
1081
|
+
props: [
|
|
1082
|
+
{
|
|
1083
|
+
role: 'presentation',
|
|
1084
|
+
hidden: !mounted,
|
|
1085
|
+
style: {
|
|
1086
|
+
pointerEvents: openReason === REASONS.triggerHover ? 'none' : undefined,
|
|
1087
|
+
userSelect: 'none',
|
|
1088
|
+
WebkitUserSelect: 'none',
|
|
1089
|
+
},
|
|
1090
|
+
},
|
|
1091
|
+
elementProps,
|
|
1092
|
+
],
|
|
1093
|
+
stateAttributesMapping: popoverBackdropStateAttributesMapping,
|
|
1094
|
+
},
|
|
1095
|
+
subSlot(slot, 're'),
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
interface PopoverBackdropState {
|
|
1100
|
+
open: boolean;
|
|
1101
|
+
transitionStatus: any;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// --- Title -------------------------------------------------------------------
|
|
1105
|
+
|
|
1106
|
+
function PopoverTitle(componentProps: any): any {
|
|
1107
|
+
const slot = S('PopoverTitle');
|
|
1108
|
+
const { render, className, style, ref, ...elementProps } = componentProps;
|
|
1109
|
+
|
|
1110
|
+
const { store } = usePopoverRootContext() as PopoverRootContextValue;
|
|
1111
|
+
|
|
1112
|
+
const id = useBaseUiId(elementProps.id, subSlot(slot, 'id'));
|
|
1113
|
+
|
|
1114
|
+
store.useSyncedValueWithCleanup('titleElementId', id, subSlot(slot, 'sync'));
|
|
1115
|
+
|
|
1116
|
+
return useRenderElement(
|
|
1117
|
+
'h2',
|
|
1118
|
+
componentProps,
|
|
1119
|
+
{
|
|
1120
|
+
ref,
|
|
1121
|
+
props: [{ id }, elementProps],
|
|
1122
|
+
},
|
|
1123
|
+
subSlot(slot, 're'),
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// --- Description -------------------------------------------------------------
|
|
1128
|
+
|
|
1129
|
+
function PopoverDescription(componentProps: any): any {
|
|
1130
|
+
const slot = S('PopoverDescription');
|
|
1131
|
+
const { render, className, style, ref, ...elementProps } = componentProps;
|
|
1132
|
+
|
|
1133
|
+
const { store } = usePopoverRootContext() as PopoverRootContextValue;
|
|
1134
|
+
|
|
1135
|
+
const id = useBaseUiId(elementProps.id, subSlot(slot, 'id'));
|
|
1136
|
+
|
|
1137
|
+
store.useSyncedValueWithCleanup('descriptionElementId', id, subSlot(slot, 'sync'));
|
|
1138
|
+
|
|
1139
|
+
return useRenderElement(
|
|
1140
|
+
'p',
|
|
1141
|
+
componentProps,
|
|
1142
|
+
{
|
|
1143
|
+
ref,
|
|
1144
|
+
props: [{ id }, elementProps],
|
|
1145
|
+
},
|
|
1146
|
+
subSlot(slot, 're'),
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// --- Close -------------------------------------------------------------------
|
|
1151
|
+
|
|
1152
|
+
function PopoverClose(componentProps: any): any {
|
|
1153
|
+
const slot = S('PopoverClose');
|
|
1154
|
+
const {
|
|
1155
|
+
render,
|
|
1156
|
+
className,
|
|
1157
|
+
style,
|
|
1158
|
+
disabled = false,
|
|
1159
|
+
nativeButton = true,
|
|
1160
|
+
ref,
|
|
1161
|
+
...elementProps
|
|
1162
|
+
} = componentProps;
|
|
1163
|
+
|
|
1164
|
+
const { buttonRef, getButtonProps } = useButton(
|
|
1165
|
+
{ disabled, focusableWhenDisabled: false, native: nativeButton },
|
|
1166
|
+
subSlot(slot, 'btn'),
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
const { store } = usePopoverRootContext() as PopoverRootContextValue;
|
|
1170
|
+
useClosePartRegistration();
|
|
1171
|
+
|
|
1172
|
+
return useRenderElement(
|
|
1173
|
+
'button',
|
|
1174
|
+
componentProps,
|
|
1175
|
+
{
|
|
1176
|
+
ref: [ref, buttonRef],
|
|
1177
|
+
props: [
|
|
1178
|
+
{
|
|
1179
|
+
onClick(event: any) {
|
|
1180
|
+
store.setOpen(false, createChangeEventDetails(REASONS.closePress, event));
|
|
1181
|
+
},
|
|
1182
|
+
},
|
|
1183
|
+
elementProps,
|
|
1184
|
+
getButtonProps,
|
|
1185
|
+
],
|
|
1186
|
+
},
|
|
1187
|
+
subSlot(slot, 're'),
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// --- Namespace ---------------------------------------------------------------
|
|
1192
|
+
|
|
1193
|
+
export const Popover = {
|
|
1194
|
+
Root: PopoverRoot,
|
|
1195
|
+
Trigger: PopoverTrigger,
|
|
1196
|
+
Portal: PopoverPortal,
|
|
1197
|
+
Positioner: PopoverPositioner,
|
|
1198
|
+
Popup: PopoverPopup,
|
|
1199
|
+
Arrow: PopoverArrow,
|
|
1200
|
+
Backdrop: PopoverBackdrop,
|
|
1201
|
+
Title: PopoverTitle,
|
|
1202
|
+
Description: PopoverDescription,
|
|
1203
|
+
Close: PopoverClose,
|
|
1204
|
+
createHandle: createPopoverHandle,
|
|
1205
|
+
};
|