@octanejs/floating-ui 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/LICENSE +21 -0
- package/package.json +44 -0
- package/src/Composite.ts +241 -0
- package/src/FloatingArrow.ts +145 -0
- package/src/FloatingFocusManager.ts +813 -0
- package/src/FloatingList.ts +137 -0
- package/src/FloatingOverlay.ts +85 -0
- package/src/FloatingPortal.ts +257 -0
- package/src/context.ts +181 -0
- package/src/delayGroup.ts +142 -0
- package/src/index.ts +79 -0
- package/src/internal.ts +46 -0
- package/src/pubsub.ts +19 -0
- package/src/safePolygon.ts +339 -0
- package/src/transitions.ts +146 -0
- package/src/tree.ts +85 -0
- package/src/useClick.ts +139 -0
- package/src/useClientPoint.ts +193 -0
- package/src/useDismiss.ts +379 -0
- package/src/useFloating.ts +220 -0
- package/src/useFocus.ts +154 -0
- package/src/useHover.ts +406 -0
- package/src/useId.ts +6 -0
- package/src/useInteractions.ts +94 -0
- package/src/useListNavigation.ts +721 -0
- package/src/useMergeRefs.ts +59 -0
- package/src/useRole.ts +106 -0
- package/src/useTypeahead.ts +168 -0
- package/src/utils/index.ts +656 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Ported from @floating-ui/react FloatingDelayGroup + useDelayGroup — share a single
|
|
2
|
+
// open/close delay across a group of floating elements. `.ts` component via
|
|
3
|
+
// createElement; useDelayGroup is a hook (resolves its own slot).
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
createElement,
|
|
7
|
+
useCallback,
|
|
8
|
+
useContext,
|
|
9
|
+
useMemo,
|
|
10
|
+
useReducer,
|
|
11
|
+
useRef,
|
|
12
|
+
} from 'octane';
|
|
13
|
+
|
|
14
|
+
import { S, splitSlot, subSlot } from './internal';
|
|
15
|
+
import { getDelay, useModernLayoutEffect } from './utils';
|
|
16
|
+
|
|
17
|
+
const NOOP = () => {};
|
|
18
|
+
export const FloatingDelayGroupContext = createContext<any>({
|
|
19
|
+
delay: 0,
|
|
20
|
+
initialDelay: 0,
|
|
21
|
+
timeoutMs: 0,
|
|
22
|
+
currentId: null,
|
|
23
|
+
setCurrentId: NOOP,
|
|
24
|
+
setState: NOOP,
|
|
25
|
+
isInstantPhase: false,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const useDelayGroupContext = () => useContext(FloatingDelayGroupContext);
|
|
29
|
+
|
|
30
|
+
export function FloatingDelayGroup(props: any): any {
|
|
31
|
+
const children = props.children;
|
|
32
|
+
const delay = props.delay;
|
|
33
|
+
const timeoutMs = props.timeoutMs ?? 0;
|
|
34
|
+
|
|
35
|
+
const [state, setState] = useReducer(
|
|
36
|
+
(prev: any, next: any) => ({ ...prev, ...next }),
|
|
37
|
+
{
|
|
38
|
+
delay,
|
|
39
|
+
timeoutMs,
|
|
40
|
+
initialDelay: delay,
|
|
41
|
+
currentId: null,
|
|
42
|
+
isInstantPhase: false,
|
|
43
|
+
},
|
|
44
|
+
S('FloatingDelayGroup:state'),
|
|
45
|
+
);
|
|
46
|
+
const initialCurrentIdRef = useRef<any>(null, S('FloatingDelayGroup:initialId'));
|
|
47
|
+
const setCurrentId = useCallback(
|
|
48
|
+
(currentId: any) => {
|
|
49
|
+
setState({ currentId });
|
|
50
|
+
},
|
|
51
|
+
[],
|
|
52
|
+
S('FloatingDelayGroup:setId'),
|
|
53
|
+
);
|
|
54
|
+
useModernLayoutEffect(
|
|
55
|
+
() => {
|
|
56
|
+
if (state.currentId) {
|
|
57
|
+
if (initialCurrentIdRef.current === null) {
|
|
58
|
+
initialCurrentIdRef.current = state.currentId;
|
|
59
|
+
} else if (!state.isInstantPhase) {
|
|
60
|
+
setState({ isInstantPhase: true });
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
if (state.isInstantPhase) {
|
|
64
|
+
setState({ isInstantPhase: false });
|
|
65
|
+
}
|
|
66
|
+
initialCurrentIdRef.current = null;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[state.currentId, state.isInstantPhase],
|
|
70
|
+
S('FloatingDelayGroup:eff'),
|
|
71
|
+
);
|
|
72
|
+
const value = useMemo(
|
|
73
|
+
() => ({ ...state, setState, setCurrentId }),
|
|
74
|
+
[state, setCurrentId],
|
|
75
|
+
S('FloatingDelayGroup:value'),
|
|
76
|
+
);
|
|
77
|
+
return createElement(FloatingDelayGroupContext.Provider, { value, children });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function useDelayGroup(...args: any[]): any {
|
|
81
|
+
const [user, slot] = splitSlot(args);
|
|
82
|
+
const context = user[0];
|
|
83
|
+
const options = (user[1] as any) ?? {};
|
|
84
|
+
|
|
85
|
+
const open = context.open;
|
|
86
|
+
const onOpenChange = context.onOpenChange;
|
|
87
|
+
const floatingId = context.floatingId;
|
|
88
|
+
const optionId = options.id;
|
|
89
|
+
const enabled = options.enabled ?? true;
|
|
90
|
+
const id = optionId != null ? optionId : floatingId;
|
|
91
|
+
const groupContext = useDelayGroupContext();
|
|
92
|
+
const { currentId, setCurrentId, initialDelay, setState, timeoutMs } = groupContext;
|
|
93
|
+
|
|
94
|
+
useModernLayoutEffect(
|
|
95
|
+
() => {
|
|
96
|
+
if (!enabled) return;
|
|
97
|
+
if (!currentId) return;
|
|
98
|
+
setState({
|
|
99
|
+
delay: { open: 1, close: getDelay(initialDelay, 'close') },
|
|
100
|
+
});
|
|
101
|
+
if (currentId !== id) {
|
|
102
|
+
onOpenChange(false);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
[enabled, id, onOpenChange, setState, currentId, initialDelay],
|
|
106
|
+
subSlot(slot, 'e:sync'),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
useModernLayoutEffect(
|
|
110
|
+
() => {
|
|
111
|
+
function unset() {
|
|
112
|
+
onOpenChange(false);
|
|
113
|
+
setState({ delay: initialDelay, currentId: null });
|
|
114
|
+
}
|
|
115
|
+
if (!enabled) return;
|
|
116
|
+
if (!currentId) return;
|
|
117
|
+
if (!open && currentId === id) {
|
|
118
|
+
if (timeoutMs) {
|
|
119
|
+
const timeout = window.setTimeout(unset, timeoutMs);
|
|
120
|
+
return () => {
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
unset();
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
[enabled, open, setState, currentId, id, onOpenChange, initialDelay, timeoutMs],
|
|
128
|
+
subSlot(slot, 'e:unset'),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
useModernLayoutEffect(
|
|
132
|
+
() => {
|
|
133
|
+
if (!enabled) return;
|
|
134
|
+
if (setCurrentId === NOOP || !open) return;
|
|
135
|
+
setCurrentId(id);
|
|
136
|
+
},
|
|
137
|
+
[enabled, open, setCurrentId, id],
|
|
138
|
+
subSlot(slot, 'e:set'),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return groupContext;
|
|
142
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @octanejs/floating-ui — a port of @floating-ui/react on top of the agnostic
|
|
2
|
+
// @floating-ui/dom. PHASE 1 (positioning) is in place: useFloating, useMergeRefs,
|
|
3
|
+
// the ref-aware `arrow`, and the re-exported @floating-ui/dom middleware. The
|
|
4
|
+
// interaction hooks (useInteractions/useHover/useClick/useDismiss/useFocus/useRole/
|
|
5
|
+
// useListNavigation/useTypeahead/useClientPoint), the components (FloatingPortal/
|
|
6
|
+
// Overlay/FocusManager/Arrow/List/Tree, Composite), and transitions land in
|
|
7
|
+
// subsequent phases.
|
|
8
|
+
|
|
9
|
+
// Agnostic positioning core, re-exported from @floating-ui/dom.
|
|
10
|
+
export {
|
|
11
|
+
autoPlacement,
|
|
12
|
+
autoUpdate,
|
|
13
|
+
computePosition,
|
|
14
|
+
detectOverflow,
|
|
15
|
+
flip,
|
|
16
|
+
getOverflowAncestors,
|
|
17
|
+
hide,
|
|
18
|
+
inline,
|
|
19
|
+
limitShift,
|
|
20
|
+
offset,
|
|
21
|
+
platform,
|
|
22
|
+
shift,
|
|
23
|
+
size,
|
|
24
|
+
} from '@floating-ui/dom';
|
|
25
|
+
|
|
26
|
+
// Positioning + context. The public `useFloating` (from ./context) wraps the
|
|
27
|
+
// positioning core and returns the interaction `context`.
|
|
28
|
+
export { useFloating, useFloatingRootContext, createPubSub } from './context';
|
|
29
|
+
export { arrow } from './useFloating';
|
|
30
|
+
// The bare positioning core (the @floating-ui/react-dom `useFloating` shape, no
|
|
31
|
+
// interaction context) — consumed by bindings that only position (e.g. @octanejs/radix's
|
|
32
|
+
// Popper). Call as `usePositionFloating([options, slot])`.
|
|
33
|
+
export { usePositionFloating } from './useFloating';
|
|
34
|
+
export { useMergeRefs } from './useMergeRefs';
|
|
35
|
+
export { useId } from './useId';
|
|
36
|
+
export {
|
|
37
|
+
useFloatingTree,
|
|
38
|
+
useFloatingParentNodeId,
|
|
39
|
+
useFloatingNodeId,
|
|
40
|
+
FloatingTree,
|
|
41
|
+
FloatingNode,
|
|
42
|
+
FloatingNodeContext,
|
|
43
|
+
FloatingTreeContext,
|
|
44
|
+
} from './tree';
|
|
45
|
+
|
|
46
|
+
// Interaction hooks (phase 2 — in progress).
|
|
47
|
+
export { useInteractions } from './useInteractions';
|
|
48
|
+
export { useRole } from './useRole';
|
|
49
|
+
export { useClick } from './useClick';
|
|
50
|
+
export { useFocus } from './useFocus';
|
|
51
|
+
export { useDismiss } from './useDismiss';
|
|
52
|
+
export { useClientPoint } from './useClientPoint';
|
|
53
|
+
export { useListNavigation } from './useListNavigation';
|
|
54
|
+
export { useTypeahead } from './useTypeahead';
|
|
55
|
+
export { useHover } from './useHover';
|
|
56
|
+
export { safePolygon } from './safePolygon';
|
|
57
|
+
|
|
58
|
+
// Components (phase 3 — in progress).
|
|
59
|
+
export { FloatingOverlay } from './FloatingOverlay';
|
|
60
|
+
export {
|
|
61
|
+
FloatingPortal,
|
|
62
|
+
FocusGuard,
|
|
63
|
+
useFloatingPortalNode,
|
|
64
|
+
usePortalContext,
|
|
65
|
+
PortalContext,
|
|
66
|
+
} from './FloatingPortal';
|
|
67
|
+
export { FloatingList, useListItem, FloatingListContext } from './FloatingList';
|
|
68
|
+
export { FloatingArrow } from './FloatingArrow';
|
|
69
|
+
export { Composite, CompositeItem, CompositeContext } from './Composite';
|
|
70
|
+
export { FloatingFocusManager, VisuallyHiddenDismiss } from './FloatingFocusManager';
|
|
71
|
+
|
|
72
|
+
// Transitions + delay group (phase 4).
|
|
73
|
+
export { useTransitionStatus, useTransitionStyles } from './transitions';
|
|
74
|
+
export {
|
|
75
|
+
FloatingDelayGroup,
|
|
76
|
+
useDelayGroup,
|
|
77
|
+
useDelayGroupContext,
|
|
78
|
+
FloatingDelayGroupContext,
|
|
79
|
+
} from './delayGroup';
|
package/src/internal.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Slot mechanics for @octanejs/floating-ui's plain-`.ts` hooks. octane's compiler
|
|
2
|
+
// injects a per-call-site Symbol slot as the trailing arg of every hook call in a
|
|
3
|
+
// compiled `.tsx`/`.tsrx`. These hook files are published source (consumed from
|
|
4
|
+
// node_modules, where the auto-slotting pass is skipped), so each hook receives the
|
|
5
|
+
// caller's slot as its trailing argument and derives a distinct sub-slot for every
|
|
6
|
+
// base hook it composes. The floating `context` threads the root slot to the
|
|
7
|
+
// interaction hooks so they need no slot of their own. (Same pattern as the other
|
|
8
|
+
// @octanejs bindings.)
|
|
9
|
+
|
|
10
|
+
// Derive a stable, distinct sub-slot from a wrapper's slot, namespaced per hook.
|
|
11
|
+
// Memoized: subSlot runs on EVERY hook call every render, and the naive form
|
|
12
|
+
// pays a string concat + global symbol-registry lookup each time. The cache is
|
|
13
|
+
// keyed by the slot symbol itself; the minted value is byte-identical to the
|
|
14
|
+
// uncached Symbol.for result, so identity is preserved across HMR re-evals and
|
|
15
|
+
// the per-package copies of this helper. Key universe is bounded: slots are
|
|
16
|
+
// per-call-site module constants (never minted per render).
|
|
17
|
+
const subSlotCache = new Map<symbol, Map<string, symbol>>();
|
|
18
|
+
export function subSlot(slot: symbol | undefined, tag: string): symbol | undefined {
|
|
19
|
+
if (slot === undefined) return undefined;
|
|
20
|
+
let byTag = subSlotCache.get(slot);
|
|
21
|
+
if (byTag === undefined) subSlotCache.set(slot, (byTag = new Map()));
|
|
22
|
+
let sym = byTag.get(tag);
|
|
23
|
+
if (sym === undefined) byTag.set(tag, (sym = Symbol.for((slot.description ?? '') + ':' + tag)));
|
|
24
|
+
return sym;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Split the compiler-injected trailing slot off a hook's args. Needed because the
|
|
28
|
+
// public hooks take optional args, so the slot can't be located positionally.
|
|
29
|
+
export function splitSlot(args: any[]): [any[], symbol | undefined] {
|
|
30
|
+
const tail = args[args.length - 1];
|
|
31
|
+
const slot = typeof tail === 'symbol' ? (tail as symbol) : undefined;
|
|
32
|
+
return [slot !== undefined ? args.slice(0, -1) : args, slot];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A stable per-call-site slot for the binding's plain-`.ts` COMPONENTS (written
|
|
36
|
+
// with createElement, not compiled, so they get no auto-injected slots). A
|
|
37
|
+
// component runs in its OWN per-instance scope (componentSlot), so a globally
|
|
38
|
+
// stable Symbol.for(tag) resolves to a distinct slot per instance. Pass S('tag')
|
|
39
|
+
// as the slot to base hooks; sub-hooks derive their own sub-slots from its
|
|
40
|
+
// description.
|
|
41
|
+
const sCache = new Map<string, symbol>();
|
|
42
|
+
export function S(tag: string): symbol {
|
|
43
|
+
let sym = sCache.get(tag);
|
|
44
|
+
if (sym === undefined) sCache.set(tag, (sym = Symbol.for('@octanejs/floating-ui:' + tag)));
|
|
45
|
+
return sym;
|
|
46
|
+
}
|
package/src/pubsub.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The tiny event emitter shared by useFloatingRootContext and FloatingTree. Kept
|
|
2
|
+
// in its own module so context.ts and tree.ts don't form an import cycle.
|
|
3
|
+
export function createPubSub(): any {
|
|
4
|
+
const map = new Map<string, Set<(data: any) => void>>();
|
|
5
|
+
return {
|
|
6
|
+
emit(event: string, data: any) {
|
|
7
|
+
map.get(event)?.forEach((listener) => listener(data));
|
|
8
|
+
},
|
|
9
|
+
on(event: string, listener: (data: any) => void) {
|
|
10
|
+
if (!map.has(event)) {
|
|
11
|
+
map.set(event, new Set());
|
|
12
|
+
}
|
|
13
|
+
map.get(event)!.add(listener);
|
|
14
|
+
},
|
|
15
|
+
off(event: string, listener: (data: any) => void) {
|
|
16
|
+
map.get(event)?.delete(listener);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// Ported from @floating-ui/react safePolygon — the "safe triangle" close handler
|
|
2
|
+
// that keeps a hover floating element open while the cursor traverses toward it.
|
|
3
|
+
// Pure geometry; the only framework touchpoints are the shared dom helpers.
|
|
4
|
+
import { isElement } from '@floating-ui/utils/dom';
|
|
5
|
+
|
|
6
|
+
import { clearTimeoutIfSet, contains, getNodeChildren, getTarget } from './utils';
|
|
7
|
+
|
|
8
|
+
function isPointInPolygon(point: number[], polygon: number[][]): boolean {
|
|
9
|
+
const [x, y] = point;
|
|
10
|
+
let isInsideResult = false;
|
|
11
|
+
const length = polygon.length;
|
|
12
|
+
for (let i = 0, j = length - 1; i < length; j = i++) {
|
|
13
|
+
const [xi, yi] = polygon[i] || [0, 0];
|
|
14
|
+
const [xj, yj] = polygon[j] || [0, 0];
|
|
15
|
+
const intersect = yi >= y !== yj >= y && x <= ((xj - xi) * (y - yi)) / (yj - yi) + xi;
|
|
16
|
+
if (intersect) {
|
|
17
|
+
isInsideResult = !isInsideResult;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return isInsideResult;
|
|
21
|
+
}
|
|
22
|
+
function isInside(point: number[], rect: any): boolean {
|
|
23
|
+
return (
|
|
24
|
+
point[0] >= rect.x &&
|
|
25
|
+
point[0] <= rect.x + rect.width &&
|
|
26
|
+
point[1] >= rect.y &&
|
|
27
|
+
point[1] <= rect.y + rect.height
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function safePolygon(options: any = {}): any {
|
|
32
|
+
const { buffer = 0.5, blockPointerEvents = false, requireIntent = true } = options;
|
|
33
|
+
|
|
34
|
+
const timeoutRef = { current: -1 };
|
|
35
|
+
let hasLanded = false;
|
|
36
|
+
let lastX: number | null = null;
|
|
37
|
+
let lastY: number | null = null;
|
|
38
|
+
let lastCursorTime = typeof performance !== 'undefined' ? performance.now() : 0;
|
|
39
|
+
|
|
40
|
+
function getCursorSpeed(x: number, y: number): number | null {
|
|
41
|
+
const currentTime = performance.now();
|
|
42
|
+
const elapsedTime = currentTime - lastCursorTime;
|
|
43
|
+
if (lastX === null || lastY === null || elapsedTime === 0) {
|
|
44
|
+
lastX = x;
|
|
45
|
+
lastY = y;
|
|
46
|
+
lastCursorTime = currentTime;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const deltaX = x - lastX;
|
|
50
|
+
const deltaY = y - lastY;
|
|
51
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
52
|
+
const speed = distance / elapsedTime;
|
|
53
|
+
lastX = x;
|
|
54
|
+
lastY = y;
|
|
55
|
+
lastCursorTime = currentTime;
|
|
56
|
+
return speed;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fn = (_ref: any) => {
|
|
60
|
+
const { x, y, placement, elements, onClose, nodeId, tree } = _ref;
|
|
61
|
+
return function onMouseMove(event: any) {
|
|
62
|
+
function close() {
|
|
63
|
+
clearTimeoutIfSet(timeoutRef);
|
|
64
|
+
onClose();
|
|
65
|
+
}
|
|
66
|
+
clearTimeoutIfSet(timeoutRef);
|
|
67
|
+
if (
|
|
68
|
+
!elements.domReference ||
|
|
69
|
+
!elements.floating ||
|
|
70
|
+
placement == null ||
|
|
71
|
+
x == null ||
|
|
72
|
+
y == null
|
|
73
|
+
) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const { clientX, clientY } = event;
|
|
77
|
+
const clientPoint = [clientX, clientY];
|
|
78
|
+
const target = getTarget(event);
|
|
79
|
+
const isLeave = event.type === 'mouseleave';
|
|
80
|
+
const isOverFloatingEl = contains(elements.floating, target as any);
|
|
81
|
+
const isOverReferenceEl = contains(elements.domReference, target as any);
|
|
82
|
+
const refRect = elements.domReference.getBoundingClientRect();
|
|
83
|
+
const rect = elements.floating.getBoundingClientRect();
|
|
84
|
+
const side = placement.split('-')[0];
|
|
85
|
+
const cursorLeaveFromRight = x > rect.right - rect.width / 2;
|
|
86
|
+
const cursorLeaveFromBottom = y > rect.bottom - rect.height / 2;
|
|
87
|
+
const isOverReferenceRect = isInside(clientPoint, refRect);
|
|
88
|
+
const isFloatingWider = rect.width > refRect.width;
|
|
89
|
+
const isFloatingTaller = rect.height > refRect.height;
|
|
90
|
+
const left = (isFloatingWider ? refRect : rect).left;
|
|
91
|
+
const right = (isFloatingWider ? refRect : rect).right;
|
|
92
|
+
const top = (isFloatingTaller ? refRect : rect).top;
|
|
93
|
+
const bottom = (isFloatingTaller ? refRect : rect).bottom;
|
|
94
|
+
|
|
95
|
+
if (isOverFloatingEl) {
|
|
96
|
+
hasLanded = true;
|
|
97
|
+
if (!isLeave) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (isOverReferenceEl) {
|
|
102
|
+
hasLanded = false;
|
|
103
|
+
}
|
|
104
|
+
if (isOverReferenceEl && !isLeave) {
|
|
105
|
+
hasLanded = true;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (
|
|
109
|
+
isLeave &&
|
|
110
|
+
isElement(event.relatedTarget) &&
|
|
111
|
+
contains(elements.floating, event.relatedTarget)
|
|
112
|
+
) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (tree && getNodeChildren(tree.nodesRef.current, nodeId).length) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
(side === 'top' && y >= refRect.bottom - 1) ||
|
|
120
|
+
(side === 'bottom' && y <= refRect.top + 1) ||
|
|
121
|
+
(side === 'left' && x >= refRect.right - 1) ||
|
|
122
|
+
(side === 'right' && x <= refRect.left + 1)
|
|
123
|
+
) {
|
|
124
|
+
return close();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let rectPoly: number[][] = [];
|
|
128
|
+
switch (side) {
|
|
129
|
+
case 'top':
|
|
130
|
+
rectPoly = [
|
|
131
|
+
[left, refRect.top + 1],
|
|
132
|
+
[left, rect.bottom - 1],
|
|
133
|
+
[right, rect.bottom - 1],
|
|
134
|
+
[right, refRect.top + 1],
|
|
135
|
+
];
|
|
136
|
+
break;
|
|
137
|
+
case 'bottom':
|
|
138
|
+
rectPoly = [
|
|
139
|
+
[left, rect.top + 1],
|
|
140
|
+
[left, refRect.bottom - 1],
|
|
141
|
+
[right, refRect.bottom - 1],
|
|
142
|
+
[right, rect.top + 1],
|
|
143
|
+
];
|
|
144
|
+
break;
|
|
145
|
+
case 'left':
|
|
146
|
+
rectPoly = [
|
|
147
|
+
[rect.right - 1, bottom],
|
|
148
|
+
[rect.right - 1, top],
|
|
149
|
+
[refRect.left + 1, top],
|
|
150
|
+
[refRect.left + 1, bottom],
|
|
151
|
+
];
|
|
152
|
+
break;
|
|
153
|
+
case 'right':
|
|
154
|
+
rectPoly = [
|
|
155
|
+
[refRect.right - 1, bottom],
|
|
156
|
+
[refRect.right - 1, top],
|
|
157
|
+
[rect.left + 1, top],
|
|
158
|
+
[rect.left + 1, bottom],
|
|
159
|
+
];
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getPolygon([px, py]: number[]): number[][] {
|
|
164
|
+
switch (side) {
|
|
165
|
+
case 'top': {
|
|
166
|
+
const cursorPointOne = [
|
|
167
|
+
isFloatingWider
|
|
168
|
+
? px + buffer / 2
|
|
169
|
+
: cursorLeaveFromRight
|
|
170
|
+
? px + buffer * 4
|
|
171
|
+
: px - buffer * 4,
|
|
172
|
+
py + buffer + 1,
|
|
173
|
+
];
|
|
174
|
+
const cursorPointTwo = [
|
|
175
|
+
isFloatingWider
|
|
176
|
+
? px - buffer / 2
|
|
177
|
+
: cursorLeaveFromRight
|
|
178
|
+
? px + buffer * 4
|
|
179
|
+
: px - buffer * 4,
|
|
180
|
+
py + buffer + 1,
|
|
181
|
+
];
|
|
182
|
+
const commonPoints = [
|
|
183
|
+
[
|
|
184
|
+
rect.left,
|
|
185
|
+
cursorLeaveFromRight
|
|
186
|
+
? rect.bottom - buffer
|
|
187
|
+
: isFloatingWider
|
|
188
|
+
? rect.bottom - buffer
|
|
189
|
+
: rect.top,
|
|
190
|
+
],
|
|
191
|
+
[
|
|
192
|
+
rect.right,
|
|
193
|
+
cursorLeaveFromRight
|
|
194
|
+
? isFloatingWider
|
|
195
|
+
? rect.bottom - buffer
|
|
196
|
+
: rect.top
|
|
197
|
+
: rect.bottom - buffer,
|
|
198
|
+
],
|
|
199
|
+
];
|
|
200
|
+
return [cursorPointOne, cursorPointTwo, ...commonPoints];
|
|
201
|
+
}
|
|
202
|
+
case 'bottom': {
|
|
203
|
+
const cursorPointOne = [
|
|
204
|
+
isFloatingWider
|
|
205
|
+
? px + buffer / 2
|
|
206
|
+
: cursorLeaveFromRight
|
|
207
|
+
? px + buffer * 4
|
|
208
|
+
: px - buffer * 4,
|
|
209
|
+
py - buffer,
|
|
210
|
+
];
|
|
211
|
+
const cursorPointTwo = [
|
|
212
|
+
isFloatingWider
|
|
213
|
+
? px - buffer / 2
|
|
214
|
+
: cursorLeaveFromRight
|
|
215
|
+
? px + buffer * 4
|
|
216
|
+
: px - buffer * 4,
|
|
217
|
+
py - buffer,
|
|
218
|
+
];
|
|
219
|
+
const commonPoints = [
|
|
220
|
+
[
|
|
221
|
+
rect.left,
|
|
222
|
+
cursorLeaveFromRight
|
|
223
|
+
? rect.top + buffer
|
|
224
|
+
: isFloatingWider
|
|
225
|
+
? rect.top + buffer
|
|
226
|
+
: rect.bottom,
|
|
227
|
+
],
|
|
228
|
+
[
|
|
229
|
+
rect.right,
|
|
230
|
+
cursorLeaveFromRight
|
|
231
|
+
? isFloatingWider
|
|
232
|
+
? rect.top + buffer
|
|
233
|
+
: rect.bottom
|
|
234
|
+
: rect.top + buffer,
|
|
235
|
+
],
|
|
236
|
+
];
|
|
237
|
+
return [cursorPointOne, cursorPointTwo, ...commonPoints];
|
|
238
|
+
}
|
|
239
|
+
case 'left': {
|
|
240
|
+
const cursorPointOne = [
|
|
241
|
+
px + buffer + 1,
|
|
242
|
+
isFloatingTaller
|
|
243
|
+
? py + buffer / 2
|
|
244
|
+
: cursorLeaveFromBottom
|
|
245
|
+
? py + buffer * 4
|
|
246
|
+
: py - buffer * 4,
|
|
247
|
+
];
|
|
248
|
+
const cursorPointTwo = [
|
|
249
|
+
px + buffer + 1,
|
|
250
|
+
isFloatingTaller
|
|
251
|
+
? py - buffer / 2
|
|
252
|
+
: cursorLeaveFromBottom
|
|
253
|
+
? py + buffer * 4
|
|
254
|
+
: py - buffer * 4,
|
|
255
|
+
];
|
|
256
|
+
const commonPoints = [
|
|
257
|
+
[
|
|
258
|
+
cursorLeaveFromBottom
|
|
259
|
+
? rect.right - buffer
|
|
260
|
+
: isFloatingTaller
|
|
261
|
+
? rect.right - buffer
|
|
262
|
+
: rect.left,
|
|
263
|
+
rect.top,
|
|
264
|
+
],
|
|
265
|
+
[
|
|
266
|
+
cursorLeaveFromBottom
|
|
267
|
+
? isFloatingTaller
|
|
268
|
+
? rect.right - buffer
|
|
269
|
+
: rect.left
|
|
270
|
+
: rect.right - buffer,
|
|
271
|
+
rect.bottom,
|
|
272
|
+
],
|
|
273
|
+
];
|
|
274
|
+
return [...commonPoints, cursorPointOne, cursorPointTwo];
|
|
275
|
+
}
|
|
276
|
+
case 'right': {
|
|
277
|
+
const cursorPointOne = [
|
|
278
|
+
px - buffer,
|
|
279
|
+
isFloatingTaller
|
|
280
|
+
? py + buffer / 2
|
|
281
|
+
: cursorLeaveFromBottom
|
|
282
|
+
? py + buffer * 4
|
|
283
|
+
: py - buffer * 4,
|
|
284
|
+
];
|
|
285
|
+
const cursorPointTwo = [
|
|
286
|
+
px - buffer,
|
|
287
|
+
isFloatingTaller
|
|
288
|
+
? py - buffer / 2
|
|
289
|
+
: cursorLeaveFromBottom
|
|
290
|
+
? py + buffer * 4
|
|
291
|
+
: py - buffer * 4,
|
|
292
|
+
];
|
|
293
|
+
const commonPoints = [
|
|
294
|
+
[
|
|
295
|
+
cursorLeaveFromBottom
|
|
296
|
+
? rect.left + buffer
|
|
297
|
+
: isFloatingTaller
|
|
298
|
+
? rect.left + buffer
|
|
299
|
+
: rect.right,
|
|
300
|
+
rect.top,
|
|
301
|
+
],
|
|
302
|
+
[
|
|
303
|
+
cursorLeaveFromBottom
|
|
304
|
+
? isFloatingTaller
|
|
305
|
+
? rect.left + buffer
|
|
306
|
+
: rect.right
|
|
307
|
+
: rect.left + buffer,
|
|
308
|
+
rect.bottom,
|
|
309
|
+
],
|
|
310
|
+
];
|
|
311
|
+
return [cursorPointOne, cursorPointTwo, ...commonPoints];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (isPointInPolygon([clientX, clientY], rectPoly)) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (hasLanded && !isOverReferenceRect) {
|
|
321
|
+
return close();
|
|
322
|
+
}
|
|
323
|
+
if (!isLeave && requireIntent) {
|
|
324
|
+
const cursorSpeed = getCursorSpeed(event.clientX, event.clientY);
|
|
325
|
+
const cursorSpeedThreshold = 0.1;
|
|
326
|
+
if (cursorSpeed !== null && cursorSpeed < cursorSpeedThreshold) {
|
|
327
|
+
return close();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (!isPointInPolygon([clientX, clientY], getPolygon([x, y]))) {
|
|
331
|
+
close();
|
|
332
|
+
} else if (!hasLanded && requireIntent) {
|
|
333
|
+
timeoutRef.current = window.setTimeout(close, 40);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
fn.__options = { blockPointerEvents };
|
|
338
|
+
return fn;
|
|
339
|
+
}
|