@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
|
@@ -1,17 +1,510 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// Ported from .base-ui/packages/react/src/floating-ui-react/hooks/useHoverReferenceInteraction.ts
|
|
2
|
+
// (v1.6.0). The trigger side of open-on-hover: `mouseenter`/`mouseleave` listeners bound directly
|
|
3
|
+
// to the trigger element (bypassing delegation, so entering from a disabled neighbour still fires),
|
|
4
|
+
// plus the `onMouseMove` rest-delay path returned as props.
|
|
5
|
+
//
|
|
6
|
+
// octane adaptations: handlers receive the NATIVE event, so `event.nativeEvent` collapses to
|
|
7
|
+
// `event`; `ReactDOM.flushSync` → octane's `flushSync`; every composed hook threads an explicit
|
|
8
|
+
// slot. Behavior is otherwise transcribed.
|
|
9
|
+
//
|
|
10
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
11
|
+
import { useEffect, useMemo, useRef, flushSync } from 'octane';
|
|
12
|
+
|
|
13
|
+
import { S, splitSlot, subSlot } from '../../internal';
|
|
14
|
+
import { addEventListener } from '../addEventListener';
|
|
15
|
+
import { mergeCleanups } from '../mergeCleanups';
|
|
16
|
+
import { ownerDocument } from '../owner';
|
|
17
|
+
import { useStableCallback } from '../useStableCallback';
|
|
18
|
+
import { useValueAsRef } from '../useValueAsRef';
|
|
19
|
+
import { isElement } from '../dom';
|
|
20
|
+
import { createChangeEventDetails, REASONS } from '../createChangeEventDetails';
|
|
21
|
+
import { useFloatingTree } from './FloatingTree';
|
|
22
|
+
import { contains, getTarget } from './element';
|
|
23
|
+
import { isMouseLikePointerType } from './event';
|
|
24
|
+
import {
|
|
25
|
+
applySafePolygonPointerEventsMutation,
|
|
26
|
+
clearSafePolygonPointerEventsMutation,
|
|
27
|
+
useHoverInteractionSharedState,
|
|
28
|
+
} from './useHoverInteractionSharedState';
|
|
29
|
+
import {
|
|
30
|
+
getDelay,
|
|
31
|
+
getRestMs,
|
|
32
|
+
isClickLikeOpenEvent as isClickLikeOpenEventShared,
|
|
33
|
+
isInsideEnabledTrigger,
|
|
34
|
+
} from './useHoverShared';
|
|
35
|
+
import type { HandleClose, HandleCloseContextBase, HoverDelay } from './useHoverShared';
|
|
36
|
+
|
|
37
|
+
export { safePolygon } from './safePolygon';
|
|
38
|
+
|
|
39
|
+
export interface UseHoverReferenceInteractionProps {
|
|
40
|
+
enabled?: boolean | undefined;
|
|
41
|
+
handleClose?: HandleClose | null | undefined;
|
|
42
|
+
restMs?: number | (() => number) | undefined;
|
|
43
|
+
delay?: HoverDelay | (() => HoverDelay) | undefined;
|
|
44
|
+
move?: boolean | undefined;
|
|
45
|
+
mouseOnly?: boolean | undefined;
|
|
46
|
+
externalTree?: any;
|
|
47
|
+
/**
|
|
48
|
+
* Whether the hook controls the active trigger. When false, the props are returned so they
|
|
49
|
+
* can be applied to inactive triggers via `getTriggerProps`.
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
52
|
+
isActiveTrigger?: boolean | undefined;
|
|
53
|
+
triggerElementRef?: { current: Element | null } | undefined;
|
|
54
|
+
getHandleCloseContext?: (() => HandleCloseContextBase | null) | undefined;
|
|
55
|
+
isClosing?: (() => boolean) | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Called before each hover-driven open attempt (immediate, delayed, and rest-ms paths).
|
|
58
|
+
* Return `false` to veto; any other return value permits the open.
|
|
59
|
+
*/
|
|
60
|
+
shouldOpen?: (() => boolean) | undefined;
|
|
9
61
|
}
|
|
10
62
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
63
|
+
const EMPTY_REF: { current: Element | null } = { current: null };
|
|
64
|
+
|
|
65
|
+
export function useHoverReferenceInteraction(...args: any[]): Record<string, any> | undefined {
|
|
66
|
+
const [user, slotArg] = splitSlot(args);
|
|
67
|
+
const slot = slotArg ?? S('useHoverReferenceInteraction');
|
|
68
|
+
const context = user[0] as any;
|
|
69
|
+
const props = (user[1] as UseHoverReferenceInteractionProps) ?? {};
|
|
70
|
+
|
|
71
|
+
const {
|
|
72
|
+
enabled = true,
|
|
73
|
+
delay = 0,
|
|
74
|
+
handleClose = null,
|
|
75
|
+
mouseOnly = false,
|
|
76
|
+
restMs = 0,
|
|
77
|
+
move = true,
|
|
78
|
+
triggerElementRef = EMPTY_REF,
|
|
79
|
+
externalTree,
|
|
80
|
+
isActiveTrigger = true,
|
|
81
|
+
getHandleCloseContext,
|
|
82
|
+
isClosing,
|
|
83
|
+
shouldOpen: shouldOpenProp,
|
|
84
|
+
} = props;
|
|
85
|
+
|
|
86
|
+
const store = context != null && 'rootStore' in context ? context.rootStore : context;
|
|
87
|
+
const dataRef = store?.context.dataRef;
|
|
88
|
+
const events = store?.context.events;
|
|
89
|
+
|
|
90
|
+
const tree = useFloatingTree(externalTree);
|
|
91
|
+
|
|
92
|
+
const instance = useHoverInteractionSharedState(store, subSlot(slot, 'shared'));
|
|
93
|
+
const isHoverCloseActiveRef = useRef(false, subSlot(slot, 'hca'));
|
|
94
|
+
|
|
95
|
+
const handleCloseRef = useValueAsRef<HandleClose | null>(handleClose, subSlot(slot, 'hc'));
|
|
96
|
+
const delayRef = useValueAsRef<HoverDelay | (() => HoverDelay)>(delay, subSlot(slot, 'delay'));
|
|
97
|
+
const restMsRef = useValueAsRef<number | (() => number)>(restMs, subSlot(slot, 'rest'));
|
|
98
|
+
const enabledRef = useValueAsRef<boolean>(enabled, subSlot(slot, 'en'));
|
|
99
|
+
const shouldOpenRef = useValueAsRef<(() => boolean) | undefined>(
|
|
100
|
+
shouldOpenProp,
|
|
101
|
+
subSlot(slot, 'so'),
|
|
102
|
+
);
|
|
103
|
+
const isClosingRef = useValueAsRef<(() => boolean) | undefined>(isClosing, subSlot(slot, 'cl'));
|
|
104
|
+
|
|
105
|
+
const isClickLikeOpenEvent = useStableCallback(
|
|
106
|
+
() => {
|
|
107
|
+
return isClickLikeOpenEventShared(dataRef.current.openEvent?.type, instance.interactedInside);
|
|
108
|
+
},
|
|
109
|
+
subSlot(slot, 'iclo'),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const checkShouldOpen = useStableCallback(
|
|
113
|
+
() => {
|
|
114
|
+
return shouldOpenRef.current?.() !== false;
|
|
115
|
+
},
|
|
116
|
+
subSlot(slot, 'cso'),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const isOverInactiveTrigger = useStableCallback(
|
|
120
|
+
(
|
|
121
|
+
currentDomReference: Element | null,
|
|
122
|
+
currentTarget: Element,
|
|
123
|
+
target: EventTarget | null,
|
|
124
|
+
): boolean => {
|
|
125
|
+
const allTriggers = store.context.triggerElements;
|
|
126
|
+
|
|
127
|
+
// Fast path for normal usage where handlers are attached directly to triggers.
|
|
128
|
+
if (allTriggers.hasElement(currentTarget)) {
|
|
129
|
+
return !currentDomReference || !contains(currentDomReference, currentTarget);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Fallback for delegated/wrapper usage where currentTarget may be outside the map.
|
|
133
|
+
if (!isElement(target)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const targetElement = target as Element;
|
|
138
|
+
return (
|
|
139
|
+
allTriggers.hasMatchingElement((trigger: Element) => contains(trigger, targetElement)) &&
|
|
140
|
+
(!currentDomReference || !contains(currentDomReference, targetElement))
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
subSlot(slot, 'ooit'),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const cleanupMouseMoveHandler = useStableCallback(
|
|
147
|
+
() => {
|
|
148
|
+
if (!instance.handler) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const doc = ownerDocument(store.select('domReferenceElement'));
|
|
152
|
+
doc.removeEventListener('mousemove', instance.handler);
|
|
153
|
+
instance.handler = undefined;
|
|
154
|
+
},
|
|
155
|
+
subSlot(slot, 'cmmh'),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const clearPointerEvents = useStableCallback(
|
|
159
|
+
() => {
|
|
160
|
+
clearSafePolygonPointerEventsMutation(instance);
|
|
161
|
+
},
|
|
162
|
+
subSlot(slot, 'cpe'),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (isActiveTrigger) {
|
|
166
|
+
instance.handleCloseOptions = handleCloseRef.current?.__options;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
useEffect(() => cleanupMouseMoveHandler, [cleanupMouseMoveHandler], subSlot(slot, 'e:cleanup'));
|
|
170
|
+
|
|
171
|
+
// When closing before opening, clear the delay timeouts to cancel it from showing.
|
|
172
|
+
useEffect(
|
|
173
|
+
() => {
|
|
174
|
+
if (!enabled) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function onOpenChangeLocal(details: any) {
|
|
179
|
+
if (!details.open) {
|
|
180
|
+
isHoverCloseActiveRef.current = details.reason === REASONS.triggerHover;
|
|
181
|
+
cleanupMouseMoveHandler();
|
|
182
|
+
instance.openChangeTimeout.clear();
|
|
183
|
+
instance.restTimeout.clear();
|
|
184
|
+
instance.blockMouseMove = true;
|
|
185
|
+
instance.restTimeoutPending = false;
|
|
186
|
+
} else {
|
|
187
|
+
isHoverCloseActiveRef.current = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
events.on('openchange', onOpenChangeLocal);
|
|
192
|
+
return () => {
|
|
193
|
+
events.off('openchange', onOpenChangeLocal);
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
[enabled, events, instance, cleanupMouseMoveHandler],
|
|
197
|
+
subSlot(slot, 'e:openchange'),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
useEffect(
|
|
201
|
+
() => {
|
|
202
|
+
if (!enabled) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function closeWithDelay(event: MouseEvent, runElseBranch = true) {
|
|
207
|
+
const closeDelay = getDelay(delayRef.current, 'close', instance.pointerType);
|
|
208
|
+
if (closeDelay) {
|
|
209
|
+
instance.openChangeTimeout.start(closeDelay, () => {
|
|
210
|
+
store.setOpen(false, createChangeEventDetails(REASONS.triggerHover, event));
|
|
211
|
+
tree?.events.emit('floating.closed', event);
|
|
212
|
+
});
|
|
213
|
+
} else if (runElseBranch) {
|
|
214
|
+
instance.openChangeTimeout.clear();
|
|
215
|
+
store.setOpen(false, createChangeEventDetails(REASONS.triggerHover, event));
|
|
216
|
+
tree?.events.emit('floating.closed', event);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const trigger =
|
|
221
|
+
(triggerElementRef.current as HTMLElement | null) ??
|
|
222
|
+
(isActiveTrigger ? (store.select('domReferenceElement') as HTMLElement | null) : null);
|
|
223
|
+
|
|
224
|
+
if (!isElement(trigger)) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function onMouseEnter(event: MouseEvent) {
|
|
229
|
+
instance.openChangeTimeout.clear();
|
|
230
|
+
instance.blockMouseMove = false;
|
|
231
|
+
|
|
232
|
+
if (mouseOnly && !isMouseLikePointerType(instance.pointerType)) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Only a rest delay is set; there's no fallback delay. `onMouseMove` handles it.
|
|
237
|
+
const restMsValue = getRestMs(restMsRef.current);
|
|
238
|
+
const openDelay = getDelay(delayRef.current, 'open', instance.pointerType);
|
|
239
|
+
const eventTarget = getTarget(event);
|
|
240
|
+
const currentTarget = (event.currentTarget as HTMLElement) ?? null;
|
|
241
|
+
const currentDomReference = store.select('domReferenceElement');
|
|
242
|
+
let triggerNode = currentTarget;
|
|
243
|
+
|
|
244
|
+
// Wrapper/delegated mode: resolve the actual trigger from the event target.
|
|
245
|
+
if (isElement(eventTarget) && !store.context.triggerElements.hasElement(eventTarget)) {
|
|
246
|
+
for (const triggerElement of store.context.triggerElements.elements()) {
|
|
247
|
+
if (contains(triggerElement, eventTarget)) {
|
|
248
|
+
triggerNode = triggerElement as HTMLElement;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Wrapper/delegated fallback: if the wrapper contains the active trigger, treat this
|
|
255
|
+
// as re-entering that active trigger.
|
|
256
|
+
if (
|
|
257
|
+
isElement(currentTarget) &&
|
|
258
|
+
isElement(currentDomReference) &&
|
|
259
|
+
!store.context.triggerElements.hasElement(currentTarget) &&
|
|
260
|
+
contains(currentTarget, currentDomReference)
|
|
261
|
+
) {
|
|
262
|
+
triggerNode = currentDomReference as HTMLElement;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const isOverInactive =
|
|
266
|
+
triggerNode == null
|
|
267
|
+
? false
|
|
268
|
+
: isOverInactiveTrigger(currentDomReference, triggerNode, eventTarget);
|
|
269
|
+
const isOpen = store.select('open');
|
|
270
|
+
const isInClosingTransition =
|
|
271
|
+
isClosingRef.current?.() ?? store.select('transitionStatus') === 'ending';
|
|
272
|
+
const isHoverCloseTransition =
|
|
273
|
+
!isOpen && isInClosingTransition && isHoverCloseActiveRef.current;
|
|
274
|
+
const isReenteringSameTriggerDuringCloseTransition =
|
|
275
|
+
!isOverInactive &&
|
|
276
|
+
isElement(triggerNode) &&
|
|
277
|
+
isElement(currentDomReference) &&
|
|
278
|
+
contains(currentDomReference, triggerNode) &&
|
|
279
|
+
isHoverCloseTransition;
|
|
280
|
+
const isRestOnlyDelay = restMsValue > 0 && !openDelay;
|
|
281
|
+
const shouldOpenImmediately =
|
|
282
|
+
(isOverInactive && (isOpen || isHoverCloseTransition)) ||
|
|
283
|
+
isReenteringSameTriggerDuringCloseTransition;
|
|
284
|
+
|
|
285
|
+
const shouldOpen = !isOpen || isOverInactive;
|
|
286
|
+
|
|
287
|
+
// Open immediately when moving between triggers while open, or during a hover-driven
|
|
288
|
+
// close transition (including same-trigger re-entry).
|
|
289
|
+
if (shouldOpenImmediately) {
|
|
290
|
+
if (checkShouldOpen()) {
|
|
291
|
+
store.setOpen(true, createChangeEventDetails(REASONS.triggerHover, event, triggerNode));
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (isRestOnlyDelay) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (openDelay) {
|
|
301
|
+
instance.openChangeTimeout.start(openDelay, () => {
|
|
302
|
+
if (shouldOpen && checkShouldOpen()) {
|
|
303
|
+
store.setOpen(
|
|
304
|
+
true,
|
|
305
|
+
createChangeEventDetails(REASONS.triggerHover, event, triggerNode),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
} else if (shouldOpen) {
|
|
310
|
+
if (checkShouldOpen()) {
|
|
311
|
+
store.setOpen(true, createChangeEventDetails(REASONS.triggerHover, event, triggerNode));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function onMouseLeave(event: MouseEvent) {
|
|
317
|
+
if (isClickLikeOpenEvent()) {
|
|
318
|
+
clearPointerEvents();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
cleanupMouseMoveHandler();
|
|
323
|
+
|
|
324
|
+
const domReferenceElement = store.select('domReferenceElement');
|
|
325
|
+
const doc = ownerDocument(domReferenceElement);
|
|
326
|
+
instance.restTimeout.clear();
|
|
327
|
+
instance.restTimeoutPending = false;
|
|
328
|
+
|
|
329
|
+
const handleCloseContextBase = dataRef.current.floatingContext ?? getHandleCloseContext?.();
|
|
330
|
+
|
|
331
|
+
if (isInsideEnabledTrigger(event.relatedTarget, store.context.triggerElements)) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (handleCloseRef.current && handleCloseContextBase) {
|
|
336
|
+
if (!store.select('open')) {
|
|
337
|
+
instance.openChangeTimeout.clear();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const currentTrigger = triggerElementRef.current;
|
|
341
|
+
|
|
342
|
+
instance.handler = handleCloseRef.current({
|
|
343
|
+
...handleCloseContextBase,
|
|
344
|
+
tree,
|
|
345
|
+
x: event.clientX,
|
|
346
|
+
y: event.clientY,
|
|
347
|
+
onClose() {
|
|
348
|
+
clearPointerEvents();
|
|
349
|
+
cleanupMouseMoveHandler();
|
|
350
|
+
if (
|
|
351
|
+
enabledRef.current &&
|
|
352
|
+
!isClickLikeOpenEvent() &&
|
|
353
|
+
currentTrigger === store.select('domReferenceElement')
|
|
354
|
+
) {
|
|
355
|
+
closeWithDelay(event, true);
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const activeHandler = instance.handler;
|
|
361
|
+
doc.addEventListener('mousemove', activeHandler as EventListener);
|
|
362
|
+
activeHandler(event);
|
|
363
|
+
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const shouldClose =
|
|
368
|
+
instance.pointerType === 'touch'
|
|
369
|
+
? !contains(store.select('floatingElement'), event.relatedTarget as Element | null)
|
|
370
|
+
: true;
|
|
371
|
+
|
|
372
|
+
if (shouldClose) {
|
|
373
|
+
closeWithDelay(event);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (move) {
|
|
378
|
+
return mergeCleanups(
|
|
379
|
+
addEventListener(trigger, 'mousemove', onMouseEnter as EventListener, { once: true }),
|
|
380
|
+
addEventListener(trigger, 'mouseenter', onMouseEnter as EventListener),
|
|
381
|
+
addEventListener(trigger, 'mouseleave', onMouseLeave as EventListener),
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return mergeCleanups(
|
|
386
|
+
addEventListener(trigger, 'mouseenter', onMouseEnter as EventListener),
|
|
387
|
+
addEventListener(trigger, 'mouseleave', onMouseLeave as EventListener),
|
|
388
|
+
);
|
|
389
|
+
},
|
|
390
|
+
[
|
|
391
|
+
cleanupMouseMoveHandler,
|
|
392
|
+
clearPointerEvents,
|
|
393
|
+
dataRef,
|
|
394
|
+
delayRef,
|
|
395
|
+
store,
|
|
396
|
+
enabled,
|
|
397
|
+
handleCloseRef,
|
|
398
|
+
instance,
|
|
399
|
+
isActiveTrigger,
|
|
400
|
+
isOverInactiveTrigger,
|
|
401
|
+
isClickLikeOpenEvent,
|
|
402
|
+
mouseOnly,
|
|
403
|
+
move,
|
|
404
|
+
restMsRef,
|
|
405
|
+
triggerElementRef,
|
|
406
|
+
tree,
|
|
407
|
+
enabledRef,
|
|
408
|
+
getHandleCloseContext,
|
|
409
|
+
isClosingRef,
|
|
410
|
+
checkShouldOpen,
|
|
411
|
+
],
|
|
412
|
+
subSlot(slot, 'e:listeners'),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
return useMemo(
|
|
416
|
+
() => {
|
|
417
|
+
if (!enabled) {
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function setPointerRef(event: PointerEvent) {
|
|
422
|
+
instance.pointerType = event.pointerType;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
onPointerDown: setPointerRef,
|
|
427
|
+
onPointerEnter: setPointerRef,
|
|
428
|
+
onMouseMove(event: MouseEvent) {
|
|
429
|
+
const trigger = event.currentTarget as HTMLElement;
|
|
430
|
+
|
|
431
|
+
const currentDomReference = store.select('domReferenceElement');
|
|
432
|
+
const currentOpen = store.select('open');
|
|
433
|
+
const isOverInactive = isOverInactiveTrigger(currentDomReference, trigger, event.target);
|
|
434
|
+
|
|
435
|
+
if (mouseOnly && !isMouseLikePointerType(instance.pointerType)) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (currentOpen && isOverInactive && instance.handleCloseOptions?.blockPointerEvents) {
|
|
440
|
+
const floatingElement = store.select('floatingElement');
|
|
441
|
+
|
|
442
|
+
if (floatingElement) {
|
|
443
|
+
const scopeElement =
|
|
444
|
+
instance.handleCloseOptions?.getScope?.() ?? trigger.ownerDocument.body;
|
|
445
|
+
|
|
446
|
+
applySafePolygonPointerEventsMutation(instance, {
|
|
447
|
+
scopeElement,
|
|
448
|
+
referenceElement: trigger,
|
|
449
|
+
floatingElement,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const restMsValue = getRestMs(restMsRef.current);
|
|
455
|
+
if ((currentOpen && !isOverInactive) || restMsValue === 0) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (
|
|
460
|
+
!isOverInactive &&
|
|
461
|
+
instance.restTimeoutPending &&
|
|
462
|
+
event.movementX ** 2 + event.movementY ** 2 < 2
|
|
463
|
+
) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
instance.restTimeout.clear();
|
|
468
|
+
|
|
469
|
+
function handleMouseMove() {
|
|
470
|
+
instance.restTimeoutPending = false;
|
|
471
|
+
|
|
472
|
+
// A delayed hover open must not override a click-like open that happened while
|
|
473
|
+
// the hover delay was pending.
|
|
474
|
+
if (isClickLikeOpenEvent()) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const latestOpen = store.select('open');
|
|
479
|
+
|
|
480
|
+
if (!instance.blockMouseMove && (!latestOpen || isOverInactive) && checkShouldOpen()) {
|
|
481
|
+
store.setOpen(true, createChangeEventDetails(REASONS.triggerHover, event, trigger));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (instance.pointerType === 'touch') {
|
|
486
|
+
flushSync(() => {
|
|
487
|
+
handleMouseMove();
|
|
488
|
+
});
|
|
489
|
+
} else if (isOverInactive && currentOpen) {
|
|
490
|
+
handleMouseMove();
|
|
491
|
+
} else {
|
|
492
|
+
instance.restTimeoutPending = true;
|
|
493
|
+
instance.restTimeout.start(restMsValue, handleMouseMove);
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
},
|
|
498
|
+
[
|
|
499
|
+
enabled,
|
|
500
|
+
instance,
|
|
501
|
+
isClickLikeOpenEvent,
|
|
502
|
+
isOverInactiveTrigger,
|
|
503
|
+
mouseOnly,
|
|
504
|
+
store,
|
|
505
|
+
restMsRef,
|
|
506
|
+
checkShouldOpen,
|
|
507
|
+
],
|
|
508
|
+
subSlot(slot, 'props'),
|
|
509
|
+
);
|
|
17
510
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/floating-ui-react/hooks/useHoverShared.ts (v1.6.0).
|
|
2
|
+
// Pure helpers shared by the reference-side and floating-side hover interactions: delay
|
|
3
|
+
// resolution (a hover delay may be a number, a {open, close} pair, or a getter), and the
|
|
4
|
+
// open-event classification that decides whether a hover close is allowed to run.
|
|
5
|
+
import { isMouseLikePointerType } from './event';
|
|
6
|
+
import type { PopupTriggerMap } from '../popups/popupTriggerMap';
|
|
7
|
+
|
|
8
|
+
export { isTargetInsideEnabledTrigger as isInsideEnabledTrigger } from './element';
|
|
9
|
+
export type { PopupTriggerMap };
|
|
10
|
+
|
|
11
|
+
export interface HandleCloseOptions {
|
|
12
|
+
blockPointerEvents?: boolean | undefined;
|
|
13
|
+
getScope?: (() => HTMLElement | SVGSVGElement | null) | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface HandleCloseContext {
|
|
17
|
+
x: number | null;
|
|
18
|
+
y: number | null;
|
|
19
|
+
placement: string | null;
|
|
20
|
+
elements: { domReference: Element | null; floating: HTMLElement | null };
|
|
21
|
+
onClose: () => void;
|
|
22
|
+
nodeId?: string | undefined;
|
|
23
|
+
tree?: any;
|
|
24
|
+
leave?: boolean | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type HandleCloseContextBase = Omit<HandleCloseContext, 'onClose' | 'tree' | 'x' | 'y'>;
|
|
28
|
+
|
|
29
|
+
export interface HandleClose {
|
|
30
|
+
(context: HandleCloseContext): (event: MouseEvent) => void;
|
|
31
|
+
__options?: HandleCloseOptions | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type HoverDelay = number | Partial<{ open: number; close: number }>;
|
|
35
|
+
|
|
36
|
+
function resolveValue<T>(
|
|
37
|
+
value: T | (() => T) | undefined,
|
|
38
|
+
pointerType?: string | undefined,
|
|
39
|
+
): T | 0 | undefined {
|
|
40
|
+
// Touch and pen never wait: a hover delay only makes sense for a cursor.
|
|
41
|
+
if (pointerType != null && !isMouseLikePointerType(pointerType)) {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
if (typeof value === 'function') {
|
|
45
|
+
return (value as () => T)();
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getDelay(
|
|
51
|
+
value: HoverDelay | (() => HoverDelay) | undefined,
|
|
52
|
+
prop: 'open' | 'close',
|
|
53
|
+
pointerType?: string | undefined,
|
|
54
|
+
): number | undefined {
|
|
55
|
+
const result = resolveValue(value, pointerType);
|
|
56
|
+
if (typeof result === 'number') {
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
return (result as Partial<{ open: number; close: number }> | undefined)?.[prop];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getRestMs(value: number | (() => number)): number {
|
|
63
|
+
if (typeof value === 'function') {
|
|
64
|
+
return value();
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isClickLikeOpenEvent(
|
|
70
|
+
openEventType: string | undefined,
|
|
71
|
+
interactedInside: boolean,
|
|
72
|
+
): boolean {
|
|
73
|
+
return interactedInside || openEventType === 'click' || openEventType === 'mousedown';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isHoverOpenEvent(openEventType: string | undefined): boolean {
|
|
77
|
+
return Boolean(openEventType?.includes('mouse')) && openEventType !== 'mousedown';
|
|
78
|
+
}
|