@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,146 @@
|
|
|
1
|
+
// Ported from @floating-ui/react useTransitionStatus / useTransitionStyles (+ the
|
|
2
|
+
// internal useDelayUnmount) — placement-aware CSS-transition state/styles for a
|
|
3
|
+
// floating element. ReactDOM.flushSync → octane flushSync.
|
|
4
|
+
import { flushSync, useEffect, useMemo, useState } from 'octane';
|
|
5
|
+
|
|
6
|
+
import { splitSlot, subSlot } from './internal';
|
|
7
|
+
import {
|
|
8
|
+
camelCaseToKebabCase,
|
|
9
|
+
execWithArgsOrReturn,
|
|
10
|
+
useLatestRef,
|
|
11
|
+
useModernLayoutEffect,
|
|
12
|
+
} from './utils';
|
|
13
|
+
|
|
14
|
+
function useDelayUnmount(open: boolean, durationMs: number, slot: symbol | undefined): boolean {
|
|
15
|
+
const [isMounted, setIsMounted] = useState(open, subSlot(slot, 'mounted'));
|
|
16
|
+
if (open && !isMounted) {
|
|
17
|
+
setIsMounted(true);
|
|
18
|
+
}
|
|
19
|
+
useEffect(
|
|
20
|
+
() => {
|
|
21
|
+
if (!open && isMounted) {
|
|
22
|
+
const timeout = setTimeout(() => setIsMounted(false), durationMs);
|
|
23
|
+
return () => clearTimeout(timeout);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
[open, isMounted, durationMs],
|
|
27
|
+
subSlot(slot, 'eff'),
|
|
28
|
+
);
|
|
29
|
+
return isMounted;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useTransitionStatus(...args: any[]): any {
|
|
33
|
+
const [user, slot] = splitSlot(args);
|
|
34
|
+
const context = user[0];
|
|
35
|
+
const props = (user[1] as any) ?? {};
|
|
36
|
+
|
|
37
|
+
const open = context.open;
|
|
38
|
+
const floating = context.elements.floating;
|
|
39
|
+
const duration = props.duration ?? 250;
|
|
40
|
+
|
|
41
|
+
const isNumberDuration = typeof duration === 'number';
|
|
42
|
+
const closeDuration = (isNumberDuration ? duration : duration.close) || 0;
|
|
43
|
+
const [status, setStatus] = useState('unmounted', subSlot(slot, 'status'));
|
|
44
|
+
const isMounted = useDelayUnmount(open, closeDuration, subSlot(slot, 'unmount'));
|
|
45
|
+
if (!isMounted && status === 'close') {
|
|
46
|
+
setStatus('unmounted');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
useModernLayoutEffect(
|
|
50
|
+
() => {
|
|
51
|
+
if (!floating) return;
|
|
52
|
+
if (open) {
|
|
53
|
+
setStatus('initial');
|
|
54
|
+
const frame = requestAnimationFrame(() => {
|
|
55
|
+
// Ensure it opens before paint — with `FloatingDelayGroup`, this avoids
|
|
56
|
+
// a flicker when moving between floating elements.
|
|
57
|
+
flushSync(() => {
|
|
58
|
+
setStatus('open');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
return () => {
|
|
62
|
+
cancelAnimationFrame(frame);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
setStatus('close');
|
|
66
|
+
},
|
|
67
|
+
[open, floating],
|
|
68
|
+
subSlot(slot, 'eff'),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return { isMounted, status };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function useTransitionStyles(...args: any[]): any {
|
|
75
|
+
const [user, slot] = splitSlot(args);
|
|
76
|
+
const context = user[0];
|
|
77
|
+
const props = (user[1] as any) ?? {};
|
|
78
|
+
|
|
79
|
+
const unstableInitial = props.initial ?? { opacity: 0 };
|
|
80
|
+
const unstableOpen = props.open;
|
|
81
|
+
const unstableClose = props.close;
|
|
82
|
+
const unstableCommon = props.common;
|
|
83
|
+
const duration = props.duration ?? 250;
|
|
84
|
+
|
|
85
|
+
const placement = context.placement;
|
|
86
|
+
const side = placement.split('-')[0];
|
|
87
|
+
const fnArgs = useMemo(() => ({ side, placement }), [side, placement], subSlot(slot, 'args'));
|
|
88
|
+
const isNumberDuration = typeof duration === 'number';
|
|
89
|
+
const openDuration = (isNumberDuration ? duration : duration.open) || 0;
|
|
90
|
+
const closeDuration = (isNumberDuration ? duration : duration.close) || 0;
|
|
91
|
+
|
|
92
|
+
const [styles, setStyles] = useState(
|
|
93
|
+
() => ({
|
|
94
|
+
...execWithArgsOrReturn(unstableCommon, fnArgs),
|
|
95
|
+
...execWithArgsOrReturn(unstableInitial, fnArgs),
|
|
96
|
+
}),
|
|
97
|
+
subSlot(slot, 'styles'),
|
|
98
|
+
);
|
|
99
|
+
const { isMounted, status } = useTransitionStatus(context, { duration }, subSlot(slot, 'status'));
|
|
100
|
+
const initialRef = useLatestRef(unstableInitial, subSlot(slot, 'initialRef'));
|
|
101
|
+
const openRef = useLatestRef(unstableOpen, subSlot(slot, 'openRef'));
|
|
102
|
+
const closeRef = useLatestRef(unstableClose, subSlot(slot, 'closeRef'));
|
|
103
|
+
const commonRef = useLatestRef(unstableCommon, subSlot(slot, 'commonRef'));
|
|
104
|
+
|
|
105
|
+
useModernLayoutEffect(
|
|
106
|
+
() => {
|
|
107
|
+
const initialStyles = execWithArgsOrReturn(initialRef.current, fnArgs);
|
|
108
|
+
const closeStyles = execWithArgsOrReturn(closeRef.current, fnArgs);
|
|
109
|
+
const commonStyles = execWithArgsOrReturn(commonRef.current, fnArgs);
|
|
110
|
+
const openStyles =
|
|
111
|
+
execWithArgsOrReturn(openRef.current, fnArgs) ||
|
|
112
|
+
Object.keys(initialStyles).reduce((acc: any, key) => {
|
|
113
|
+
acc[key] = '';
|
|
114
|
+
return acc;
|
|
115
|
+
}, {});
|
|
116
|
+
if (status === 'initial') {
|
|
117
|
+
setStyles((s: any) => ({
|
|
118
|
+
transitionProperty: s.transitionProperty,
|
|
119
|
+
...commonStyles,
|
|
120
|
+
...initialStyles,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
if (status === 'open') {
|
|
124
|
+
setStyles({
|
|
125
|
+
transitionProperty: Object.keys(openStyles).map(camelCaseToKebabCase).join(','),
|
|
126
|
+
transitionDuration: openDuration + 'ms',
|
|
127
|
+
...commonStyles,
|
|
128
|
+
...openStyles,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (status === 'close') {
|
|
132
|
+
const s = closeStyles || initialStyles;
|
|
133
|
+
setStyles({
|
|
134
|
+
transitionProperty: Object.keys(s).map(camelCaseToKebabCase).join(','),
|
|
135
|
+
transitionDuration: closeDuration + 'ms',
|
|
136
|
+
...commonStyles,
|
|
137
|
+
...s,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
[closeDuration, closeRef, initialRef, openRef, commonRef, openDuration, status, fnArgs],
|
|
142
|
+
subSlot(slot, 'eff'),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return { isMounted, styles };
|
|
146
|
+
}
|
package/src/tree.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Ported from @floating-ui/react's FloatingTree / FloatingNode. The contexts + read
|
|
2
|
+
// hooks let useFloating / the interaction hooks query nesting; the components +
|
|
3
|
+
// useFloatingNodeId register nodes in the tree. `.ts` components via createElement.
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
createElement,
|
|
7
|
+
useCallback,
|
|
8
|
+
useContext,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from 'octane';
|
|
13
|
+
|
|
14
|
+
import { createPubSub } from './pubsub';
|
|
15
|
+
import { S, splitSlot, subSlot } from './internal';
|
|
16
|
+
import { useId } from './useId';
|
|
17
|
+
import { useModernLayoutEffect } from './utils';
|
|
18
|
+
|
|
19
|
+
export const FloatingNodeContext = createContext<any>(null);
|
|
20
|
+
export const FloatingTreeContext = createContext<any>(null);
|
|
21
|
+
|
|
22
|
+
export const useFloatingParentNodeId = (): string | null => {
|
|
23
|
+
const context = useContext(FloatingNodeContext);
|
|
24
|
+
return context ? context.id : null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const useFloatingTree = (): any => useContext(FloatingTreeContext);
|
|
28
|
+
|
|
29
|
+
export function useFloatingNodeId(...args: any[]): string {
|
|
30
|
+
const [user, slotArg] = splitSlot(args);
|
|
31
|
+
const slot = slotArg ?? S('useFloatingNodeId');
|
|
32
|
+
const customParentId = user[0];
|
|
33
|
+
|
|
34
|
+
const id = useId(subSlot(slot, 'id'));
|
|
35
|
+
const tree = useFloatingTree();
|
|
36
|
+
const reactParentId = useFloatingParentNodeId();
|
|
37
|
+
const parentId = customParentId || reactParentId;
|
|
38
|
+
useModernLayoutEffect(
|
|
39
|
+
() => {
|
|
40
|
+
if (!id) return;
|
|
41
|
+
const node = { id, parentId };
|
|
42
|
+
tree?.addNode(node);
|
|
43
|
+
return () => {
|
|
44
|
+
tree?.removeNode(node);
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
[tree, id, parentId],
|
|
48
|
+
subSlot(slot, 'eff'),
|
|
49
|
+
);
|
|
50
|
+
return id;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function FloatingNode(props: any): any {
|
|
54
|
+
const children = props.children;
|
|
55
|
+
const id = props.id;
|
|
56
|
+
const parentId = useFloatingParentNodeId();
|
|
57
|
+
const value = useMemo(() => ({ id, parentId }), [id, parentId], S('FloatingNode:value'));
|
|
58
|
+
return createElement(FloatingNodeContext.Provider, { value, children });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function FloatingTree(props: any): any {
|
|
62
|
+
const children = props.children;
|
|
63
|
+
const nodesRef = useRef<any[]>([], S('FloatingTree:nodes'));
|
|
64
|
+
const addNode = useCallback(
|
|
65
|
+
(node: any) => {
|
|
66
|
+
nodesRef.current = [...nodesRef.current, node];
|
|
67
|
+
},
|
|
68
|
+
[],
|
|
69
|
+
S('FloatingTree:add'),
|
|
70
|
+
);
|
|
71
|
+
const removeNode = useCallback(
|
|
72
|
+
(node: any) => {
|
|
73
|
+
nodesRef.current = nodesRef.current.filter((n) => n !== node);
|
|
74
|
+
},
|
|
75
|
+
[],
|
|
76
|
+
S('FloatingTree:remove'),
|
|
77
|
+
);
|
|
78
|
+
const [events] = useState(() => createPubSub(), S('FloatingTree:events'));
|
|
79
|
+
const value = useMemo(
|
|
80
|
+
() => ({ nodesRef, addNode, removeNode, events }),
|
|
81
|
+
[addNode, removeNode, events],
|
|
82
|
+
S('FloatingTree:value'),
|
|
83
|
+
);
|
|
84
|
+
return createElement(FloatingTreeContext.Provider, { value, children });
|
|
85
|
+
}
|
package/src/useClick.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Ported from @floating-ui/react useClick. octane events are NATIVE, so the React
|
|
2
|
+
// `event.nativeEvent` accesses become `event`.
|
|
3
|
+
import { isHTMLElement } from '@floating-ui/utils/dom';
|
|
4
|
+
import { useMemo, useRef } from 'octane';
|
|
5
|
+
|
|
6
|
+
import { splitSlot, subSlot } from './internal';
|
|
7
|
+
import { isMouseLikePointerType, isTypeableElement } from './utils';
|
|
8
|
+
|
|
9
|
+
function isButtonTarget(event: any): boolean {
|
|
10
|
+
return isHTMLElement(event.target) && event.target.tagName === 'BUTTON';
|
|
11
|
+
}
|
|
12
|
+
function isAnchorTarget(event: any): boolean {
|
|
13
|
+
return isHTMLElement(event.target) && event.target.tagName === 'A';
|
|
14
|
+
}
|
|
15
|
+
function isSpaceIgnored(element: any): boolean {
|
|
16
|
+
return isTypeableElement(element);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useClick(...args: any[]): any {
|
|
20
|
+
const [user, slot] = splitSlot(args);
|
|
21
|
+
const context = user[0];
|
|
22
|
+
const props = (user[1] as any) ?? {};
|
|
23
|
+
|
|
24
|
+
const open = context.open;
|
|
25
|
+
const onOpenChange = context.onOpenChange;
|
|
26
|
+
const dataRef = context.dataRef;
|
|
27
|
+
const domReference = context.elements.domReference;
|
|
28
|
+
|
|
29
|
+
const enabled = props.enabled ?? true;
|
|
30
|
+
const eventOption = props.event ?? 'click';
|
|
31
|
+
const toggle = props.toggle ?? true;
|
|
32
|
+
const ignoreMouse = props.ignoreMouse ?? false;
|
|
33
|
+
const keyboardHandlers = props.keyboardHandlers ?? true;
|
|
34
|
+
const stickIfOpen = props.stickIfOpen ?? true;
|
|
35
|
+
|
|
36
|
+
const pointerTypeRef = useRef<any>(undefined, subSlot(slot, 'ptype'));
|
|
37
|
+
const didKeyDownRef = useRef(false, subSlot(slot, 'kd'));
|
|
38
|
+
|
|
39
|
+
const reference = useMemo(
|
|
40
|
+
() => ({
|
|
41
|
+
onPointerDown(event: any) {
|
|
42
|
+
pointerTypeRef.current = event.pointerType;
|
|
43
|
+
},
|
|
44
|
+
onMouseDown(event: any) {
|
|
45
|
+
const pointerType = pointerTypeRef.current;
|
|
46
|
+
if (event.button !== 0) return;
|
|
47
|
+
if (eventOption === 'click') return;
|
|
48
|
+
if (isMouseLikePointerType(pointerType, true) && ignoreMouse) return;
|
|
49
|
+
if (
|
|
50
|
+
open &&
|
|
51
|
+
toggle &&
|
|
52
|
+
(dataRef.current.openEvent && stickIfOpen
|
|
53
|
+
? dataRef.current.openEvent.type === 'mousedown'
|
|
54
|
+
: true)
|
|
55
|
+
) {
|
|
56
|
+
onOpenChange(false, event, 'click');
|
|
57
|
+
} else {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
onOpenChange(true, event, 'click');
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
onClick(event: any) {
|
|
63
|
+
const pointerType = pointerTypeRef.current;
|
|
64
|
+
if (eventOption === 'mousedown' && pointerTypeRef.current) {
|
|
65
|
+
pointerTypeRef.current = undefined;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (isMouseLikePointerType(pointerType, true) && ignoreMouse) return;
|
|
69
|
+
if (
|
|
70
|
+
open &&
|
|
71
|
+
toggle &&
|
|
72
|
+
(dataRef.current.openEvent && stickIfOpen
|
|
73
|
+
? dataRef.current.openEvent.type === 'click'
|
|
74
|
+
: true)
|
|
75
|
+
) {
|
|
76
|
+
onOpenChange(false, event, 'click');
|
|
77
|
+
} else {
|
|
78
|
+
onOpenChange(true, event, 'click');
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
onKeyDown(event: any) {
|
|
82
|
+
pointerTypeRef.current = undefined;
|
|
83
|
+
if (event.defaultPrevented || !keyboardHandlers || isButtonTarget(event)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (event.key === ' ' && !isSpaceIgnored(domReference)) {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
didKeyDownRef.current = true;
|
|
89
|
+
}
|
|
90
|
+
if (isAnchorTarget(event)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (event.key === 'Enter') {
|
|
94
|
+
if (open && toggle) {
|
|
95
|
+
onOpenChange(false, event, 'click');
|
|
96
|
+
} else {
|
|
97
|
+
onOpenChange(true, event, 'click');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
onKeyUp(event: any) {
|
|
102
|
+
if (
|
|
103
|
+
event.defaultPrevented ||
|
|
104
|
+
!keyboardHandlers ||
|
|
105
|
+
isButtonTarget(event) ||
|
|
106
|
+
isSpaceIgnored(domReference)
|
|
107
|
+
) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (event.key === ' ' && didKeyDownRef.current) {
|
|
111
|
+
didKeyDownRef.current = false;
|
|
112
|
+
if (open && toggle) {
|
|
113
|
+
onOpenChange(false, event, 'click');
|
|
114
|
+
} else {
|
|
115
|
+
onOpenChange(true, event, 'click');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
[
|
|
121
|
+
dataRef,
|
|
122
|
+
domReference,
|
|
123
|
+
eventOption,
|
|
124
|
+
ignoreMouse,
|
|
125
|
+
keyboardHandlers,
|
|
126
|
+
onOpenChange,
|
|
127
|
+
open,
|
|
128
|
+
stickIfOpen,
|
|
129
|
+
toggle,
|
|
130
|
+
],
|
|
131
|
+
subSlot(slot, 'm:ref'),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return useMemo(
|
|
135
|
+
() => (enabled ? { reference } : {}),
|
|
136
|
+
[enabled, reference],
|
|
137
|
+
subSlot(slot, 'm:ret'),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Ported from @floating-ui/react useClientPoint — positions the floating element
|
|
2
|
+
// relative to a client point (e.g. the mouse). octane events are native.
|
|
3
|
+
import { getWindow } from '@floating-ui/utils/dom';
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'octane';
|
|
5
|
+
|
|
6
|
+
import { splitSlot, subSlot } from './internal';
|
|
7
|
+
import {
|
|
8
|
+
contains,
|
|
9
|
+
getTarget,
|
|
10
|
+
isMouseLikePointerType,
|
|
11
|
+
useEffectEvent,
|
|
12
|
+
useModernLayoutEffect,
|
|
13
|
+
} from './utils';
|
|
14
|
+
|
|
15
|
+
function createVirtualElement(domElement: any, data: any): any {
|
|
16
|
+
let offsetX: number | null = null;
|
|
17
|
+
let offsetY: number | null = null;
|
|
18
|
+
let isAutoUpdateEvent = false;
|
|
19
|
+
return {
|
|
20
|
+
contextElement: domElement || undefined,
|
|
21
|
+
getBoundingClientRect() {
|
|
22
|
+
const domRect = domElement?.getBoundingClientRect() || { width: 0, height: 0, x: 0, y: 0 };
|
|
23
|
+
const isXAxis = data.axis === 'x' || data.axis === 'both';
|
|
24
|
+
const isYAxis = data.axis === 'y' || data.axis === 'both';
|
|
25
|
+
const canTrackCursorOnAutoUpdate =
|
|
26
|
+
['mouseenter', 'mousemove'].includes(data.dataRef.current.openEvent?.type || '') &&
|
|
27
|
+
data.pointerType !== 'touch';
|
|
28
|
+
let width = domRect.width;
|
|
29
|
+
let height = domRect.height;
|
|
30
|
+
let x = domRect.x;
|
|
31
|
+
let y = domRect.y;
|
|
32
|
+
if (offsetX == null && data.x && isXAxis) {
|
|
33
|
+
offsetX = domRect.x - data.x;
|
|
34
|
+
}
|
|
35
|
+
if (offsetY == null && data.y && isYAxis) {
|
|
36
|
+
offsetY = domRect.y - data.y;
|
|
37
|
+
}
|
|
38
|
+
x -= offsetX || 0;
|
|
39
|
+
y -= offsetY || 0;
|
|
40
|
+
width = 0;
|
|
41
|
+
height = 0;
|
|
42
|
+
if (!isAutoUpdateEvent || canTrackCursorOnAutoUpdate) {
|
|
43
|
+
width = data.axis === 'y' ? domRect.width : 0;
|
|
44
|
+
height = data.axis === 'x' ? domRect.height : 0;
|
|
45
|
+
x = isXAxis && data.x != null ? data.x : x;
|
|
46
|
+
y = isYAxis && data.y != null ? data.y : y;
|
|
47
|
+
} else if (isAutoUpdateEvent && !canTrackCursorOnAutoUpdate) {
|
|
48
|
+
height = data.axis === 'x' ? domRect.height : height;
|
|
49
|
+
width = data.axis === 'y' ? domRect.width : width;
|
|
50
|
+
}
|
|
51
|
+
isAutoUpdateEvent = true;
|
|
52
|
+
return { width, height, x, y, top: y, right: x + width, bottom: y + height, left: x };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function isMouseBasedEvent(event: any): boolean {
|
|
57
|
+
return event != null && event.clientX != null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function useClientPoint(...args: any[]): any {
|
|
61
|
+
const [user, slot] = splitSlot(args);
|
|
62
|
+
const context = user[0];
|
|
63
|
+
const props = (user[1] as any) ?? {};
|
|
64
|
+
|
|
65
|
+
const open = context.open;
|
|
66
|
+
const dataRef = context.dataRef;
|
|
67
|
+
const floating = context.elements.floating;
|
|
68
|
+
const domReference = context.elements.domReference;
|
|
69
|
+
const refs = context.refs;
|
|
70
|
+
|
|
71
|
+
const enabled = props.enabled ?? true;
|
|
72
|
+
const axis = props.axis ?? 'both';
|
|
73
|
+
const x = props.x ?? null;
|
|
74
|
+
const y = props.y ?? null;
|
|
75
|
+
|
|
76
|
+
const initialRef = useRef(false, subSlot(slot, 'init'));
|
|
77
|
+
const cleanupListenerRef = useRef<any>(null, subSlot(slot, 'cleanup'));
|
|
78
|
+
const [pointerType, setPointerType] = useState<any>(undefined, subSlot(slot, 'ptype'));
|
|
79
|
+
const [reactive, setReactive] = useState<any[]>([], subSlot(slot, 'reactive'));
|
|
80
|
+
|
|
81
|
+
const setReference = useEffectEvent(
|
|
82
|
+
(px: number, py: number) => {
|
|
83
|
+
if (initialRef.current) return;
|
|
84
|
+
if (dataRef.current.openEvent && !isMouseBasedEvent(dataRef.current.openEvent)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
refs.setPositionReference(
|
|
88
|
+
createVirtualElement(domReference, { x: px, y: py, axis, dataRef, pointerType }),
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
subSlot(slot, 'setref'),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const handleReferenceEnterOrMove = useEffectEvent(
|
|
95
|
+
(event: any) => {
|
|
96
|
+
if (x != null || y != null) return;
|
|
97
|
+
if (!open) {
|
|
98
|
+
setReference(event.clientX, event.clientY);
|
|
99
|
+
} else if (!cleanupListenerRef.current) {
|
|
100
|
+
setReactive([]);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
subSlot(slot, 'enter'),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const openCheck = isMouseLikePointerType(pointerType) ? floating : open;
|
|
107
|
+
const addListener = useCallback(
|
|
108
|
+
() => {
|
|
109
|
+
if (!openCheck || !enabled || x != null || y != null) return;
|
|
110
|
+
const win = getWindow(floating);
|
|
111
|
+
function handleMouseMove(event: any) {
|
|
112
|
+
const target = getTarget(event);
|
|
113
|
+
if (!contains(floating, target as any)) {
|
|
114
|
+
setReference(event.clientX, event.clientY);
|
|
115
|
+
} else {
|
|
116
|
+
win.removeEventListener('mousemove', handleMouseMove);
|
|
117
|
+
cleanupListenerRef.current = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!dataRef.current.openEvent || isMouseBasedEvent(dataRef.current.openEvent)) {
|
|
121
|
+
win.addEventListener('mousemove', handleMouseMove);
|
|
122
|
+
const cleanup = () => {
|
|
123
|
+
win.removeEventListener('mousemove', handleMouseMove);
|
|
124
|
+
cleanupListenerRef.current = null;
|
|
125
|
+
};
|
|
126
|
+
cleanupListenerRef.current = cleanup;
|
|
127
|
+
return cleanup;
|
|
128
|
+
}
|
|
129
|
+
refs.setPositionReference(domReference);
|
|
130
|
+
},
|
|
131
|
+
[openCheck, enabled, x, y, floating, dataRef, refs, domReference, setReference],
|
|
132
|
+
subSlot(slot, 'add'),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
useEffect(
|
|
136
|
+
() => {
|
|
137
|
+
return addListener();
|
|
138
|
+
},
|
|
139
|
+
[addListener, reactive],
|
|
140
|
+
subSlot(slot, 'e:add'),
|
|
141
|
+
);
|
|
142
|
+
useEffect(
|
|
143
|
+
() => {
|
|
144
|
+
if (enabled && !floating) {
|
|
145
|
+
initialRef.current = false;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
[enabled, floating],
|
|
149
|
+
subSlot(slot, 'e:reset'),
|
|
150
|
+
);
|
|
151
|
+
useEffect(
|
|
152
|
+
() => {
|
|
153
|
+
if (!enabled && open) {
|
|
154
|
+
initialRef.current = true;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
[enabled, open],
|
|
158
|
+
subSlot(slot, 'e:block'),
|
|
159
|
+
);
|
|
160
|
+
useModernLayoutEffect(
|
|
161
|
+
() => {
|
|
162
|
+
if (enabled && (x != null || y != null)) {
|
|
163
|
+
initialRef.current = false;
|
|
164
|
+
setReference(x, y);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
[enabled, x, y, setReference],
|
|
168
|
+
subSlot(slot, 'e:explicit'),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const reference = useMemo(
|
|
172
|
+
() => {
|
|
173
|
+
function setPointerTypeRef(_ref: any) {
|
|
174
|
+
const { pointerType: pt } = _ref;
|
|
175
|
+
setPointerType(pt);
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
onPointerDown: setPointerTypeRef,
|
|
179
|
+
onPointerEnter: setPointerTypeRef,
|
|
180
|
+
onMouseMove: handleReferenceEnterOrMove,
|
|
181
|
+
onMouseEnter: handleReferenceEnterOrMove,
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
[handleReferenceEnterOrMove],
|
|
185
|
+
subSlot(slot, 'm:ref'),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
return useMemo(
|
|
189
|
+
() => (enabled ? { reference } : {}),
|
|
190
|
+
[enabled, reference],
|
|
191
|
+
subSlot(slot, 'm:ret'),
|
|
192
|
+
);
|
|
193
|
+
}
|