@octanejs/floating-ui 0.1.8 → 0.1.10
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 +3 -3
- package/src/Composite.ts +118 -20
- package/src/FloatingArrow.ts +61 -4
- package/src/FloatingFocusManager.ts +127 -26
- package/src/FloatingList.ts +58 -13
- package/src/FloatingOverlay.ts +26 -3
- package/src/FloatingPortal.ts +100 -23
- package/src/context.ts +43 -15
- package/src/delayGroup.ts +66 -8
- package/src/index.ts +108 -0
- package/src/pubsub.ts +3 -1
- package/src/safePolygon.ts +13 -4
- package/src/transitions.ts +76 -10
- package/src/tree.ts +53 -11
- package/src/types.ts +337 -0
- package/src/useClick.ts +63 -13
- package/src/useClientPoint.ts +50 -10
- package/src/useDismiss.ts +86 -12
- package/src/useFloating.ts +46 -14
- package/src/useFocus.ts +33 -7
- package/src/useHover.ts +80 -19
- package/src/useId.ts +1 -1
- package/src/useInteractions.ts +12 -3
- package/src/useListNavigation.ts +162 -8
- package/src/useMergeRefs.ts +6 -1
- package/src/useRole.ts +41 -12
- package/src/useTypeahead.ts +75 -10
- package/src/utils/index.ts +159 -62
package/src/useDismiss.ts
CHANGED
|
@@ -28,27 +28,101 @@ import {
|
|
|
28
28
|
isRootElement,
|
|
29
29
|
useEffectEvent,
|
|
30
30
|
} from './utils';
|
|
31
|
+
import type { ElementProps, FloatingRootContext } from './types';
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
export interface UseDismissProps {
|
|
34
|
+
/**
|
|
35
|
+
* Whether the Hook is enabled, including all internal Effects and event
|
|
36
|
+
* handlers.
|
|
37
|
+
* @default true
|
|
38
|
+
*/
|
|
39
|
+
enabled?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether to dismiss the floating element upon pressing the `esc` key.
|
|
42
|
+
* @default true
|
|
43
|
+
*/
|
|
44
|
+
escapeKey?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether to dismiss the floating element upon pressing the reference
|
|
47
|
+
* element. You likely want to ensure the `move` option in the `useHover()`
|
|
48
|
+
* Hook has been disabled when this is in use.
|
|
49
|
+
* @default false
|
|
50
|
+
*/
|
|
51
|
+
referencePress?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The type of event to use to determine a “press”.
|
|
54
|
+
* - `pointerdown` is eager on both mouse + touch input.
|
|
55
|
+
* - `mousedown` is eager on mouse input, but lazy on touch input.
|
|
56
|
+
* - `click` is lazy on both mouse + touch input.
|
|
57
|
+
* @default 'pointerdown'
|
|
58
|
+
*/
|
|
59
|
+
referencePressEvent?: 'pointerdown' | 'mousedown' | 'click';
|
|
60
|
+
/**
|
|
61
|
+
* Whether to dismiss the floating element upon pressing outside of the
|
|
62
|
+
* floating element. Accepts a guard function to filter which presses count
|
|
63
|
+
* as outside (native `MouseEvent`).
|
|
64
|
+
* @default true
|
|
65
|
+
*/
|
|
66
|
+
outsidePress?: boolean | ((event: MouseEvent) => boolean);
|
|
67
|
+
/**
|
|
68
|
+
* The type of event to use to determine an outside “press”.
|
|
69
|
+
* - `pointerdown` is eager on both mouse + touch input.
|
|
70
|
+
* - `mousedown` is eager on mouse input, but lazy on touch input.
|
|
71
|
+
* - `click` is lazy on both mouse + touch input.
|
|
72
|
+
* @default 'pointerdown'
|
|
73
|
+
*/
|
|
74
|
+
outsidePressEvent?: 'pointerdown' | 'mousedown' | 'click';
|
|
75
|
+
/**
|
|
76
|
+
* Whether to dismiss the floating element upon scrolling an overflow
|
|
77
|
+
* ancestor.
|
|
78
|
+
* @default false
|
|
79
|
+
*/
|
|
80
|
+
ancestorScroll?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Determines whether event listeners bubble upwards through a tree of
|
|
83
|
+
* floating elements.
|
|
84
|
+
*/
|
|
85
|
+
bubbles?: boolean | { escapeKey?: boolean; outsidePress?: boolean };
|
|
86
|
+
/**
|
|
87
|
+
* Determines whether to use capture phase event listeners.
|
|
88
|
+
*/
|
|
89
|
+
capture?: boolean | { escapeKey?: boolean; outsidePress?: boolean };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type PressHandlerKey = 'pointerdown' | 'mousedown' | 'click';
|
|
93
|
+
|
|
94
|
+
const bubbleHandlerKeys: Record<PressHandlerKey, string> = {
|
|
33
95
|
pointerdown: 'onPointerDown',
|
|
34
96
|
mousedown: 'onMouseDown',
|
|
35
97
|
click: 'onClick',
|
|
36
98
|
};
|
|
37
|
-
const captureHandlerKeys:
|
|
99
|
+
const captureHandlerKeys: Record<PressHandlerKey, string> = {
|
|
38
100
|
pointerdown: 'onPointerDownCapture',
|
|
39
101
|
mousedown: 'onMouseDownCapture',
|
|
40
102
|
click: 'onClickCapture',
|
|
41
103
|
};
|
|
42
|
-
const normalizeProp = (
|
|
104
|
+
const normalizeProp = (
|
|
105
|
+
normalizable?: boolean | { escapeKey?: boolean; outsidePress?: boolean },
|
|
106
|
+
) => ({
|
|
43
107
|
escapeKey: typeof normalizable === 'boolean' ? normalizable : (normalizable?.escapeKey ?? false),
|
|
44
108
|
outsidePress:
|
|
45
109
|
typeof normalizable === 'boolean' ? normalizable : (normalizable?.outsidePress ?? true),
|
|
46
110
|
});
|
|
47
111
|
|
|
48
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Closes the floating element when a dismissal is requested — by default, when
|
|
114
|
+
* the user presses the `escape` key or outside of the floating element.
|
|
115
|
+
* @see https://floating-ui.com/docs/useDismiss
|
|
116
|
+
*/
|
|
117
|
+
export function useDismiss(
|
|
118
|
+
context: FloatingRootContext,
|
|
119
|
+
props?: UseDismissProps,
|
|
120
|
+
slot?: symbol,
|
|
121
|
+
): ElementProps;
|
|
122
|
+
export function useDismiss(...args: any[]): ElementProps {
|
|
49
123
|
const [user, slot] = splitSlot(args);
|
|
50
|
-
const context = user[0];
|
|
51
|
-
const props = (user[1] as
|
|
124
|
+
const context = user[0] as FloatingRootContext;
|
|
125
|
+
const props = (user[1] as UseDismissProps) ?? {};
|
|
52
126
|
|
|
53
127
|
const open = context.open;
|
|
54
128
|
const onOpenChange = context.onOpenChange;
|
|
@@ -78,7 +152,7 @@ export function useDismiss(...args: any[]): any {
|
|
|
78
152
|
const isComposingRef = useRef(false, subSlot(slot, 'comp'));
|
|
79
153
|
|
|
80
154
|
const closeOnEscapeKeyDown = useEffectEvent(
|
|
81
|
-
(event:
|
|
155
|
+
(event: KeyboardEvent) => {
|
|
82
156
|
if (!open || !enabled || !escapeKey || event.key !== 'Escape') {
|
|
83
157
|
return;
|
|
84
158
|
}
|
|
@@ -102,13 +176,13 @@ export function useDismiss(...args: any[]): any {
|
|
|
102
176
|
}
|
|
103
177
|
}
|
|
104
178
|
}
|
|
105
|
-
onOpenChange(false, isReactEvent(event) ? event.nativeEvent : event, 'escape-key');
|
|
179
|
+
onOpenChange(false, isReactEvent(event) ? (event as any).nativeEvent : event, 'escape-key');
|
|
106
180
|
},
|
|
107
181
|
subSlot(slot, 'esc'),
|
|
108
182
|
);
|
|
109
183
|
|
|
110
184
|
const closeOnEscapeKeyDownCapture = useEffectEvent(
|
|
111
|
-
(event:
|
|
185
|
+
(event: KeyboardEvent) => {
|
|
112
186
|
const callback = () => {
|
|
113
187
|
closeOnEscapeKeyDown(event);
|
|
114
188
|
getTarget(event)?.removeEventListener('keydown', callback);
|
|
@@ -119,7 +193,7 @@ export function useDismiss(...args: any[]): any {
|
|
|
119
193
|
);
|
|
120
194
|
|
|
121
195
|
const closeOnPressOutside = useEffectEvent(
|
|
122
|
-
(event:
|
|
196
|
+
(event: MouseEvent) => {
|
|
123
197
|
const insideReactTree = dataRef.current.insideReactTree;
|
|
124
198
|
dataRef.current.insideReactTree = false;
|
|
125
199
|
|
|
@@ -211,7 +285,7 @@ export function useDismiss(...args: any[]): any {
|
|
|
211
285
|
);
|
|
212
286
|
|
|
213
287
|
const closeOnPressOutsideCapture = useEffectEvent(
|
|
214
|
-
(event:
|
|
288
|
+
(event: MouseEvent) => {
|
|
215
289
|
const callback = () => {
|
|
216
290
|
closeOnPressOutside(event);
|
|
217
291
|
getTarget(event)?.removeEventListener(outsidePressEvent, callback);
|
|
@@ -371,7 +445,7 @@ export function useDismiss(...args: any[]): any {
|
|
|
371
445
|
subSlot(slot, 'm:flo'),
|
|
372
446
|
);
|
|
373
447
|
|
|
374
|
-
return useMemo(
|
|
448
|
+
return useMemo<ElementProps>(
|
|
375
449
|
() => (enabled ? { reference, floating } : {}),
|
|
376
450
|
[enabled, reference, floating],
|
|
377
451
|
subSlot(slot, 'm:ret'),
|
package/src/useFloating.ts
CHANGED
|
@@ -4,10 +4,25 @@
|
|
|
4
4
|
// ./internal). The returned `context`/refs carry the root slot so the interaction
|
|
5
5
|
// hooks (later phases) can compose without their own slot.
|
|
6
6
|
import { arrow as arrowCore, computePosition } from '@floating-ui/dom';
|
|
7
|
+
import type {
|
|
8
|
+
ComputePositionConfig,
|
|
9
|
+
Derivable,
|
|
10
|
+
Middleware,
|
|
11
|
+
MiddlewareState,
|
|
12
|
+
} from '@floating-ui/dom';
|
|
7
13
|
import { flushSync, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'octane';
|
|
8
14
|
|
|
9
15
|
import { splitSlot, subSlot } from './internal';
|
|
10
16
|
import { deepEqual, getDPR, roundByDPR } from './utils';
|
|
17
|
+
import type {
|
|
18
|
+
ArrowOptions,
|
|
19
|
+
FloatingStyles,
|
|
20
|
+
MutableRefObject,
|
|
21
|
+
ReferenceType,
|
|
22
|
+
UsePositionFloatingData,
|
|
23
|
+
UsePositionFloatingOptions,
|
|
24
|
+
UsePositionFloatingReturn,
|
|
25
|
+
} from './types';
|
|
11
26
|
|
|
12
27
|
// Keep a ref pointed at the latest `value` without retriggering effects.
|
|
13
28
|
function useLatestRef<T>(value: T, slot: symbol | undefined): { current: T } {
|
|
@@ -24,9 +39,22 @@ function useLatestRef<T>(value: T, slot: symbol | undefined): { current: T } {
|
|
|
24
39
|
|
|
25
40
|
// The positioning core (ported from @floating-ui/react-dom's useFloating). The
|
|
26
41
|
// PUBLIC useFloating (in ./context) wraps this and adds the interaction context.
|
|
42
|
+
// Called as `usePositionFloating([options, slot])` — the array form keeps the
|
|
43
|
+
// compiler-injected trailing slot detectable for plain-`.ts` callers.
|
|
44
|
+
// RT defaults to `any` (not upstream's `ReferenceType`) so that pre-typing
|
|
45
|
+
// callers that supply untyped elements — and therefore give inference no
|
|
46
|
+
// candidate — keep their loose reference typing (e.g. @octanejs/base-ui builds
|
|
47
|
+
// its own stricter FloatingContext from this return). Callers with typed
|
|
48
|
+
// elements infer RT precisely.
|
|
49
|
+
export function usePositionFloating<RT extends ReferenceType = any>(
|
|
50
|
+
args: [UsePositionFloatingOptions<RT>?, (symbol | undefined)?],
|
|
51
|
+
): UsePositionFloatingReturn<RT>;
|
|
52
|
+
// Loose fallback: pre-typing callers built untyped option bags in plain `.ts`;
|
|
53
|
+
// they keep compiling while still receiving the typed return.
|
|
54
|
+
export function usePositionFloating(args: unknown[]): UsePositionFloatingReturn<any>;
|
|
27
55
|
export function usePositionFloating(args: any[]): any {
|
|
28
56
|
const [user, slot] = splitSlot(args);
|
|
29
|
-
const options = (user[0] as
|
|
57
|
+
const options = (user[0] as UsePositionFloatingOptions) ?? {};
|
|
30
58
|
|
|
31
59
|
const placement = options.placement ?? 'bottom';
|
|
32
60
|
const strategy = options.strategy ?? 'absolute';
|
|
@@ -38,7 +66,7 @@ export function usePositionFloating(args: any[]): any {
|
|
|
38
66
|
const whileElementsMounted = options.whileElementsMounted;
|
|
39
67
|
const open = options.open;
|
|
40
68
|
|
|
41
|
-
const [data, setData] = useState(
|
|
69
|
+
const [data, setData] = useState<UsePositionFloatingData>(
|
|
42
70
|
{
|
|
43
71
|
x: 0,
|
|
44
72
|
y: 0,
|
|
@@ -55,14 +83,14 @@ export function usePositionFloating(args: any[]): any {
|
|
|
55
83
|
setLatestMiddleware(middleware);
|
|
56
84
|
}
|
|
57
85
|
|
|
58
|
-
const [_reference, _setReference] = useState(null, subSlot(slot, 'ref'));
|
|
59
|
-
const [_floating, _setFloating] = useState(null, subSlot(slot, 'flo'));
|
|
86
|
+
const [_reference, _setReference] = useState<ReferenceType | null>(null, subSlot(slot, 'ref'));
|
|
87
|
+
const [_floating, _setFloating] = useState<HTMLElement | null>(null, subSlot(slot, 'flo'));
|
|
60
88
|
|
|
61
|
-
const referenceRef = useRef<
|
|
62
|
-
const floatingRef = useRef<
|
|
89
|
+
const referenceRef = useRef<ReferenceType | null>(null, subSlot(slot, 'rref'));
|
|
90
|
+
const floatingRef = useRef<HTMLElement | null>(null, subSlot(slot, 'rflo'));
|
|
63
91
|
|
|
64
92
|
const setReference = useCallback(
|
|
65
|
-
(node:
|
|
93
|
+
(node: ReferenceType | null) => {
|
|
66
94
|
if (node !== referenceRef.current) {
|
|
67
95
|
referenceRef.current = node;
|
|
68
96
|
_setReference(node);
|
|
@@ -72,7 +100,7 @@ export function usePositionFloating(args: any[]): any {
|
|
|
72
100
|
subSlot(slot, 'sref'),
|
|
73
101
|
);
|
|
74
102
|
const setFloating = useCallback(
|
|
75
|
-
(node:
|
|
103
|
+
(node: HTMLElement | null) => {
|
|
76
104
|
if (node !== floatingRef.current) {
|
|
77
105
|
floatingRef.current = node;
|
|
78
106
|
_setFloating(node);
|
|
@@ -97,7 +125,11 @@ export function usePositionFloating(args: any[]): any {
|
|
|
97
125
|
if (!referenceRef.current || !floatingRef.current) {
|
|
98
126
|
return;
|
|
99
127
|
}
|
|
100
|
-
const config:
|
|
128
|
+
const config: Partial<ComputePositionConfig> = {
|
|
129
|
+
placement,
|
|
130
|
+
strategy,
|
|
131
|
+
middleware: latestMiddleware,
|
|
132
|
+
};
|
|
101
133
|
if (platformRef.current) {
|
|
102
134
|
config.platform = platformRef.current;
|
|
103
135
|
}
|
|
@@ -122,7 +154,7 @@ export function usePositionFloating(args: any[]): any {
|
|
|
122
154
|
() => {
|
|
123
155
|
if (open === false && dataRef.current.isPositioned) {
|
|
124
156
|
dataRef.current.isPositioned = false;
|
|
125
|
-
setData((d
|
|
157
|
+
setData((d) => ({ ...d, isPositioned: false }));
|
|
126
158
|
}
|
|
127
159
|
},
|
|
128
160
|
[open],
|
|
@@ -167,7 +199,7 @@ export function usePositionFloating(args: any[]): any {
|
|
|
167
199
|
subSlot(slot, 'm:el'),
|
|
168
200
|
);
|
|
169
201
|
|
|
170
|
-
const floatingStyles = useMemo(
|
|
202
|
+
const floatingStyles = useMemo<FloatingStyles>(
|
|
171
203
|
() => {
|
|
172
204
|
const initialStyles = { position: strategy, left: 0, top: 0 };
|
|
173
205
|
if (!elements.floating) {
|
|
@@ -196,14 +228,14 @@ export function usePositionFloating(args: any[]): any {
|
|
|
196
228
|
}
|
|
197
229
|
|
|
198
230
|
// Ref-aware `arrow` middleware: accepts an octane ref ({current}) or an element.
|
|
199
|
-
export const arrow = (options:
|
|
200
|
-
function isRef(value:
|
|
231
|
+
export const arrow = (options: ArrowOptions | Derivable<ArrowOptions>): Middleware => {
|
|
232
|
+
function isRef(value: unknown): value is MutableRefObject<Element | null> {
|
|
201
233
|
return {}.hasOwnProperty.call(value, 'current');
|
|
202
234
|
}
|
|
203
235
|
return {
|
|
204
236
|
name: 'arrow',
|
|
205
237
|
options,
|
|
206
|
-
fn(state:
|
|
238
|
+
fn(state: MiddlewareState) {
|
|
207
239
|
const { element, padding } = typeof options === 'function' ? options(state) : options;
|
|
208
240
|
if (element && isRef(element)) {
|
|
209
241
|
if (element.current != null) {
|
package/src/useFocus.ts
CHANGED
|
@@ -15,11 +15,37 @@ import {
|
|
|
15
15
|
isTypeableElement,
|
|
16
16
|
matchesFocusVisible,
|
|
17
17
|
} from './utils';
|
|
18
|
+
import type { ElementProps, FloatingRootContext, OpenChangeReason } from './types';
|
|
18
19
|
|
|
19
|
-
export
|
|
20
|
+
export interface UseFocusProps {
|
|
21
|
+
/**
|
|
22
|
+
* Whether the Hook is enabled, including all internal Effects and event
|
|
23
|
+
* handlers.
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the open state only changes if the focus event is considered
|
|
29
|
+
* visible (`:focus-visible` CSS selector).
|
|
30
|
+
* @default true
|
|
31
|
+
*/
|
|
32
|
+
visibleOnly?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Opens the floating element while the reference element has focus, like CSS
|
|
37
|
+
* `:focus`.
|
|
38
|
+
* @see https://floating-ui.com/docs/useFocus
|
|
39
|
+
*/
|
|
40
|
+
export function useFocus(
|
|
41
|
+
context: FloatingRootContext,
|
|
42
|
+
props?: UseFocusProps,
|
|
43
|
+
slot?: symbol,
|
|
44
|
+
): ElementProps;
|
|
45
|
+
export function useFocus(...args: any[]): ElementProps {
|
|
20
46
|
const [user, slot] = splitSlot(args);
|
|
21
|
-
const context = user[0];
|
|
22
|
-
const props = (user[1] as
|
|
47
|
+
const context = user[0] as FloatingRootContext;
|
|
48
|
+
const props = (user[1] as UseFocusProps) ?? {};
|
|
23
49
|
|
|
24
50
|
const open = context.open;
|
|
25
51
|
const onOpenChange = context.onOpenChange;
|
|
@@ -73,7 +99,7 @@ export function useFocus(...args: any[]): any {
|
|
|
73
99
|
useEffect(
|
|
74
100
|
() => {
|
|
75
101
|
if (!enabled) return;
|
|
76
|
-
function onOpenChangeLocal(_ref:
|
|
102
|
+
function onOpenChangeLocal(_ref: { reason?: OpenChangeReason }) {
|
|
77
103
|
const { reason } = _ref;
|
|
78
104
|
if (reason === 'reference-press' || reason === 'escape-key') {
|
|
79
105
|
blockFocusRef.current = true;
|
|
@@ -103,7 +129,7 @@ export function useFocus(...args: any[]): any {
|
|
|
103
129
|
onMouseLeave() {
|
|
104
130
|
blockFocusRef.current = false;
|
|
105
131
|
},
|
|
106
|
-
onFocus(event:
|
|
132
|
+
onFocus(event: FocusEvent) {
|
|
107
133
|
if (blockFocusRef.current) return;
|
|
108
134
|
const target = getTarget(event);
|
|
109
135
|
if (visibleOnly && isElement(target)) {
|
|
@@ -117,7 +143,7 @@ export function useFocus(...args: any[]): any {
|
|
|
117
143
|
}
|
|
118
144
|
onOpenChange(true, event, 'focus');
|
|
119
145
|
},
|
|
120
|
-
onBlur(event:
|
|
146
|
+
onBlur(event: FocusEvent) {
|
|
121
147
|
blockFocusRef.current = false;
|
|
122
148
|
const relatedTarget = event.relatedTarget;
|
|
123
149
|
|
|
@@ -146,7 +172,7 @@ export function useFocus(...args: any[]): any {
|
|
|
146
172
|
subSlot(slot, 'm:ref'),
|
|
147
173
|
);
|
|
148
174
|
|
|
149
|
-
return useMemo(
|
|
175
|
+
return useMemo<ElementProps>(
|
|
150
176
|
() => (enabled ? { reference } : {}),
|
|
151
177
|
[enabled, reference],
|
|
152
178
|
subSlot(slot, 'm:ret'),
|
package/src/useHover.ts
CHANGED
|
@@ -18,20 +18,76 @@ import {
|
|
|
18
18
|
useLatestRef,
|
|
19
19
|
useModernLayoutEffect,
|
|
20
20
|
} from './utils';
|
|
21
|
+
import type {
|
|
22
|
+
Delay,
|
|
23
|
+
ElementProps,
|
|
24
|
+
FloatingRootContext,
|
|
25
|
+
HandleClose,
|
|
26
|
+
OpenChangeReason,
|
|
27
|
+
} from './types';
|
|
28
|
+
|
|
29
|
+
export interface UseHoverProps {
|
|
30
|
+
/**
|
|
31
|
+
* Whether the Hook is enabled, including all internal Effects and event
|
|
32
|
+
* handlers.
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Accepts an event handler that runs on `mousemove` to control when the
|
|
38
|
+
* floating element closes once the cursor leaves the reference element.
|
|
39
|
+
* @default null
|
|
40
|
+
*/
|
|
41
|
+
handleClose?: HandleClose | null;
|
|
42
|
+
/**
|
|
43
|
+
* Waits until the user’s cursor is at “rest” over the reference element
|
|
44
|
+
* before changing the `open` state.
|
|
45
|
+
* @default 0
|
|
46
|
+
*/
|
|
47
|
+
restMs?: number | (() => number);
|
|
48
|
+
/**
|
|
49
|
+
* Waits for the specified time when the event listener runs before changing
|
|
50
|
+
* the `open` state.
|
|
51
|
+
* @default 0
|
|
52
|
+
*/
|
|
53
|
+
delay?: Delay | (() => Delay);
|
|
54
|
+
/**
|
|
55
|
+
* Whether the logic only runs for mouse input, ignoring touch input.
|
|
56
|
+
* Note: due to a bug with Linux Chrome, "pen" inputs are considered "mouse".
|
|
57
|
+
* @default false
|
|
58
|
+
*/
|
|
59
|
+
mouseOnly?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Whether moving the cursor over the floating element will open it, without a
|
|
62
|
+
* regular hover event required.
|
|
63
|
+
* @default true
|
|
64
|
+
*/
|
|
65
|
+
move?: boolean;
|
|
66
|
+
}
|
|
21
67
|
|
|
22
68
|
const safePolygonIdentifier = createAttribute('safe-polygon');
|
|
23
69
|
|
|
24
|
-
function getRestMs(value:
|
|
70
|
+
function getRestMs(value: number | (() => number)): number {
|
|
25
71
|
if (typeof value === 'function') {
|
|
26
72
|
return value();
|
|
27
73
|
}
|
|
28
74
|
return value;
|
|
29
75
|
}
|
|
30
76
|
|
|
31
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Opens the floating element while hovering over the reference element, like
|
|
79
|
+
* CSS `:hover`.
|
|
80
|
+
* @see https://floating-ui.com/docs/useHover
|
|
81
|
+
*/
|
|
82
|
+
export function useHover(
|
|
83
|
+
context: FloatingRootContext,
|
|
84
|
+
props?: UseHoverProps,
|
|
85
|
+
slot?: symbol,
|
|
86
|
+
): ElementProps;
|
|
87
|
+
export function useHover(...args: any[]): ElementProps {
|
|
32
88
|
const [user, slot] = splitSlot(args);
|
|
33
|
-
const context = user[0];
|
|
34
|
-
const props = (user[1] as
|
|
89
|
+
const context = user[0] as FloatingRootContext;
|
|
90
|
+
const props = (user[1] as UseHoverProps) ?? {};
|
|
35
91
|
|
|
36
92
|
const open = context.open;
|
|
37
93
|
const onOpenChange = context.onOpenChange;
|
|
@@ -53,13 +109,16 @@ export function useHover(...args: any[]): any {
|
|
|
53
109
|
const openRef = useLatestRef(open, subSlot(slot, 'open'));
|
|
54
110
|
const restMsRef = useLatestRef(restMs, subSlot(slot, 'restms'));
|
|
55
111
|
|
|
56
|
-
const pointerTypeRef = useRef<
|
|
112
|
+
const pointerTypeRef = useRef<string | undefined>(undefined, subSlot(slot, 'ptype'));
|
|
57
113
|
const timeoutRef = useRef(-1, subSlot(slot, 'timeout'));
|
|
58
|
-
const handlerRef = useRef<
|
|
114
|
+
const handlerRef = useRef<((event: MouseEvent) => void) | undefined>(
|
|
115
|
+
undefined,
|
|
116
|
+
subSlot(slot, 'handler'),
|
|
117
|
+
);
|
|
59
118
|
const restTimeoutRef = useRef(-1, subSlot(slot, 'resttimeout'));
|
|
60
119
|
const blockMouseMoveRef = useRef(true, subSlot(slot, 'block'));
|
|
61
120
|
const performedPointerEventsMutationRef = useRef(false, subSlot(slot, 'ppem'));
|
|
62
|
-
const unbindMouseMoveRef = useRef<
|
|
121
|
+
const unbindMouseMoveRef = useRef<() => void>(() => {}, subSlot(slot, 'unbind'));
|
|
63
122
|
const restTimeoutPendingRef = useRef(false, subSlot(slot, 'rtp'));
|
|
64
123
|
|
|
65
124
|
const isHoverOpen = useEffectEvent(
|
|
@@ -96,7 +155,7 @@ export function useHover(...args: any[]): any {
|
|
|
96
155
|
if (!enabled) return;
|
|
97
156
|
if (!handleCloseRef.current) return;
|
|
98
157
|
if (!open) return;
|
|
99
|
-
function onLeave(event:
|
|
158
|
+
function onLeave(event: MouseEvent) {
|
|
100
159
|
if (isHoverOpen()) {
|
|
101
160
|
onOpenChange(false, event, 'hover');
|
|
102
161
|
}
|
|
@@ -112,7 +171,7 @@ export function useHover(...args: any[]): any {
|
|
|
112
171
|
);
|
|
113
172
|
|
|
114
173
|
const closeWithDelay = useCallback(
|
|
115
|
-
(event:
|
|
174
|
+
(event: Event, runElseBranch = true, reason: OpenChangeReason = 'hover') => {
|
|
116
175
|
const closeDelay = getDelay(delayRef.current, 'close', pointerTypeRef.current);
|
|
117
176
|
if (closeDelay && !handlerRef.current) {
|
|
118
177
|
clearTimeoutIfSet(timeoutRef);
|
|
@@ -161,7 +220,7 @@ export function useHover(...args: any[]): any {
|
|
|
161
220
|
useEffect(
|
|
162
221
|
() => {
|
|
163
222
|
if (!enabled) return;
|
|
164
|
-
function onReferenceMouseEnter(event:
|
|
223
|
+
function onReferenceMouseEnter(event: MouseEvent) {
|
|
165
224
|
clearTimeoutIfSet(timeoutRef);
|
|
166
225
|
blockMouseMoveRef.current = false;
|
|
167
226
|
if (
|
|
@@ -181,7 +240,7 @@ export function useHover(...args: any[]): any {
|
|
|
181
240
|
onOpenChange(true, event, 'hover');
|
|
182
241
|
}
|
|
183
242
|
}
|
|
184
|
-
function onReferenceMouseLeave(event:
|
|
243
|
+
function onReferenceMouseLeave(event: MouseEvent) {
|
|
185
244
|
if (isClickLikeOpenEvent()) {
|
|
186
245
|
clearPointerEvents();
|
|
187
246
|
return;
|
|
@@ -216,13 +275,13 @@ export function useHover(...args: any[]): any {
|
|
|
216
275
|
}
|
|
217
276
|
const shouldClose =
|
|
218
277
|
pointerTypeRef.current === 'touch'
|
|
219
|
-
? !contains(elements.floating, event.relatedTarget)
|
|
278
|
+
? !contains(elements.floating, event.relatedTarget as Element | null)
|
|
220
279
|
: true;
|
|
221
280
|
if (shouldClose) {
|
|
222
281
|
closeWithDelay(event);
|
|
223
282
|
}
|
|
224
283
|
}
|
|
225
|
-
function onScrollMouseLeave(event:
|
|
284
|
+
function onScrollMouseLeave(event: MouseEvent) {
|
|
226
285
|
if (isClickLikeOpenEvent()) return;
|
|
227
286
|
if (!dataRef.current.floatingContext) return;
|
|
228
287
|
handleCloseRef.current?.({
|
|
@@ -242,13 +301,15 @@ export function useHover(...args: any[]): any {
|
|
|
242
301
|
function onFloatingMouseEnter() {
|
|
243
302
|
clearTimeoutIfSet(timeoutRef);
|
|
244
303
|
}
|
|
245
|
-
function onFloatingMouseLeave(event:
|
|
304
|
+
function onFloatingMouseLeave(event: MouseEvent) {
|
|
246
305
|
if (!isClickLikeOpenEvent()) {
|
|
247
306
|
closeWithDelay(event, false);
|
|
248
307
|
}
|
|
249
308
|
}
|
|
250
309
|
if (isElement(elements.domReference)) {
|
|
251
|
-
|
|
310
|
+
// lib.dom's `ElementEventMap` lacks mouse events; these listeners are
|
|
311
|
+
// registered on a plain Element, so go through the generic overload.
|
|
312
|
+
const reference = elements.domReference as HTMLElement;
|
|
252
313
|
const floating = elements.floating;
|
|
253
314
|
if (open) {
|
|
254
315
|
reference.addEventListener('mouseleave', onScrollMouseLeave);
|
|
@@ -311,7 +372,7 @@ export function useHover(...args: any[]): any {
|
|
|
311
372
|
if (isElement(elements.domReference) && floatingEl) {
|
|
312
373
|
const body = getDocument(elements.floating).body;
|
|
313
374
|
body.setAttribute(safePolygonIdentifier, '');
|
|
314
|
-
const ref = elements.domReference;
|
|
375
|
+
const ref = elements.domReference as HTMLElement;
|
|
315
376
|
const parentFloating = tree?.nodesRef.current.find((node: any) => node.id === parentId)
|
|
316
377
|
?.context?.elements.floating;
|
|
317
378
|
if (parentFloating) {
|
|
@@ -360,13 +421,13 @@ export function useHover(...args: any[]): any {
|
|
|
360
421
|
|
|
361
422
|
const reference = useMemo(
|
|
362
423
|
() => {
|
|
363
|
-
function setPointerRef(event:
|
|
424
|
+
function setPointerRef(event: PointerEvent) {
|
|
364
425
|
pointerTypeRef.current = event.pointerType;
|
|
365
426
|
}
|
|
366
427
|
return {
|
|
367
428
|
onPointerDown: setPointerRef,
|
|
368
429
|
onPointerEnter: setPointerRef,
|
|
369
|
-
onMouseMove(event:
|
|
430
|
+
onMouseMove(event: MouseEvent) {
|
|
370
431
|
function handleMouseMove() {
|
|
371
432
|
if (!blockMouseMoveRef.current && !openRef.current) {
|
|
372
433
|
onOpenChange(true, event, 'hover');
|
|
@@ -398,7 +459,7 @@ export function useHover(...args: any[]): any {
|
|
|
398
459
|
subSlot(slot, 'm:ref'),
|
|
399
460
|
);
|
|
400
461
|
|
|
401
|
-
return useMemo(
|
|
462
|
+
return useMemo<ElementProps>(
|
|
402
463
|
() => (enabled ? { reference } : {}),
|
|
403
464
|
[enabled, reference],
|
|
404
465
|
subSlot(slot, 'm:ret'),
|
package/src/useId.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @floating-ui/react's useId, backed by octane's SSR-stable useId.
|
|
2
2
|
import { useId as octaneUseId } from 'octane';
|
|
3
3
|
|
|
4
|
-
export function useId(slot
|
|
4
|
+
export function useId(slot?: symbol | undefined): string {
|
|
5
5
|
return octaneUseId(slot);
|
|
6
6
|
}
|
package/src/useInteractions.ts
CHANGED
|
@@ -4,11 +4,16 @@ import { useCallback, useMemo } from 'octane';
|
|
|
4
4
|
|
|
5
5
|
import { splitSlot, subSlot } from './internal';
|
|
6
6
|
import { FOCUSABLE_ATTRIBUTE } from './utils';
|
|
7
|
+
import type { ElementProps, UseInteractionsReturn } from './types';
|
|
7
8
|
|
|
8
9
|
const ACTIVE_KEY = 'active';
|
|
9
10
|
const SELECTED_KEY = 'selected';
|
|
10
11
|
|
|
11
|
-
function mergeProps(
|
|
12
|
+
function mergeProps(
|
|
13
|
+
userProps: any,
|
|
14
|
+
propsList: Array<ElementProps | void | null | undefined>,
|
|
15
|
+
elementKey: 'reference' | 'floating' | 'item',
|
|
16
|
+
): Record<string, unknown> {
|
|
12
17
|
const map = new Map<string, any[]>();
|
|
13
18
|
const isItem = elementKey === 'item';
|
|
14
19
|
let domUserProps = userProps;
|
|
@@ -31,7 +36,7 @@ function mergeProps(userProps: any, propsList: any[], elementKey: string): any {
|
|
|
31
36
|
return propsOrGetProps;
|
|
32
37
|
})
|
|
33
38
|
.concat(userProps)
|
|
34
|
-
.reduce((acc:
|
|
39
|
+
.reduce((acc: Record<string, unknown>, props: any) => {
|
|
35
40
|
if (!props) {
|
|
36
41
|
return acc;
|
|
37
42
|
}
|
|
@@ -62,9 +67,13 @@ function mergeProps(userProps: any, propsList: any[], elementKey: string): any {
|
|
|
62
67
|
};
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
export function useInteractions(
|
|
71
|
+
propsList?: Array<ElementProps | void | null | undefined>,
|
|
72
|
+
slot?: symbol,
|
|
73
|
+
): UseInteractionsReturn;
|
|
65
74
|
export function useInteractions(...args: any[]): any {
|
|
66
75
|
const [user, slot] = splitSlot(args);
|
|
67
|
-
const propsList = (user[0] as any[]) ?? [];
|
|
76
|
+
const propsList: Array<ElementProps | void | null | undefined> = (user[0] as any[]) ?? [];
|
|
68
77
|
|
|
69
78
|
const referenceDeps = propsList.map((key) => (key == null ? undefined : key.reference));
|
|
70
79
|
const floatingDeps = propsList.map((key) => (key == null ? undefined : key.floating));
|