@octanejs/base-ui 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/alert-dialog.ts +2 -0
- package/src/button.ts +63 -0
- package/src/context-menu.ts +317 -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 +10 -0
- package/src/menu.ts +3387 -0
- package/src/menubar.ts +160 -0
- package/src/popover.ts +99 -25
- 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/ContextMenuRootContext.ts +44 -0
- package/src/utils/FloatingPortalLite.ts +29 -0
- package/src/utils/MenubarContext.ts +38 -0
- package/src/utils/RequestQueue.ts +132 -0
- package/src/utils/TimeoutManager.ts +32 -0
- package/src/utils/composite/grid-utils.ts +437 -0
- package/src/utils/composite/gridNavigation.ts +127 -0
- package/src/utils/composite/list-utils.ts +56 -28
- package/src/utils/composite/useCompositeRoot.ts +11 -13
- package/src/utils/constants.ts +22 -0
- package/src/utils/createChangeEventDetails.ts +4 -0
- package/src/utils/findRootOwnerId.ts +19 -0
- package/src/utils/floating/FloatingDelayGroup.ts +280 -0
- package/src/utils/floating/element.ts +36 -2
- package/src/utils/floating/gridNavigation.ts +54 -0
- 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/floating/useListNavigation.ts +1018 -0
- package/src/utils/floating/useTypeahead.ts +287 -0
- package/src/utils/getCssDimensions.ts +28 -0
- package/src/utils/getPseudoElementBounds.ts +54 -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/useMixedToggleClickHandler.ts +77 -0
- package/src/utils/useOpenInteractionType.ts +37 -1
- package/src/utils/usePopupAutoResize.ts +250 -0
- package/src/utils/usePopupViewport.ts +375 -0
- package/src/utils/usePreviousValue.ts +25 -0
package/src/tooltip.ts
ADDED
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/tooltip/ (v1.6.0): store (TooltipStore/TooltipHandle),
|
|
2
|
+
// provider, root (+ TooltipInteractions), trigger, portal, positioner, popup, arrow, viewport.
|
|
3
|
+
// Reuses the Store + floating stack built for Dialog/Popover, plus the Phase-3b hover/focus layer
|
|
4
|
+
// (a tooltip is hover-first, so `useHoverReferenceInteraction` + `useFocus` + `useDelayGroup` are
|
|
5
|
+
// its primary interactions) and the Phase-3c viewport.
|
|
6
|
+
//
|
|
7
|
+
// octane adaptations: forwardRef → ref-as-prop; native events (so `event.nativeEvent` collapses to
|
|
8
|
+
// `event`); every Store hook-method + hook threads an explicit slot; `React.createContext` →
|
|
9
|
+
// octane `createContext`.
|
|
10
|
+
import { createContext, createElement, Fragment, useContext, useMemo, useRef } from 'octane';
|
|
11
|
+
|
|
12
|
+
import { S, subSlot } from './internal';
|
|
13
|
+
import { useRenderElement } from './utils/useRenderElement';
|
|
14
|
+
import { mergeProps } from './utils/mergeProps';
|
|
15
|
+
import { useBaseUiId } from './utils/useBaseUiId';
|
|
16
|
+
import { createChangeEventDetails, REASONS } from './utils/createChangeEventDetails';
|
|
17
|
+
import { ReactStore } from './utils/store/ReactStore';
|
|
18
|
+
import { createSelector } from './utils/store/createSelector';
|
|
19
|
+
import { useTimeout } from './utils/useTimeout';
|
|
20
|
+
import { useValueAsRef } from './utils/useValueAsRef';
|
|
21
|
+
import { useLayoutEffect, useCallback, useImperativeHandle, isChildrenBlock } from 'octane';
|
|
22
|
+
import { useDismiss } from './utils/floating/useDismiss';
|
|
23
|
+
import { useClientPoint } from './utils/floating/useClientPoint';
|
|
24
|
+
import { useFocus } from './utils/floating/useFocus';
|
|
25
|
+
import { FloatingDelayGroup, useDelayGroup } from './utils/floating/FloatingDelayGroup';
|
|
26
|
+
import {
|
|
27
|
+
safePolygon,
|
|
28
|
+
useHoverReferenceInteraction,
|
|
29
|
+
} from './utils/floating/useHoverReferenceInteraction';
|
|
30
|
+
import { useHoverFloatingInteraction } from './utils/floating/useHoverFloatingInteraction';
|
|
31
|
+
import { useHoverInteractionSharedState } from './utils/floating/useHoverInteractionSharedState';
|
|
32
|
+
import { contains } from './utils/floating/element';
|
|
33
|
+
import { isMouseLikePointerType } from './utils/floating/event';
|
|
34
|
+
import { isElement } from './utils/dom';
|
|
35
|
+
import { FloatingPortalLite } from './utils/FloatingPortalLite';
|
|
36
|
+
import {
|
|
37
|
+
useAnchorPositioning,
|
|
38
|
+
type Side,
|
|
39
|
+
type Align,
|
|
40
|
+
type UseAnchorPositioningSharedParameters,
|
|
41
|
+
} from './utils/useAnchorPositioning';
|
|
42
|
+
import { usePositioner } from './utils/usePositioner';
|
|
43
|
+
import { usePopupViewport } from './utils/usePopupViewport';
|
|
44
|
+
import { adaptiveOrigin } from './utils/adaptiveOriginMiddleware';
|
|
45
|
+
import { useOpenChangeComplete } from './utils/useOpenChangeComplete';
|
|
46
|
+
import { getDisabledMountTransitionStyles } from './utils/getDisabledMountTransitionStyles';
|
|
47
|
+
import { POPUP_COLLISION_AVOIDANCE } from './utils/constants';
|
|
48
|
+
import {
|
|
49
|
+
popupStateMapping,
|
|
50
|
+
triggerOpenStateMapping,
|
|
51
|
+
CommonTriggerDataAttributes,
|
|
52
|
+
} from './utils/popupStateMapping';
|
|
53
|
+
import { transitionStatusMapping } from './utils/useTransitionStatus';
|
|
54
|
+
import type { StateAttributesMapping } from './utils/getStateAttributesProps';
|
|
55
|
+
import {
|
|
56
|
+
createInitialPopupStoreState,
|
|
57
|
+
createPopupFloatingRootContext,
|
|
58
|
+
popupStoreSelectors,
|
|
59
|
+
applyPopupOpenChange,
|
|
60
|
+
usePopupStore,
|
|
61
|
+
useInitialOpenSync,
|
|
62
|
+
useImplicitActiveTrigger,
|
|
63
|
+
useOpenStateTransitions,
|
|
64
|
+
usePopupInteractionProps,
|
|
65
|
+
useTriggerDataForwarding,
|
|
66
|
+
FOCUSABLE_POPUP_PROPS,
|
|
67
|
+
type PopupStoreState,
|
|
68
|
+
type PopupStoreContext,
|
|
69
|
+
} from './utils/popups';
|
|
70
|
+
import { PopupTriggerMap } from './utils/popups/popupTriggerMap';
|
|
71
|
+
|
|
72
|
+
export const OPEN_DELAY = 600;
|
|
73
|
+
|
|
74
|
+
const TOOLTIP_TRIGGER_IDENTIFIER = 'data-base-ui-tooltip-trigger';
|
|
75
|
+
|
|
76
|
+
export const TooltipTriggerDataAttributes = {
|
|
77
|
+
popupOpen: CommonTriggerDataAttributes.popupOpen,
|
|
78
|
+
triggerDisabled: 'data-trigger-disabled',
|
|
79
|
+
} as const;
|
|
80
|
+
|
|
81
|
+
export const TooltipViewportCssVars = {
|
|
82
|
+
popupWidth: '--popup-width',
|
|
83
|
+
popupHeight: '--popup-height',
|
|
84
|
+
} as const;
|
|
85
|
+
|
|
86
|
+
// --- Store -------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
export type TooltipState<Payload> = PopupStoreState<Payload> & {
|
|
89
|
+
disabled: boolean;
|
|
90
|
+
instantType: 'delay' | 'dismiss' | 'focus' | undefined;
|
|
91
|
+
isInstantPhase: boolean;
|
|
92
|
+
trackCursorAxis: 'none' | 'x' | 'y' | 'both';
|
|
93
|
+
disableHoverablePopup: boolean;
|
|
94
|
+
openChangeReason: string | null;
|
|
95
|
+
closeOnClick: boolean;
|
|
96
|
+
closeDelay: number;
|
|
97
|
+
hasViewport: boolean;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
type TooltipContext = PopupStoreContext<any> & {
|
|
101
|
+
readonly popupRef: { current: HTMLElement | null };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const tooltipSelectors = {
|
|
105
|
+
...popupStoreSelectors,
|
|
106
|
+
disabled: createSelector((state: TooltipState<unknown>) => state.disabled),
|
|
107
|
+
instantType: createSelector((state: TooltipState<unknown>) => state.instantType),
|
|
108
|
+
isInstantPhase: createSelector((state: TooltipState<unknown>) => state.isInstantPhase),
|
|
109
|
+
trackCursorAxis: createSelector((state: TooltipState<unknown>) => state.trackCursorAxis),
|
|
110
|
+
disableHoverablePopup: createSelector(
|
|
111
|
+
(state: TooltipState<unknown>) => state.disableHoverablePopup,
|
|
112
|
+
),
|
|
113
|
+
lastOpenChangeReason: createSelector((state: TooltipState<unknown>) => state.openChangeReason),
|
|
114
|
+
closeOnClick: createSelector((state: TooltipState<unknown>) => state.closeOnClick),
|
|
115
|
+
closeDelay: createSelector((state: TooltipState<unknown>) => state.closeDelay),
|
|
116
|
+
hasViewport: createSelector((state: TooltipState<unknown>) => state.hasViewport),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function createInitialTooltipState<Payload>(): TooltipState<Payload> {
|
|
120
|
+
return {
|
|
121
|
+
...createInitialPopupStoreState<Payload>(),
|
|
122
|
+
disabled: false,
|
|
123
|
+
instantType: undefined,
|
|
124
|
+
isInstantPhase: false,
|
|
125
|
+
trackCursorAxis: 'none',
|
|
126
|
+
disableHoverablePopup: false,
|
|
127
|
+
openChangeReason: null,
|
|
128
|
+
closeOnClick: true,
|
|
129
|
+
closeDelay: 0,
|
|
130
|
+
hasViewport: false,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class TooltipStore<Payload> extends ReactStore<
|
|
135
|
+
Readonly<TooltipState<Payload>>,
|
|
136
|
+
TooltipContext,
|
|
137
|
+
typeof tooltipSelectors
|
|
138
|
+
> {
|
|
139
|
+
constructor(
|
|
140
|
+
initialState?: Partial<TooltipState<Payload>>,
|
|
141
|
+
floatingId?: string | undefined,
|
|
142
|
+
nested = false,
|
|
143
|
+
) {
|
|
144
|
+
const triggerElements = new PopupTriggerMap();
|
|
145
|
+
const state = { ...createInitialTooltipState<Payload>(), ...initialState };
|
|
146
|
+
state.floatingRootContext = createPopupFloatingRootContext(triggerElements, floatingId, nested);
|
|
147
|
+
|
|
148
|
+
super(
|
|
149
|
+
state,
|
|
150
|
+
{
|
|
151
|
+
popupRef: { current: null },
|
|
152
|
+
onOpenChange: undefined,
|
|
153
|
+
onOpenChangeComplete: undefined,
|
|
154
|
+
triggerElements,
|
|
155
|
+
} as TooltipContext,
|
|
156
|
+
tooltipSelectors,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
setOpen = (nextOpen: boolean, eventDetails: any) => {
|
|
161
|
+
applyPopupOpenChange(this as any, nextOpen, eventDetails, {
|
|
162
|
+
extraState: { openChangeReason: eventDetails.reason } as any,
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Used by trigger clicks to clear a delayed hover open without reporting a public state change.
|
|
167
|
+
cancelPendingOpen(event: MouseEvent | PointerEvent): void {
|
|
168
|
+
(this.state.floatingRootContext as any).dispatchOpenChange(
|
|
169
|
+
false,
|
|
170
|
+
createChangeEventDetails(REASONS.triggerPress, event),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
static useStore<Payload>(
|
|
175
|
+
externalStore: TooltipStore<Payload> | undefined,
|
|
176
|
+
initialState: Partial<TooltipState<Payload>>,
|
|
177
|
+
slot: symbol | undefined,
|
|
178
|
+
): TooltipStore<Payload> {
|
|
179
|
+
return usePopupStore(
|
|
180
|
+
externalStore as any,
|
|
181
|
+
(floatingId, nested) => new TooltipStore<Payload>(initialState, floatingId, nested) as any,
|
|
182
|
+
true,
|
|
183
|
+
slot,
|
|
184
|
+
).store as unknown as TooltipStore<Payload>;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- Handle ------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
export class TooltipHandle<Payload> {
|
|
191
|
+
readonly store: TooltipStore<Payload>;
|
|
192
|
+
|
|
193
|
+
constructor(store?: TooltipStore<Payload>) {
|
|
194
|
+
this.store = store ?? new TooltipStore<Payload>();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
open(triggerId: string): void {
|
|
198
|
+
const triggerElement = triggerId
|
|
199
|
+
? (this.store.context.triggerElements.getById(triggerId) as HTMLElement | undefined)
|
|
200
|
+
: undefined;
|
|
201
|
+
|
|
202
|
+
if (triggerId && !triggerElement) {
|
|
203
|
+
throw new Error(`Base UI: TooltipHandle.open: No trigger found with id "${triggerId}".`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.store.setOpen(
|
|
207
|
+
true,
|
|
208
|
+
createChangeEventDetails(REASONS.imperativeAction, undefined, triggerElement),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
close(): void {
|
|
213
|
+
this.store.setOpen(
|
|
214
|
+
false,
|
|
215
|
+
createChangeEventDetails(REASONS.imperativeAction, undefined, undefined),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
get isOpen(): boolean {
|
|
220
|
+
return this.store.select('open');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function createTooltipHandle<Payload = unknown>(): TooltipHandle<Payload> {
|
|
225
|
+
return new TooltipHandle<Payload>();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- Contexts ----------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
const TooltipRootContext = createContext<TooltipStore<any> | undefined>(undefined);
|
|
231
|
+
|
|
232
|
+
export function useTooltipRootContext(optional?: boolean): TooltipStore<any> | undefined {
|
|
233
|
+
const context = useContext(TooltipRootContext);
|
|
234
|
+
if (context === undefined && !optional) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
'Base UI: TooltipRootContext is missing. Tooltip parts must be placed within <Tooltip.Root>.',
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return context;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface TooltipProviderContextValue {
|
|
243
|
+
delay: number | undefined;
|
|
244
|
+
closeDelay: number | undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const TooltipProviderContext = createContext<TooltipProviderContextValue | undefined>(undefined);
|
|
248
|
+
|
|
249
|
+
function useTooltipProviderContext(): TooltipProviderContextValue | undefined {
|
|
250
|
+
return useContext(TooltipProviderContext);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const TooltipPortalContext = createContext<boolean | undefined>(undefined);
|
|
254
|
+
|
|
255
|
+
function useTooltipPortalContext(): boolean {
|
|
256
|
+
const value = useContext(TooltipPortalContext);
|
|
257
|
+
if (value === undefined) {
|
|
258
|
+
throw new Error('Base UI: <Tooltip.Portal> is missing.');
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface TooltipPositionerContextValue {
|
|
264
|
+
side: Side;
|
|
265
|
+
align: Align;
|
|
266
|
+
arrowRef: { current: Element | null };
|
|
267
|
+
arrowUncentered: boolean;
|
|
268
|
+
arrowStyles: Record<string, any>;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const TooltipPositionerContext = createContext<TooltipPositionerContextValue | undefined>(
|
|
272
|
+
undefined,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
export function useTooltipPositionerContext(): TooltipPositionerContextValue {
|
|
276
|
+
const context = useContext(TooltipPositionerContext);
|
|
277
|
+
if (!context) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
'Base UI: TooltipPositionerContext is missing. TooltipPositioner parts must be placed within <Tooltip.Positioner>.',
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return context;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// --- Provider ----------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
// Shares one delay across a group of tooltips: once one is visible, its neighbours open instantly.
|
|
288
|
+
function TooltipProvider(props: any): any {
|
|
289
|
+
const slot = S('TooltipProvider');
|
|
290
|
+
const { delay, closeDelay, timeout = 400, children } = props;
|
|
291
|
+
|
|
292
|
+
const contextValue = useMemo(
|
|
293
|
+
() => ({ delay, closeDelay }),
|
|
294
|
+
[delay, closeDelay],
|
|
295
|
+
subSlot(slot, 'ctx'),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const delayValue = useMemo(
|
|
299
|
+
() => ({ open: delay, close: closeDelay }),
|
|
300
|
+
[delay, closeDelay],
|
|
301
|
+
subSlot(slot, 'delay'),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
return createElement(TooltipProviderContext.Provider, {
|
|
305
|
+
value: contextValue,
|
|
306
|
+
children: createElement(FloatingDelayGroup, {
|
|
307
|
+
delay: delayValue as any,
|
|
308
|
+
timeoutMs: timeout,
|
|
309
|
+
children,
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// --- Interactions ------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
function TooltipInteractions(props: any): any {
|
|
317
|
+
const slot = S('TooltipInteractions');
|
|
318
|
+
const { store, disabled, trackCursorAxis } = props;
|
|
319
|
+
|
|
320
|
+
const floatingRootContext = store.useState('floatingRootContext', subSlot(slot, 'frc'));
|
|
321
|
+
|
|
322
|
+
const dismiss = useDismiss(
|
|
323
|
+
floatingRootContext,
|
|
324
|
+
{ enabled: !disabled, referencePress: () => store.select('closeOnClick') },
|
|
325
|
+
subSlot(slot, 'dismiss'),
|
|
326
|
+
);
|
|
327
|
+
const clientPoint = useClientPoint(
|
|
328
|
+
floatingRootContext,
|
|
329
|
+
{
|
|
330
|
+
enabled: !disabled && trackCursorAxis !== 'none',
|
|
331
|
+
axis: trackCursorAxis === 'none' ? undefined : trackCursorAxis,
|
|
332
|
+
},
|
|
333
|
+
subSlot(slot, 'cp'),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const activeTriggerProps = useMemo(
|
|
337
|
+
() => mergeProps(clientPoint.reference, dismiss.reference),
|
|
338
|
+
[clientPoint.reference, dismiss.reference],
|
|
339
|
+
subSlot(slot, 'atp'),
|
|
340
|
+
);
|
|
341
|
+
const inactiveTriggerProps = useMemo(
|
|
342
|
+
() => mergeProps(clientPoint.trigger, dismiss.trigger),
|
|
343
|
+
[clientPoint.trigger, dismiss.trigger],
|
|
344
|
+
subSlot(slot, 'itp'),
|
|
345
|
+
);
|
|
346
|
+
const popupProps = useMemo(
|
|
347
|
+
() => mergeProps(FOCUSABLE_POPUP_PROPS, clientPoint.floating, dismiss.floating),
|
|
348
|
+
[clientPoint.floating, dismiss.floating],
|
|
349
|
+
subSlot(slot, 'pp'),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
usePopupInteractionProps(
|
|
353
|
+
store,
|
|
354
|
+
{ activeTriggerProps, inactiveTriggerProps, popupProps } as any,
|
|
355
|
+
subSlot(slot, 'pip'),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// --- Root --------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
function TooltipRoot<Payload = unknown>(props: any): any {
|
|
364
|
+
const slot = S('TooltipRoot');
|
|
365
|
+
const {
|
|
366
|
+
disabled = false,
|
|
367
|
+
defaultOpen = false,
|
|
368
|
+
open: openProp,
|
|
369
|
+
disableHoverablePopup = false,
|
|
370
|
+
trackCursorAxis = 'none',
|
|
371
|
+
actionsRef,
|
|
372
|
+
onOpenChange,
|
|
373
|
+
onOpenChangeComplete,
|
|
374
|
+
handle,
|
|
375
|
+
triggerId: triggerIdProp,
|
|
376
|
+
defaultTriggerId: defaultTriggerIdProp = null,
|
|
377
|
+
children,
|
|
378
|
+
} = props;
|
|
379
|
+
|
|
380
|
+
const store = TooltipStore.useStore<Payload>(
|
|
381
|
+
handle?.store,
|
|
382
|
+
{
|
|
383
|
+
open: defaultOpen,
|
|
384
|
+
openProp,
|
|
385
|
+
activeTriggerId: defaultTriggerIdProp,
|
|
386
|
+
triggerIdProp,
|
|
387
|
+
} as any,
|
|
388
|
+
subSlot(slot, 'store'),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
useInitialOpenSync(store, openProp, defaultOpen, defaultTriggerIdProp, subSlot(slot, 'ios'));
|
|
392
|
+
|
|
393
|
+
store.useControlledProp('openProp', openProp, subSlot(slot, 'cp:open'));
|
|
394
|
+
store.useControlledProp('triggerIdProp', triggerIdProp, subSlot(slot, 'cp:trigger'));
|
|
395
|
+
|
|
396
|
+
store.useContextCallback('onOpenChange', onOpenChange, subSlot(slot, 'cb:open'));
|
|
397
|
+
store.useContextCallback(
|
|
398
|
+
'onOpenChangeComplete',
|
|
399
|
+
onOpenChangeComplete,
|
|
400
|
+
subSlot(slot, 'cb:complete'),
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const openState = store.useState('open', subSlot(slot, 'open'));
|
|
404
|
+
const open = !disabled && openState;
|
|
405
|
+
|
|
406
|
+
const activeTriggerId = store.useState('activeTriggerId', subSlot(slot, 'atid'));
|
|
407
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
408
|
+
const payload = store.useState('payload', subSlot(slot, 'payload'));
|
|
409
|
+
|
|
410
|
+
store.useSyncedValue('trackCursorAxis', trackCursorAxis, subSlot(slot, 'sv:tca'));
|
|
411
|
+
store.useSyncedValue('disableHoverablePopup', disableHoverablePopup, subSlot(slot, 'sv:dhp'));
|
|
412
|
+
store.useSyncedValue('disabled', disabled, subSlot(slot, 'sv:disabled'));
|
|
413
|
+
|
|
414
|
+
useImplicitActiveTrigger(store, { closeOnActiveTriggerUnmount: true }, subSlot(slot, 'implicit'));
|
|
415
|
+
const { forceUnmount, transitionStatus } = useOpenStateTransitions(
|
|
416
|
+
open,
|
|
417
|
+
store as any,
|
|
418
|
+
undefined,
|
|
419
|
+
subSlot(slot, 'transitions'),
|
|
420
|
+
);
|
|
421
|
+
const isInstantPhase = store.useState('isInstantPhase', subSlot(slot, 'iip'));
|
|
422
|
+
const instantType = store.useState('instantType', subSlot(slot, 'itype'));
|
|
423
|
+
const lastOpenChangeReason = store.useState('lastOpenChangeReason', subSlot(slot, 'locr'));
|
|
424
|
+
|
|
425
|
+
const previousInstantTypeRef = useRef<string | undefined | null>(null, subSlot(slot, 'pit'));
|
|
426
|
+
|
|
427
|
+
useLayoutEffect(
|
|
428
|
+
() => {
|
|
429
|
+
if (openState && disabled) {
|
|
430
|
+
store.setOpen(false, createChangeEventDetails(REASONS.disabled));
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
[openState, disabled, store],
|
|
434
|
+
subSlot(slot, 'l:disabled'),
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// Animations are instant in two cases: opening during the provider's instant phase (an adjacent
|
|
438
|
+
// tooltip took over), and closing because another tooltip opened (reason === 'none'). Otherwise
|
|
439
|
+
// the animation plays — in particular, the 'ending' phase is NOT made instant unless a sibling
|
|
440
|
+
// caused it.
|
|
441
|
+
useLayoutEffect(
|
|
442
|
+
() => {
|
|
443
|
+
if (
|
|
444
|
+
(transitionStatus === 'ending' && lastOpenChangeReason === REASONS.none) ||
|
|
445
|
+
(transitionStatus !== 'ending' && isInstantPhase)
|
|
446
|
+
) {
|
|
447
|
+
if (instantType !== 'delay') {
|
|
448
|
+
previousInstantTypeRef.current = instantType;
|
|
449
|
+
}
|
|
450
|
+
store.set('instantType', 'delay');
|
|
451
|
+
} else if (previousInstantTypeRef.current !== null) {
|
|
452
|
+
store.set('instantType', previousInstantTypeRef.current);
|
|
453
|
+
previousInstantTypeRef.current = null;
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
[transitionStatus, isInstantPhase, lastOpenChangeReason, instantType, store],
|
|
457
|
+
subSlot(slot, 'l:instant'),
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
useLayoutEffect(
|
|
461
|
+
() => {
|
|
462
|
+
if (open && activeTriggerId == null) {
|
|
463
|
+
store.set('payload', undefined);
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
[store, activeTriggerId, open],
|
|
467
|
+
subSlot(slot, 'l:payload'),
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const handleImperativeClose = useCallback(
|
|
471
|
+
() => {
|
|
472
|
+
store.setOpen(false, createChangeEventDetails(REASONS.imperativeAction));
|
|
473
|
+
},
|
|
474
|
+
[store],
|
|
475
|
+
subSlot(slot, 'close'),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
useImperativeHandle(
|
|
479
|
+
actionsRef,
|
|
480
|
+
() => ({ unmount: forceUnmount, close: handleImperativeClose }),
|
|
481
|
+
[forceUnmount, handleImperativeClose],
|
|
482
|
+
subSlot(slot, 'imp'),
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
const shouldRenderInteractions = open || mounted || (!disabled && trackCursorAxis !== 'none');
|
|
486
|
+
|
|
487
|
+
const resolvedChildren =
|
|
488
|
+
typeof children === 'function' && !isChildrenBlock(children) ? children({ payload }) : children;
|
|
489
|
+
|
|
490
|
+
// `TooltipInteractions` is a headless SIBLING of the children, exactly as Base UI renders it.
|
|
491
|
+
const content = [
|
|
492
|
+
shouldRenderInteractions
|
|
493
|
+
? createElement(TooltipInteractions, {
|
|
494
|
+
key: 'interactions',
|
|
495
|
+
store,
|
|
496
|
+
disabled,
|
|
497
|
+
trackCursorAxis,
|
|
498
|
+
})
|
|
499
|
+
: null,
|
|
500
|
+
createElement(Fragment, { key: 'children', children: resolvedChildren }),
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
return createElement(TooltipRootContext.Provider, { value: store, children: content });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// --- Trigger -----------------------------------------------------------------
|
|
507
|
+
|
|
508
|
+
function getTargetElement(event: Event): Element | null {
|
|
509
|
+
if ('composedPath' in event) {
|
|
510
|
+
const path = event.composedPath();
|
|
511
|
+
for (let i = 0; i < path.length; i += 1) {
|
|
512
|
+
const element = path[i];
|
|
513
|
+
if (isElement(element)) {
|
|
514
|
+
return element as Element;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const target = event.target;
|
|
519
|
+
if (isElement(target)) {
|
|
520
|
+
return target as Element;
|
|
521
|
+
}
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function closestEnabledTooltipTrigger(element: Element | null): Element | null {
|
|
526
|
+
let current = element;
|
|
527
|
+
while (current) {
|
|
528
|
+
if (current.hasAttribute(TOOLTIP_TRIGGER_IDENTIFIER)) {
|
|
529
|
+
return current;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const parentElement = current.parentElement;
|
|
533
|
+
if (parentElement) {
|
|
534
|
+
current = parentElement;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const root = current.getRootNode();
|
|
539
|
+
current =
|
|
540
|
+
'host' in root && isElement((root as ShadowRoot).host) ? (root as ShadowRoot).host : null;
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function TooltipTrigger(componentProps: any): any {
|
|
546
|
+
const slot = S('TooltipTrigger');
|
|
547
|
+
const {
|
|
548
|
+
render,
|
|
549
|
+
className,
|
|
550
|
+
style,
|
|
551
|
+
handle,
|
|
552
|
+
payload,
|
|
553
|
+
disabled: disabledProp,
|
|
554
|
+
delay,
|
|
555
|
+
closeOnClick = true,
|
|
556
|
+
closeDelay,
|
|
557
|
+
id: idProp,
|
|
558
|
+
ref,
|
|
559
|
+
...elementProps
|
|
560
|
+
} = componentProps;
|
|
561
|
+
|
|
562
|
+
const rootContext = useTooltipRootContext(true);
|
|
563
|
+
const store = (handle?.store ?? rootContext) as TooltipStore<any> | undefined;
|
|
564
|
+
if (!store) {
|
|
565
|
+
throw new Error(
|
|
566
|
+
'Base UI: <Tooltip.Trigger> must be either used within a <Tooltip.Root> component or provided with a handle.',
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const thisTriggerId = useBaseUiId(idProp, subSlot(slot, 'id'));
|
|
571
|
+
const isTriggerActive = store.useState('isTriggerActive', subSlot(slot, 'ita'), thisTriggerId);
|
|
572
|
+
const isOpenedByThisTrigger = store.useState(
|
|
573
|
+
'isOpenedByTrigger',
|
|
574
|
+
subSlot(slot, 'obt'),
|
|
575
|
+
thisTriggerId,
|
|
576
|
+
);
|
|
577
|
+
const floatingRootContext = store.useState('floatingRootContext', subSlot(slot, 'frc'));
|
|
578
|
+
|
|
579
|
+
const triggerElementRef = useRef<Element | null>(null, subSlot(slot, 'trigEl'));
|
|
580
|
+
|
|
581
|
+
const delayWithDefault = delay ?? OPEN_DELAY;
|
|
582
|
+
const closeDelayWithDefault = closeDelay ?? 0;
|
|
583
|
+
|
|
584
|
+
const { registerTrigger, isMountedByThisTrigger } = useTriggerDataForwarding(
|
|
585
|
+
thisTriggerId,
|
|
586
|
+
triggerElementRef,
|
|
587
|
+
store,
|
|
588
|
+
{ payload, closeOnClick, closeDelay: closeDelayWithDefault },
|
|
589
|
+
subSlot(slot, 'tdf'),
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
const providerContext = useTooltipProviderContext();
|
|
593
|
+
const { delayRef, isInstantPhase, hasProvider } = useDelayGroup(
|
|
594
|
+
floatingRootContext,
|
|
595
|
+
{ open: isOpenedByThisTrigger },
|
|
596
|
+
subSlot(slot, 'group'),
|
|
597
|
+
);
|
|
598
|
+
const hoverInteraction = useHoverInteractionSharedState(
|
|
599
|
+
floatingRootContext,
|
|
600
|
+
subSlot(slot, 'hoverState'),
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
store.useSyncedValue('isInstantPhase', isInstantPhase, subSlot(slot, 'sv:iip'));
|
|
604
|
+
|
|
605
|
+
const rootDisabled = store.useState('disabled', subSlot(slot, 'rootDisabled'));
|
|
606
|
+
const disabled = disabledProp ?? rootDisabled;
|
|
607
|
+
const disabledRef = useValueAsRef<boolean>(disabled, subSlot(slot, 'disabledRef'));
|
|
608
|
+
const trackCursorAxis = store.useState('trackCursorAxis', subSlot(slot, 'tca'));
|
|
609
|
+
const disableHoverablePopup = store.useState('disableHoverablePopup', subSlot(slot, 'dhp'));
|
|
610
|
+
|
|
611
|
+
const isNestedTriggerHoveredRef = useRef(false, subSlot(slot, 'nested'));
|
|
612
|
+
const nestedTriggerOpenTimeout = useTimeout(subSlot(slot, 'nestedTo'));
|
|
613
|
+
// A local copy, so `mouseleave` can clear it without resetting the hover hook's own pointerType.
|
|
614
|
+
const pointerTypeRef = useRef<string | undefined>(undefined, subSlot(slot, 'ptype'));
|
|
615
|
+
|
|
616
|
+
function getOpenDelay(): number {
|
|
617
|
+
const providerDelay = providerContext?.delay;
|
|
618
|
+
const groupOpenValue = typeof delayRef.current === 'object' ? delayRef.current.open : undefined;
|
|
619
|
+
|
|
620
|
+
let computedOpenDelay = delayWithDefault;
|
|
621
|
+
if (hasProvider) {
|
|
622
|
+
if (groupOpenValue !== 0) {
|
|
623
|
+
computedOpenDelay = delay ?? providerDelay ?? delayWithDefault;
|
|
624
|
+
} else {
|
|
625
|
+
computedOpenDelay = 0;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return computedOpenDelay;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function isEnabledNestedTriggerTarget(target: Element | null): boolean {
|
|
632
|
+
const triggerEl = triggerElementRef.current;
|
|
633
|
+
if (!triggerEl || !target) {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
const nearestTrigger = closestEnabledTooltipTrigger(target);
|
|
637
|
+
return (
|
|
638
|
+
nearestTrigger !== null && nearestTrigger !== triggerEl && contains(triggerEl, nearestTrigger)
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function detectNestedTriggerHover(target: Element | null): boolean {
|
|
643
|
+
const nestedTriggerHovered = isEnabledNestedTriggerTarget(target);
|
|
644
|
+
|
|
645
|
+
isNestedTriggerHoveredRef.current = nestedTriggerHovered;
|
|
646
|
+
if (nestedTriggerHovered) {
|
|
647
|
+
hoverInteraction.openChangeTimeout.clear();
|
|
648
|
+
hoverInteraction.restTimeout.clear();
|
|
649
|
+
hoverInteraction.restTimeoutPending = false;
|
|
650
|
+
nestedTriggerOpenTimeout.clear();
|
|
651
|
+
}
|
|
652
|
+
return nestedTriggerHovered;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const hoverProps = useHoverReferenceInteraction(
|
|
656
|
+
floatingRootContext,
|
|
657
|
+
{
|
|
658
|
+
enabled: !disabled,
|
|
659
|
+
mouseOnly: true,
|
|
660
|
+
move: false,
|
|
661
|
+
handleClose: !disableHoverablePopup && trackCursorAxis !== 'both' ? safePolygon() : null,
|
|
662
|
+
restMs: getOpenDelay,
|
|
663
|
+
delay() {
|
|
664
|
+
const closeValue =
|
|
665
|
+
typeof delayRef.current === 'object' ? delayRef.current.close : undefined;
|
|
666
|
+
|
|
667
|
+
let computedCloseDelay: number | undefined = closeDelayWithDefault;
|
|
668
|
+
if (closeDelay == null && hasProvider) {
|
|
669
|
+
computedCloseDelay = closeValue;
|
|
670
|
+
}
|
|
671
|
+
return { close: computedCloseDelay } as any;
|
|
672
|
+
},
|
|
673
|
+
triggerElementRef,
|
|
674
|
+
isActiveTrigger: isTriggerActive,
|
|
675
|
+
isClosing: () => store.select('transitionStatus') === 'ending',
|
|
676
|
+
shouldOpen() {
|
|
677
|
+
return !isNestedTriggerHoveredRef.current;
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
subSlot(slot, 'hover'),
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
const focusProps = useFocus(
|
|
684
|
+
floatingRootContext,
|
|
685
|
+
{ enabled: !disabled },
|
|
686
|
+
subSlot(slot, 'focus'),
|
|
687
|
+
).reference;
|
|
688
|
+
|
|
689
|
+
function handleNestedTriggerHover(event: MouseEvent): void {
|
|
690
|
+
const wasNestedTriggerHovered = isNestedTriggerHoveredRef.current;
|
|
691
|
+
const target = getTargetElement(event);
|
|
692
|
+
const nestedTriggerHovered = detectNestedTriggerHover(target);
|
|
693
|
+
const triggerEl = triggerElementRef.current as HTMLElement | null;
|
|
694
|
+
const targetInsideTrigger = triggerEl && target && contains(triggerEl, target);
|
|
695
|
+
|
|
696
|
+
// Only close hover-opened parents. Focus/click-like opens stay owned by their original
|
|
697
|
+
// interaction and must not be clobbered by a nested hover.
|
|
698
|
+
if (
|
|
699
|
+
nestedTriggerHovered &&
|
|
700
|
+
store!.select('open') &&
|
|
701
|
+
store!.select('lastOpenChangeReason') === REASONS.triggerHover
|
|
702
|
+
) {
|
|
703
|
+
store!.setOpen(false, createChangeEventDetails(REASONS.triggerHover, event));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (
|
|
708
|
+
wasNestedTriggerHovered &&
|
|
709
|
+
!nestedTriggerHovered &&
|
|
710
|
+
targetInsideTrigger &&
|
|
711
|
+
!disabledRef.current &&
|
|
712
|
+
!store!.select('open') &&
|
|
713
|
+
triggerEl &&
|
|
714
|
+
// Match the hover hook's non-strict mouse fallback for mouse-only event sequences.
|
|
715
|
+
isMouseLikePointerType(pointerTypeRef.current)
|
|
716
|
+
) {
|
|
717
|
+
const openTooltip = () => {
|
|
718
|
+
if (!isNestedTriggerHoveredRef.current && !disabledRef.current && !store!.select('open')) {
|
|
719
|
+
store!.setOpen(true, createChangeEventDetails(REASONS.triggerHover, event, triggerEl));
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
const openDelay = getOpenDelay();
|
|
724
|
+
|
|
725
|
+
// With `move: false` the hover hook only listens to mouseenter/mouseleave on the parent
|
|
726
|
+
// trigger, so leaving a nested child for the parent area fires nothing it can react to.
|
|
727
|
+
// Reopen locally instead.
|
|
728
|
+
if (openDelay === 0) {
|
|
729
|
+
nestedTriggerOpenTimeout.clear();
|
|
730
|
+
openTooltip();
|
|
731
|
+
} else {
|
|
732
|
+
nestedTriggerOpenTimeout.start(openDelay, openTooltip);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const rootTriggerProps = store.useState(
|
|
738
|
+
'triggerProps',
|
|
739
|
+
subSlot(slot, 'tp'),
|
|
740
|
+
isMountedByThisTrigger,
|
|
741
|
+
);
|
|
742
|
+
const shouldApplyRootTriggerProps = isMountedByThisTrigger || trackCursorAxis !== 'none';
|
|
743
|
+
|
|
744
|
+
const state = { open: isOpenedByThisTrigger };
|
|
745
|
+
|
|
746
|
+
return useRenderElement(
|
|
747
|
+
'button',
|
|
748
|
+
{ render, className, style },
|
|
749
|
+
{
|
|
750
|
+
state,
|
|
751
|
+
ref: [ref, registerTrigger, triggerElementRef],
|
|
752
|
+
props: [
|
|
753
|
+
hoverProps,
|
|
754
|
+
focusProps,
|
|
755
|
+
shouldApplyRootTriggerProps ? rootTriggerProps : undefined,
|
|
756
|
+
{
|
|
757
|
+
onMouseOver(event: MouseEvent) {
|
|
758
|
+
handleNestedTriggerHover(event);
|
|
759
|
+
},
|
|
760
|
+
onFocus(event: FocusEvent) {
|
|
761
|
+
if (isEnabledNestedTriggerTarget(getTargetElement(event))) {
|
|
762
|
+
(event as any).preventBaseUIHandler?.();
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
onMouseLeave() {
|
|
766
|
+
isNestedTriggerHoveredRef.current = false;
|
|
767
|
+
nestedTriggerOpenTimeout.clear();
|
|
768
|
+
pointerTypeRef.current = undefined;
|
|
769
|
+
},
|
|
770
|
+
onPointerEnter(event: PointerEvent) {
|
|
771
|
+
pointerTypeRef.current = event.pointerType;
|
|
772
|
+
},
|
|
773
|
+
onPointerDown(event: PointerEvent) {
|
|
774
|
+
pointerTypeRef.current = event.pointerType;
|
|
775
|
+
store!.set('closeOnClick', closeOnClick);
|
|
776
|
+
if (closeOnClick && !store!.select('open')) {
|
|
777
|
+
store!.cancelPendingOpen(event);
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
onClick(event: MouseEvent) {
|
|
781
|
+
if (closeOnClick && !store!.select('open')) {
|
|
782
|
+
store!.cancelPendingOpen(event);
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
id: thisTriggerId,
|
|
786
|
+
[TooltipTriggerDataAttributes.triggerDisabled]: disabled ? '' : undefined,
|
|
787
|
+
[TOOLTIP_TRIGGER_IDENTIFIER]: disabled ? undefined : '',
|
|
788
|
+
},
|
|
789
|
+
elementProps,
|
|
790
|
+
],
|
|
791
|
+
stateAttributesMapping: triggerOpenStateMapping,
|
|
792
|
+
},
|
|
793
|
+
subSlot(slot, 're'),
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// --- Portal ------------------------------------------------------------------
|
|
798
|
+
|
|
799
|
+
function TooltipPortal(props: any): any {
|
|
800
|
+
const slot = S('TooltipPortal');
|
|
801
|
+
const { keepMounted = false, ref, ...portalProps } = props;
|
|
802
|
+
|
|
803
|
+
const store = useTooltipRootContext() as TooltipStore<any>;
|
|
804
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
805
|
+
|
|
806
|
+
if (!(mounted || keepMounted)) {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return createElement(TooltipPortalContext.Provider, {
|
|
811
|
+
value: keepMounted,
|
|
812
|
+
children: createElement(FloatingPortalLite, { ref, ...portalProps }),
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// --- Positioner --------------------------------------------------------------
|
|
817
|
+
|
|
818
|
+
function TooltipPositioner(componentProps: any): any {
|
|
819
|
+
const slot = S('TooltipPositioner');
|
|
820
|
+
const {
|
|
821
|
+
render,
|
|
822
|
+
className,
|
|
823
|
+
style,
|
|
824
|
+
anchor,
|
|
825
|
+
positionMethod = 'absolute',
|
|
826
|
+
side = 'top',
|
|
827
|
+
align = 'center',
|
|
828
|
+
sideOffset = 0,
|
|
829
|
+
alignOffset = 0,
|
|
830
|
+
collisionBoundary = 'clipping-ancestors',
|
|
831
|
+
collisionPadding = 5,
|
|
832
|
+
arrowPadding = 5,
|
|
833
|
+
sticky = false,
|
|
834
|
+
disableAnchorTracking = false,
|
|
835
|
+
collisionAvoidance = POPUP_COLLISION_AVOIDANCE,
|
|
836
|
+
ref,
|
|
837
|
+
...elementProps
|
|
838
|
+
} = componentProps;
|
|
839
|
+
|
|
840
|
+
const store = useTooltipRootContext() as TooltipStore<any>;
|
|
841
|
+
const keepMounted = useTooltipPortalContext();
|
|
842
|
+
|
|
843
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
844
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
845
|
+
const trackCursorAxis = store.useState('trackCursorAxis', subSlot(slot, 'tca'));
|
|
846
|
+
const disableHoverablePopup = store.useState('disableHoverablePopup', subSlot(slot, 'dhp'));
|
|
847
|
+
const floatingRootContext = store.useState('floatingRootContext', subSlot(slot, 'frc'));
|
|
848
|
+
const instantType = store.useState('instantType', subSlot(slot, 'instant'));
|
|
849
|
+
const transitionStatus = store.useState('transitionStatus', subSlot(slot, 'trans'));
|
|
850
|
+
const hasViewport = store.useState('hasViewport', subSlot(slot, 'viewport'));
|
|
851
|
+
|
|
852
|
+
const positioning = useAnchorPositioning(
|
|
853
|
+
{
|
|
854
|
+
anchor,
|
|
855
|
+
positionMethod,
|
|
856
|
+
floatingRootContext,
|
|
857
|
+
mounted,
|
|
858
|
+
side,
|
|
859
|
+
sideOffset,
|
|
860
|
+
align,
|
|
861
|
+
alignOffset,
|
|
862
|
+
collisionBoundary,
|
|
863
|
+
collisionPadding,
|
|
864
|
+
sticky,
|
|
865
|
+
arrowPadding,
|
|
866
|
+
disableAnchorTracking,
|
|
867
|
+
keepMounted,
|
|
868
|
+
collisionAvoidance,
|
|
869
|
+
adaptiveOrigin: hasViewport ? adaptiveOrigin : undefined,
|
|
870
|
+
},
|
|
871
|
+
subSlot(slot, 'pos'),
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
const state = useMemo(
|
|
875
|
+
() => ({
|
|
876
|
+
open,
|
|
877
|
+
side: positioning.side,
|
|
878
|
+
align: positioning.align,
|
|
879
|
+
anchorHidden: positioning.anchorHidden,
|
|
880
|
+
instant: trackCursorAxis !== 'none' ? 'tracking-cursor' : instantType,
|
|
881
|
+
}),
|
|
882
|
+
[
|
|
883
|
+
open,
|
|
884
|
+
positioning.side,
|
|
885
|
+
positioning.align,
|
|
886
|
+
positioning.anchorHidden,
|
|
887
|
+
trackCursorAxis,
|
|
888
|
+
instantType,
|
|
889
|
+
],
|
|
890
|
+
subSlot(slot, 'state'),
|
|
891
|
+
);
|
|
892
|
+
|
|
893
|
+
const setPositionerElement = store.useStateSetter('positionerElement', subSlot(slot, 'spe'));
|
|
894
|
+
|
|
895
|
+
const element = usePositioner(
|
|
896
|
+
{ render, className, style },
|
|
897
|
+
state,
|
|
898
|
+
{
|
|
899
|
+
styles: positioning.positionerStyles,
|
|
900
|
+
transitionStatus,
|
|
901
|
+
props: elementProps,
|
|
902
|
+
refs: [ref, setPositionerElement],
|
|
903
|
+
hidden: !mounted,
|
|
904
|
+
inert: !open || trackCursorAxis === 'both' || disableHoverablePopup,
|
|
905
|
+
},
|
|
906
|
+
subSlot(slot, 'positioner'),
|
|
907
|
+
);
|
|
908
|
+
|
|
909
|
+
return createElement(TooltipPositionerContext.Provider, {
|
|
910
|
+
value: positioning as TooltipPositionerContextValue,
|
|
911
|
+
children: element,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// --- Popup -------------------------------------------------------------------
|
|
916
|
+
|
|
917
|
+
const popupStateAttributesMapping: StateAttributesMapping<any> = {
|
|
918
|
+
...(popupStateMapping as StateAttributesMapping<any>),
|
|
919
|
+
...(transitionStatusMapping as StateAttributesMapping<any>),
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
function TooltipPopup(componentProps: any): any {
|
|
923
|
+
const slot = S('TooltipPopup');
|
|
924
|
+
const { render, className, style, ref, ...elementProps } = componentProps;
|
|
925
|
+
|
|
926
|
+
const store = useTooltipRootContext() as TooltipStore<any>;
|
|
927
|
+
const { side, align } = useTooltipPositionerContext();
|
|
928
|
+
|
|
929
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
930
|
+
const instantType = store.useState('instantType', subSlot(slot, 'instant'));
|
|
931
|
+
const transitionStatus = store.useState('transitionStatus', subSlot(slot, 'trans'));
|
|
932
|
+
const popupProps = store.useState('popupProps', subSlot(slot, 'pp'));
|
|
933
|
+
const floatingContext = store.useState('floatingRootContext', subSlot(slot, 'frc'));
|
|
934
|
+
const disabled = store.useState('disabled', subSlot(slot, 'disabled'));
|
|
935
|
+
const closeDelay = store.useState('closeDelay', subSlot(slot, 'closeDelay'));
|
|
936
|
+
|
|
937
|
+
useOpenChangeComplete(
|
|
938
|
+
{
|
|
939
|
+
open,
|
|
940
|
+
ref: store.context.popupRef,
|
|
941
|
+
onComplete() {
|
|
942
|
+
if (open) {
|
|
943
|
+
store.context.onOpenChangeComplete?.(true);
|
|
944
|
+
}
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
subSlot(slot, 'occ'),
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
useHoverFloatingInteraction(
|
|
951
|
+
floatingContext,
|
|
952
|
+
{ enabled: !disabled, closeDelay },
|
|
953
|
+
subSlot(slot, 'hoverPopup'),
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
const setPopupElement = store.useStateSetter('popupElement', subSlot(slot, 'spe'));
|
|
957
|
+
|
|
958
|
+
const state = { open, side, align, instant: instantType, transitionStatus };
|
|
959
|
+
|
|
960
|
+
return useRenderElement(
|
|
961
|
+
'div',
|
|
962
|
+
{ render, className, style },
|
|
963
|
+
{
|
|
964
|
+
state,
|
|
965
|
+
ref: [ref, store.context.popupRef, setPopupElement],
|
|
966
|
+
props: [popupProps, getDisabledMountTransitionStyles(transitionStatus), elementProps],
|
|
967
|
+
stateAttributesMapping: popupStateAttributesMapping,
|
|
968
|
+
},
|
|
969
|
+
subSlot(slot, 're'),
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// --- Arrow -------------------------------------------------------------------
|
|
974
|
+
|
|
975
|
+
function TooltipArrow(componentProps: any): any {
|
|
976
|
+
const slot = S('TooltipArrow');
|
|
977
|
+
const { render, className, style, ref, ...elementProps } = componentProps;
|
|
978
|
+
|
|
979
|
+
const store = useTooltipRootContext() as TooltipStore<any>;
|
|
980
|
+
const { arrowRef, side, align, arrowUncentered, arrowStyles } = useTooltipPositionerContext();
|
|
981
|
+
|
|
982
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
983
|
+
const instantType = store.useState('instantType', subSlot(slot, 'instant'));
|
|
984
|
+
|
|
985
|
+
const state = { open, side, align, uncentered: arrowUncentered, instant: instantType };
|
|
986
|
+
|
|
987
|
+
return useRenderElement(
|
|
988
|
+
'div',
|
|
989
|
+
{ render, className, style },
|
|
990
|
+
{
|
|
991
|
+
state,
|
|
992
|
+
ref: [ref, arrowRef],
|
|
993
|
+
props: [{ style: arrowStyles, 'aria-hidden': true }, elementProps],
|
|
994
|
+
stateAttributesMapping: popupStateMapping as StateAttributesMapping<any>,
|
|
995
|
+
},
|
|
996
|
+
subSlot(slot, 're'),
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// --- Viewport ----------------------------------------------------------------
|
|
1001
|
+
|
|
1002
|
+
const viewportStateAttributesMapping: StateAttributesMapping<any> = {
|
|
1003
|
+
activationDirection: (value: string | undefined) =>
|
|
1004
|
+
value ? { 'data-activation-direction': value } : null,
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
function TooltipViewport(componentProps: any): any {
|
|
1008
|
+
const slot = S('TooltipViewport');
|
|
1009
|
+
const { render, className, style, children, ref, ...elementProps } = componentProps;
|
|
1010
|
+
|
|
1011
|
+
const store = useTooltipRootContext() as TooltipStore<any>;
|
|
1012
|
+
const positioner = useTooltipPositionerContext();
|
|
1013
|
+
|
|
1014
|
+
const instantType = store.useState('instantType', subSlot(slot, 'instant'));
|
|
1015
|
+
|
|
1016
|
+
const { children: childrenToRender, state: viewportState } = usePopupViewport(
|
|
1017
|
+
{ store, side: positioner.side, cssVars: TooltipViewportCssVars, children },
|
|
1018
|
+
subSlot(slot, 'vp'),
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
const state = {
|
|
1022
|
+
activationDirection: viewportState.activationDirection,
|
|
1023
|
+
transitioning: viewportState.transitioning,
|
|
1024
|
+
instant: instantType,
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
return useRenderElement(
|
|
1028
|
+
'div',
|
|
1029
|
+
{ render, className, style },
|
|
1030
|
+
{
|
|
1031
|
+
state,
|
|
1032
|
+
ref,
|
|
1033
|
+
props: [elementProps, { children: childrenToRender }],
|
|
1034
|
+
stateAttributesMapping: viewportStateAttributesMapping,
|
|
1035
|
+
},
|
|
1036
|
+
subSlot(slot, 're'),
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// --- Namespace ---------------------------------------------------------------
|
|
1041
|
+
|
|
1042
|
+
export const Tooltip = {
|
|
1043
|
+
Provider: TooltipProvider,
|
|
1044
|
+
Root: TooltipRoot,
|
|
1045
|
+
Trigger: TooltipTrigger,
|
|
1046
|
+
Portal: TooltipPortal,
|
|
1047
|
+
Positioner: TooltipPositioner,
|
|
1048
|
+
Popup: TooltipPopup,
|
|
1049
|
+
Arrow: TooltipArrow,
|
|
1050
|
+
Viewport: TooltipViewport,
|
|
1051
|
+
createHandle: createTooltipHandle,
|
|
1052
|
+
Handle: TooltipHandle,
|
|
1053
|
+
};
|