@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/FloatingList.ts
CHANGED
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
useRef,
|
|
11
11
|
useState,
|
|
12
12
|
} from 'octane';
|
|
13
|
+
import type { OctaneNode } from 'octane';
|
|
13
14
|
|
|
14
15
|
import { S, splitSlot, subSlot } from './internal';
|
|
15
16
|
import { useModernLayoutEffect } from './utils';
|
|
17
|
+
import type { MutableRefObject } from './types';
|
|
16
18
|
|
|
17
19
|
function sortByDocumentPosition(a: Node, b: Node): number {
|
|
18
20
|
const position = a.compareDocumentPosition(b);
|
|
@@ -28,29 +30,55 @@ function sortByDocumentPosition(a: Node, b: Node): number {
|
|
|
28
30
|
return 0;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
interface FloatingListContextValue {
|
|
34
|
+
register: (node: Node) => void;
|
|
35
|
+
unregister: (node: Node) => void;
|
|
36
|
+
map: Map<Node, number | null>;
|
|
37
|
+
elementsRef: MutableRefObject<Array<HTMLElement | null>>;
|
|
38
|
+
labelsRef?: MutableRefObject<Array<string | null>>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const FloatingListContext = createContext<FloatingListContextValue>({
|
|
32
42
|
register: () => {},
|
|
33
43
|
unregister: () => {},
|
|
34
44
|
map: new Map(),
|
|
35
45
|
elementsRef: { current: [] },
|
|
36
46
|
});
|
|
37
47
|
|
|
38
|
-
export
|
|
48
|
+
export interface FloatingListProps {
|
|
49
|
+
children: OctaneNode;
|
|
50
|
+
/**
|
|
51
|
+
* A ref to the list of HTML elements, ordered by their index.
|
|
52
|
+
* `useListNavigation`'s `listRef` prop.
|
|
53
|
+
*/
|
|
54
|
+
elementsRef: MutableRefObject<Array<HTMLElement | null>>;
|
|
55
|
+
/**
|
|
56
|
+
* A ref to the list of element labels, ordered by their index.
|
|
57
|
+
* `useTypeahead`'s `listRef` prop.
|
|
58
|
+
*/
|
|
59
|
+
labelsRef?: MutableRefObject<Array<string | null>>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Provides context for a list of items within the floating element.
|
|
64
|
+
* @see https://floating-ui.com/docs/FloatingList
|
|
65
|
+
*/
|
|
66
|
+
export function FloatingList(props: FloatingListProps): OctaneNode {
|
|
39
67
|
const children = props.children;
|
|
40
68
|
const elementsRef = props.elementsRef;
|
|
41
69
|
const labelsRef = props.labelsRef;
|
|
42
70
|
|
|
43
|
-
const [nodes, setNodes] = useState(() => new Set<
|
|
71
|
+
const [nodes, setNodes] = useState(() => new Set<Node>(), S('FloatingList:nodes'));
|
|
44
72
|
const register = useCallback(
|
|
45
|
-
(node:
|
|
46
|
-
setNodes((prevSet
|
|
73
|
+
(node: Node) => {
|
|
74
|
+
setNodes((prevSet) => new Set(prevSet).add(node));
|
|
47
75
|
},
|
|
48
76
|
[],
|
|
49
77
|
S('FloatingList:register'),
|
|
50
78
|
);
|
|
51
79
|
const unregister = useCallback(
|
|
52
|
-
(node:
|
|
53
|
-
setNodes((prevSet
|
|
80
|
+
(node: Node) => {
|
|
81
|
+
setNodes((prevSet) => {
|
|
54
82
|
const set = new Set(prevSet);
|
|
55
83
|
set.delete(node);
|
|
56
84
|
return set;
|
|
@@ -61,7 +89,7 @@ export function FloatingList(props: any): any {
|
|
|
61
89
|
);
|
|
62
90
|
const map = useMemo(
|
|
63
91
|
() => {
|
|
64
|
-
const newMap = new Map();
|
|
92
|
+
const newMap = new Map<Node, number | null>();
|
|
65
93
|
const sortedNodes = Array.from(nodes.keys()).sort(sortByDocumentPosition);
|
|
66
94
|
sortedNodes.forEach((node, index) => {
|
|
67
95
|
newMap.set(node, index);
|
|
@@ -71,7 +99,7 @@ export function FloatingList(props: any): any {
|
|
|
71
99
|
[nodes],
|
|
72
100
|
S('FloatingList:map'),
|
|
73
101
|
);
|
|
74
|
-
const value = useMemo(
|
|
102
|
+
const value = useMemo<FloatingListContextValue>(
|
|
75
103
|
() => ({ register, unregister, map, elementsRef, labelsRef }),
|
|
76
104
|
[register, unregister, map, elementsRef, labelsRef],
|
|
77
105
|
S('FloatingList:value'),
|
|
@@ -79,18 +107,35 @@ export function FloatingList(props: any): any {
|
|
|
79
107
|
return createElement(FloatingListContext.Provider, { value, children });
|
|
80
108
|
}
|
|
81
109
|
|
|
110
|
+
export interface UseListItemProps {
|
|
111
|
+
label?: string | null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Used to register a list item and its index (DOM position) in the
|
|
116
|
+
* `FloatingList`.
|
|
117
|
+
* @see https://floating-ui.com/docs/FloatingList#uselistitem
|
|
118
|
+
*/
|
|
119
|
+
export function useListItem(
|
|
120
|
+
props?: UseListItemProps,
|
|
121
|
+
slot?: symbol,
|
|
122
|
+
): { ref: (node: HTMLElement | null) => void; index: number };
|
|
123
|
+
export function useListItem(slot?: symbol): {
|
|
124
|
+
ref: (node: HTMLElement | null) => void;
|
|
125
|
+
index: number;
|
|
126
|
+
};
|
|
82
127
|
export function useListItem(...args: any[]): any {
|
|
83
128
|
const [user, slotArg] = splitSlot(args);
|
|
84
129
|
const slot = slotArg ?? S('useListItem');
|
|
85
|
-
const props = (user[0] as
|
|
130
|
+
const props = (user[0] as UseListItemProps) ?? {};
|
|
86
131
|
const label = props.label;
|
|
87
132
|
|
|
88
133
|
const { register, unregister, map, elementsRef, labelsRef } = useContext(FloatingListContext);
|
|
89
|
-
const [index, setIndex] = useState<
|
|
90
|
-
const componentRef = useRef<
|
|
134
|
+
const [index, setIndex] = useState<number | null>(null, subSlot(slot, 'index'));
|
|
135
|
+
const componentRef = useRef<HTMLElement | null>(null, subSlot(slot, 'ref'));
|
|
91
136
|
|
|
92
137
|
const ref = useCallback(
|
|
93
|
-
(node:
|
|
138
|
+
(node: HTMLElement | null) => {
|
|
94
139
|
componentRef.current = node;
|
|
95
140
|
if (index !== null) {
|
|
96
141
|
elementsRef.current[index] = node;
|
package/src/FloatingOverlay.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// optional body scroll-lock. A `.ts` component (createElement, ref-as-prop). React
|
|
3
3
|
// forwardRef → `props.ref`.
|
|
4
4
|
import { createElement } from 'octane';
|
|
5
|
+
import type { OctaneNode } from 'octane';
|
|
5
6
|
|
|
6
7
|
import { S } from './internal';
|
|
7
|
-
import { getPlatform, useModernLayoutEffect } from './utils';
|
|
8
|
+
import { getPlatform, useModernLayoutEffect, type CSSProperties } from './utils';
|
|
9
|
+
import type { HTMLProps, MutableRefObject, RefCallback } from './types';
|
|
8
10
|
|
|
9
11
|
const scrollbarProperty = '--floating-ui-scrollbar-width';
|
|
10
12
|
let lockCount = 0;
|
|
@@ -48,7 +50,28 @@ function enableScrollLock() {
|
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
export
|
|
53
|
+
export interface FloatingOverlayProps {
|
|
54
|
+
/**
|
|
55
|
+
* Whether the overlay should lock scrolling on the document body.
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
lockScroll?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Provides base styling for a fixed overlay element to dim content or block
|
|
63
|
+
* pointer events behind a floating element.
|
|
64
|
+
* @see https://floating-ui.com/docs/FloatingOverlay
|
|
65
|
+
*/
|
|
66
|
+
export function FloatingOverlay(
|
|
67
|
+
props: FloatingOverlayProps &
|
|
68
|
+
HTMLProps<HTMLDivElement> & {
|
|
69
|
+
// Ref-as-prop (octane has no forwardRef); the overlay's own styles merge
|
|
70
|
+
// over the OBJECT form of `style`.
|
|
71
|
+
ref?: MutableRefObject<HTMLDivElement | null> | RefCallback<HTMLDivElement> | null;
|
|
72
|
+
style?: CSSProperties;
|
|
73
|
+
},
|
|
74
|
+
): OctaneNode {
|
|
52
75
|
const { lockScroll = false, ref, ...rest } = props;
|
|
53
76
|
|
|
54
77
|
useModernLayoutEffect(
|
|
@@ -79,7 +102,7 @@ export function FloatingOverlay(props: any): any {
|
|
|
79
102
|
right: 0,
|
|
80
103
|
bottom: 0,
|
|
81
104
|
left: 0,
|
|
82
|
-
...rest.style,
|
|
105
|
+
...(rest.style as CSSProperties | undefined),
|
|
83
106
|
},
|
|
84
107
|
});
|
|
85
108
|
}
|
package/src/FloatingPortal.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
useRef,
|
|
15
15
|
useState,
|
|
16
16
|
} from 'octane';
|
|
17
|
+
import type { OctaneNode } from 'octane';
|
|
17
18
|
|
|
18
19
|
import { S, splitSlot, subSlot } from './internal';
|
|
19
20
|
import { useId } from './useId';
|
|
@@ -26,9 +27,11 @@ import {
|
|
|
26
27
|
isOutsideEvent,
|
|
27
28
|
isSafari,
|
|
28
29
|
useModernLayoutEffect,
|
|
30
|
+
type CSSProperties,
|
|
29
31
|
} from './utils';
|
|
32
|
+
import type { HTMLProps, MutableRefObject, OpenChangeReason, RefCallback } from './types';
|
|
30
33
|
|
|
31
|
-
const HIDDEN_STYLES:
|
|
34
|
+
const HIDDEN_STYLES: CSSProperties = {
|
|
32
35
|
border: 0,
|
|
33
36
|
clip: 'rect(0 0 0 0)',
|
|
34
37
|
height: '1px',
|
|
@@ -42,8 +45,12 @@ const HIDDEN_STYLES: any = {
|
|
|
42
45
|
left: 0,
|
|
43
46
|
};
|
|
44
47
|
|
|
45
|
-
export function FocusGuard(
|
|
46
|
-
|
|
48
|
+
export function FocusGuard(
|
|
49
|
+
props: HTMLProps<HTMLSpanElement> & {
|
|
50
|
+
ref?: MutableRefObject<HTMLSpanElement | null> | RefCallback<HTMLSpanElement> | null;
|
|
51
|
+
},
|
|
52
|
+
): OctaneNode {
|
|
53
|
+
const [role, setRole] = useState<'button' | undefined>(undefined, S('FocusGuard:role'));
|
|
47
54
|
useModernLayoutEffect(
|
|
48
55
|
() => {
|
|
49
56
|
if (isSafari()) {
|
|
@@ -63,28 +70,66 @@ export function FocusGuard(props: any): any {
|
|
|
63
70
|
});
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
const HIDDEN_OWNER_STYLES:
|
|
73
|
+
const HIDDEN_OWNER_STYLES: CSSProperties = {
|
|
67
74
|
clipPath: 'inset(50%)',
|
|
68
75
|
position: 'fixed',
|
|
69
76
|
top: 0,
|
|
70
77
|
left: 0,
|
|
71
78
|
};
|
|
72
|
-
|
|
79
|
+
|
|
80
|
+
// What a non-modal FloatingFocusManager registers on its enclosing portal.
|
|
81
|
+
export interface FocusManagerState {
|
|
82
|
+
modal: boolean;
|
|
83
|
+
open: boolean;
|
|
84
|
+
onOpenChange: (open: boolean, event?: Event, reason?: OpenChangeReason) => void;
|
|
85
|
+
domReference: Element | null;
|
|
86
|
+
closeOnFocusOut: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface PortalContextValue {
|
|
90
|
+
preserveTabOrder: boolean;
|
|
91
|
+
portalNode: HTMLElement | null;
|
|
92
|
+
setFocusManagerState: (
|
|
93
|
+
state:
|
|
94
|
+
| FocusManagerState
|
|
95
|
+
| null
|
|
96
|
+
| ((prev: FocusManagerState | null) => FocusManagerState | null),
|
|
97
|
+
) => void;
|
|
98
|
+
beforeInsideRef: MutableRefObject<HTMLSpanElement | null>;
|
|
99
|
+
afterInsideRef: MutableRefObject<HTMLSpanElement | null>;
|
|
100
|
+
beforeOutsideRef: MutableRefObject<HTMLSpanElement | null>;
|
|
101
|
+
afterOutsideRef: MutableRefObject<HTMLSpanElement | null>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const PortalContext = createContext<PortalContextValue | null>(null);
|
|
73
105
|
const attr = createAttribute('portal');
|
|
74
106
|
|
|
75
|
-
export
|
|
107
|
+
export interface UseFloatingPortalNodeProps {
|
|
108
|
+
id?: string;
|
|
109
|
+
root?: HTMLElement | ShadowRoot | null | MutableRefObject<HTMLElement | ShadowRoot | null>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @see https://floating-ui.com/docs/FloatingPortal#usefloatingportalnode
|
|
114
|
+
*/
|
|
115
|
+
export function useFloatingPortalNode(
|
|
116
|
+
props?: UseFloatingPortalNodeProps,
|
|
117
|
+
slot?: symbol,
|
|
118
|
+
): HTMLElement | null;
|
|
119
|
+
export function useFloatingPortalNode(slot?: symbol): HTMLElement | null;
|
|
120
|
+
export function useFloatingPortalNode(...args: any[]): HTMLElement | null {
|
|
76
121
|
// Exported hook → may be called directly by consumers (compiler injects the
|
|
77
122
|
// slot) or by FloatingPortal (passes an S() slot); fall back to S() otherwise.
|
|
78
123
|
const [user, slotArg] = splitSlot(args);
|
|
79
124
|
const slot = slotArg ?? S('useFloatingPortalNode');
|
|
80
|
-
const props = (user[0] as
|
|
125
|
+
const props = (user[0] as UseFloatingPortalNodeProps) ?? {};
|
|
81
126
|
const id = props.id;
|
|
82
127
|
const root = props.root;
|
|
83
128
|
|
|
84
129
|
const uniqueId = useId(subSlot(slot, 'id'));
|
|
85
130
|
const portalContext = usePortalContext();
|
|
86
|
-
const [portalNode, setPortalNode] = useState<
|
|
87
|
-
const portalNodeRef = useRef<
|
|
131
|
+
const [portalNode, setPortalNode] = useState<HTMLElement | null>(null, subSlot(slot, 'node'));
|
|
132
|
+
const portalNodeRef = useRef<HTMLElement | null>(null, subSlot(slot, 'noderef'));
|
|
88
133
|
|
|
89
134
|
useModernLayoutEffect(
|
|
90
135
|
() => {
|
|
@@ -121,10 +166,15 @@ export function useFloatingPortalNode(...args: any[]): any {
|
|
|
121
166
|
if (root === null) return;
|
|
122
167
|
if (!uniqueId) return;
|
|
123
168
|
if (portalNodeRef.current) return;
|
|
124
|
-
let container
|
|
169
|
+
let container:
|
|
170
|
+
| HTMLElement
|
|
171
|
+
| ShadowRoot
|
|
172
|
+
| MutableRefObject<HTMLElement | ShadowRoot | null>
|
|
173
|
+
| null
|
|
174
|
+
| undefined = root || portalContext?.portalNode;
|
|
125
175
|
if (container && !isNode(container)) container = container.current;
|
|
126
176
|
container = container || document.body;
|
|
127
|
-
let idWrapper = null;
|
|
177
|
+
let idWrapper: HTMLDivElement | null = null;
|
|
128
178
|
if (id) {
|
|
129
179
|
idWrapper = document.createElement('div');
|
|
130
180
|
idWrapper.id = id;
|
|
@@ -145,18 +195,45 @@ export function useFloatingPortalNode(...args: any[]): any {
|
|
|
145
195
|
return portalNode;
|
|
146
196
|
}
|
|
147
197
|
|
|
148
|
-
export
|
|
198
|
+
export interface FloatingPortalProps {
|
|
199
|
+
children?: OctaneNode;
|
|
200
|
+
/**
|
|
201
|
+
* Optionally selects the node with the id if it exists, or create it and
|
|
202
|
+
* append it to the specified `root` (by default `document.body`).
|
|
203
|
+
*/
|
|
204
|
+
id?: string;
|
|
205
|
+
/**
|
|
206
|
+
* Specifies the root node the portal container will be appended to.
|
|
207
|
+
*/
|
|
208
|
+
root?: HTMLElement | ShadowRoot | null | MutableRefObject<HTMLElement | ShadowRoot | null>;
|
|
209
|
+
/**
|
|
210
|
+
* When using non-modal focus management using `FloatingFocusManager`, this
|
|
211
|
+
* will preserve the tab order context based on the octane tree instead of the
|
|
212
|
+
* DOM tree.
|
|
213
|
+
*/
|
|
214
|
+
preserveTabOrder?: boolean;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Portals the floating element into a given container element — by default,
|
|
219
|
+
* outside of the app root and into the body.
|
|
220
|
+
* @see https://floating-ui.com/docs/FloatingPortal
|
|
221
|
+
*/
|
|
222
|
+
export function FloatingPortal(props: FloatingPortalProps): OctaneNode {
|
|
149
223
|
const children = props.children;
|
|
150
224
|
const id = props.id;
|
|
151
225
|
const root = props.root;
|
|
152
226
|
const preserveTabOrder = props.preserveTabOrder ?? true;
|
|
153
227
|
|
|
154
|
-
const portalNode = useFloatingPortalNode(
|
|
155
|
-
const [focusManagerState, setFocusManagerState] = useState<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
228
|
+
const portalNode = useFloatingPortalNode({ id, root }, S('FloatingPortal:node'));
|
|
229
|
+
const [focusManagerState, setFocusManagerState] = useState<FocusManagerState | null>(
|
|
230
|
+
null,
|
|
231
|
+
S('FloatingPortal:fms'),
|
|
232
|
+
);
|
|
233
|
+
const beforeOutsideRef = useRef<HTMLSpanElement | null>(null, S('FloatingPortal:bo'));
|
|
234
|
+
const afterOutsideRef = useRef<HTMLSpanElement | null>(null, S('FloatingPortal:ao'));
|
|
235
|
+
const beforeInsideRef = useRef<HTMLSpanElement | null>(null, S('FloatingPortal:bi'));
|
|
236
|
+
const afterInsideRef = useRef<HTMLSpanElement | null>(null, S('FloatingPortal:ai'));
|
|
160
237
|
const modal = focusManagerState?.modal;
|
|
161
238
|
const open = focusManagerState?.open;
|
|
162
239
|
const shouldRenderGuards =
|
|
@@ -171,7 +248,7 @@ export function FloatingPortal(props: any): any {
|
|
|
171
248
|
if (!portalNode || !preserveTabOrder || modal) {
|
|
172
249
|
return;
|
|
173
250
|
}
|
|
174
|
-
function onFocus(event:
|
|
251
|
+
function onFocus(event: FocusEvent) {
|
|
175
252
|
if (portalNode && isOutsideEvent(event)) {
|
|
176
253
|
const focusing = event.type === 'focusin';
|
|
177
254
|
const manageFocus = focusing ? enableFocusInside : disableFocusInside;
|
|
@@ -199,7 +276,7 @@ export function FloatingPortal(props: any): any {
|
|
|
199
276
|
S('FloatingPortal:e:enable'),
|
|
200
277
|
);
|
|
201
278
|
|
|
202
|
-
const value = useMemo(
|
|
279
|
+
const value = useMemo<PortalContextValue>(
|
|
203
280
|
() => ({
|
|
204
281
|
preserveTabOrder,
|
|
205
282
|
beforeOutsideRef,
|
|
@@ -221,7 +298,7 @@ export function FloatingPortal(props: any): any {
|
|
|
221
298
|
createElement(FocusGuard, {
|
|
222
299
|
'data-type': 'outside',
|
|
223
300
|
ref: beforeOutsideRef,
|
|
224
|
-
onFocus: (event:
|
|
301
|
+
onFocus: (event: FocusEvent) => {
|
|
225
302
|
if (isOutsideEvent(event, portalNode)) {
|
|
226
303
|
beforeInsideRef.current?.focus();
|
|
227
304
|
} else {
|
|
@@ -239,7 +316,7 @@ export function FloatingPortal(props: any): any {
|
|
|
239
316
|
createElement(FocusGuard, {
|
|
240
317
|
'data-type': 'outside',
|
|
241
318
|
ref: afterOutsideRef,
|
|
242
|
-
onFocus: (event:
|
|
319
|
+
onFocus: (event: FocusEvent) => {
|
|
243
320
|
if (isOutsideEvent(event, portalNode)) {
|
|
244
321
|
afterInsideRef.current?.focus();
|
|
245
322
|
} else {
|
|
@@ -254,4 +331,4 @@ export function FloatingPortal(props: any): any {
|
|
|
254
331
|
});
|
|
255
332
|
}
|
|
256
333
|
|
|
257
|
-
export const usePortalContext = () => useContext(PortalContext);
|
|
334
|
+
export const usePortalContext = (): PortalContextValue | null => useContext(PortalContext);
|
package/src/context.ts
CHANGED
|
@@ -11,26 +11,41 @@ import { useFloatingParentNodeId, useFloatingTree } from './tree';
|
|
|
11
11
|
import { useId } from './useId';
|
|
12
12
|
import { usePositionFloating } from './useFloating';
|
|
13
13
|
import { useEffectEvent, useModernLayoutEffect } from './utils';
|
|
14
|
+
import type {
|
|
15
|
+
ContextData,
|
|
16
|
+
ExtendedElements,
|
|
17
|
+
ExtendedRefs,
|
|
18
|
+
FloatingContext,
|
|
19
|
+
FloatingRootContext,
|
|
20
|
+
OpenChangeReason,
|
|
21
|
+
ReferenceType,
|
|
22
|
+
UseFloatingOptions,
|
|
23
|
+
UseFloatingReturn,
|
|
24
|
+
UseFloatingRootContextOptions,
|
|
25
|
+
} from './types';
|
|
14
26
|
|
|
15
27
|
export { createPubSub } from './pubsub';
|
|
16
28
|
|
|
17
|
-
export function useFloatingRootContext(
|
|
29
|
+
export function useFloatingRootContext(
|
|
30
|
+
options: UseFloatingRootContextOptions,
|
|
31
|
+
slot?: symbol | undefined,
|
|
32
|
+
): FloatingRootContext {
|
|
18
33
|
const open = options.open ?? false;
|
|
19
34
|
const onOpenChangeProp = options.onOpenChange;
|
|
20
35
|
const elementsProp = options.elements;
|
|
21
36
|
|
|
22
37
|
const floatingId = useId(subSlot(slot, 'id'));
|
|
23
|
-
const dataRef = useRef<
|
|
38
|
+
const dataRef = useRef<ContextData>({}, subSlot(slot, 'data'));
|
|
24
39
|
const [events] = useState(() => createPubSub(), subSlot(slot, 'events'));
|
|
25
40
|
const nested = useFloatingParentNodeId() != null;
|
|
26
41
|
|
|
27
|
-
const [positionReference, setPositionReference] = useState(
|
|
42
|
+
const [positionReference, setPositionReference] = useState<ReferenceType | null>(
|
|
28
43
|
elementsProp.reference,
|
|
29
44
|
subSlot(slot, 'posref'),
|
|
30
45
|
);
|
|
31
46
|
|
|
32
47
|
const onOpenChange = useEffectEvent(
|
|
33
|
-
(openVal: boolean, event?: Event, reason?:
|
|
48
|
+
(openVal: boolean, event?: Event, reason?: OpenChangeReason) => {
|
|
34
49
|
dataRef.current.openEvent = openVal ? event : undefined;
|
|
35
50
|
events.emit('openchange', { open: openVal, event, reason, nested });
|
|
36
51
|
onOpenChangeProp?.(openVal, event, reason);
|
|
@@ -50,16 +65,26 @@ export function useFloatingRootContext(options: any, slot: symbol | undefined):
|
|
|
50
65
|
subSlot(slot, 'm:el'),
|
|
51
66
|
);
|
|
52
67
|
|
|
53
|
-
return useMemo(
|
|
68
|
+
return useMemo<FloatingRootContext>(
|
|
54
69
|
() => ({ dataRef, open, onOpenChange, elements, events, floatingId, refs }),
|
|
55
70
|
[open, onOpenChange, elements, events, floatingId, refs],
|
|
56
71
|
subSlot(slot, 'm:ret'),
|
|
57
72
|
);
|
|
58
73
|
}
|
|
59
74
|
|
|
75
|
+
export function useFloating<RT extends ReferenceType = ReferenceType>(
|
|
76
|
+
options?: UseFloatingOptions,
|
|
77
|
+
slot?: symbol,
|
|
78
|
+
): UseFloatingReturn<RT>;
|
|
79
|
+
// Loose fallback: pre-typing callers built untyped option bags in plain `.ts`;
|
|
80
|
+
// they keep compiling while still receiving the typed return.
|
|
81
|
+
export function useFloating(
|
|
82
|
+
options: Record<string, unknown> | undefined,
|
|
83
|
+
slot?: symbol,
|
|
84
|
+
): UseFloatingReturn;
|
|
60
85
|
export function useFloating(...args: any[]): any {
|
|
61
86
|
const [user, slot] = splitSlot(args);
|
|
62
|
-
const options = (user[0] as
|
|
87
|
+
const options = (user[0] as UseFloatingOptions) ?? {};
|
|
63
88
|
const nodeId = options.nodeId;
|
|
64
89
|
|
|
65
90
|
const internalRootContext = useFloatingRootContext(
|
|
@@ -76,11 +101,14 @@ export function useFloating(...args: any[]): any {
|
|
|
76
101
|
const rootContext = options.rootContext || internalRootContext;
|
|
77
102
|
const computedElements = rootContext.elements;
|
|
78
103
|
|
|
79
|
-
const [_domReference, setDomReference] = useState(null, subSlot(slot, 'domref'));
|
|
80
|
-
const [positionReference, _setPositionReference] = useState
|
|
104
|
+
const [_domReference, setDomReference] = useState<Element | null>(null, subSlot(slot, 'domref'));
|
|
105
|
+
const [positionReference, _setPositionReference] = useState<ReferenceType | null>(
|
|
106
|
+
null,
|
|
107
|
+
subSlot(slot, 'posref'),
|
|
108
|
+
);
|
|
81
109
|
const optionDomReference = computedElements?.domReference;
|
|
82
110
|
const domReference = optionDomReference || _domReference;
|
|
83
|
-
const domReferenceRef = useRef<
|
|
111
|
+
const domReferenceRef = useRef<Element | null>(null, subSlot(slot, 'domrefref'));
|
|
84
112
|
const tree = useFloatingTree();
|
|
85
113
|
|
|
86
114
|
useModernLayoutEffect(
|
|
@@ -105,7 +133,7 @@ export function useFloating(...args: any[]): any {
|
|
|
105
133
|
]);
|
|
106
134
|
|
|
107
135
|
const setPositionReference = useCallback(
|
|
108
|
-
(node:
|
|
136
|
+
(node: ReferenceType | null) => {
|
|
109
137
|
const computedPositionReference = isElement(node)
|
|
110
138
|
? {
|
|
111
139
|
getBoundingClientRect: () => node.getBoundingClientRect(),
|
|
@@ -121,7 +149,7 @@ export function useFloating(...args: any[]): any {
|
|
|
121
149
|
);
|
|
122
150
|
|
|
123
151
|
const setReference = useCallback(
|
|
124
|
-
(node:
|
|
152
|
+
(node: ReferenceType | null) => {
|
|
125
153
|
if (isElement(node) || node === null) {
|
|
126
154
|
domReferenceRef.current = node;
|
|
127
155
|
setDomReference(node);
|
|
@@ -138,7 +166,7 @@ export function useFloating(...args: any[]): any {
|
|
|
138
166
|
subSlot(slot, 'sr'),
|
|
139
167
|
);
|
|
140
168
|
|
|
141
|
-
const refs = useMemo(
|
|
169
|
+
const refs = useMemo<ExtendedRefs<ReferenceType>>(
|
|
142
170
|
() => ({
|
|
143
171
|
...position.refs,
|
|
144
172
|
setReference,
|
|
@@ -149,13 +177,13 @@ export function useFloating(...args: any[]): any {
|
|
|
149
177
|
subSlot(slot, 'm:refs'),
|
|
150
178
|
);
|
|
151
179
|
|
|
152
|
-
const elements = useMemo(
|
|
180
|
+
const elements = useMemo<ExtendedElements<ReferenceType>>(
|
|
153
181
|
() => ({ ...position.elements, domReference }),
|
|
154
182
|
[position.elements, domReference],
|
|
155
183
|
subSlot(slot, 'm:el'),
|
|
156
184
|
);
|
|
157
185
|
|
|
158
|
-
const context = useMemo(
|
|
186
|
+
const context = useMemo<FloatingContext>(
|
|
159
187
|
() => ({ ...position, ...rootContext, refs, elements, nodeId }),
|
|
160
188
|
[position, refs, elements, nodeId, rootContext],
|
|
161
189
|
subSlot(slot, 'm:ctx'),
|
|
@@ -173,7 +201,7 @@ export function useFloating(...args: any[]): any {
|
|
|
173
201
|
subSlot(slot, 'e:ctx'),
|
|
174
202
|
);
|
|
175
203
|
|
|
176
|
-
return useMemo(
|
|
204
|
+
return useMemo<UseFloatingReturn>(
|
|
177
205
|
() => ({ ...position, context, refs, elements }),
|
|
178
206
|
[position, refs, elements, context],
|
|
179
207
|
subSlot(slot, 'm:ret'),
|
package/src/delayGroup.ts
CHANGED
|
@@ -10,12 +10,36 @@ import {
|
|
|
10
10
|
useReducer,
|
|
11
11
|
useRef,
|
|
12
12
|
} from 'octane';
|
|
13
|
+
import type { OctaneNode } from 'octane';
|
|
13
14
|
|
|
14
15
|
import { S, splitSlot, subSlot } from './internal';
|
|
15
16
|
import { getDelay, useModernLayoutEffect } from './utils';
|
|
17
|
+
import type { Delay, FloatingRootContext } from './types';
|
|
18
|
+
|
|
19
|
+
export interface GroupState {
|
|
20
|
+
delay: Delay;
|
|
21
|
+
initialDelay: Delay;
|
|
22
|
+
currentId: any;
|
|
23
|
+
timeoutMs: number;
|
|
24
|
+
isInstantPhase: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GroupContext extends GroupState {
|
|
28
|
+
setCurrentId: (currentId: any) => void;
|
|
29
|
+
setState: (state: Partial<GroupState>) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface UseGroupOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Whether delay grouping should be enabled.
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
id?: any;
|
|
39
|
+
}
|
|
16
40
|
|
|
17
41
|
const NOOP = () => {};
|
|
18
|
-
export const FloatingDelayGroupContext = createContext<
|
|
42
|
+
export const FloatingDelayGroupContext = createContext<GroupContext>({
|
|
19
43
|
delay: 0,
|
|
20
44
|
initialDelay: 0,
|
|
21
45
|
timeoutMs: 0,
|
|
@@ -25,15 +49,39 @@ export const FloatingDelayGroupContext = createContext<any>({
|
|
|
25
49
|
isInstantPhase: false,
|
|
26
50
|
});
|
|
27
51
|
|
|
28
|
-
|
|
52
|
+
/**
|
|
53
|
+
* @deprecated
|
|
54
|
+
* Use the return value of `useDelayGroup()` instead.
|
|
55
|
+
*/
|
|
56
|
+
export const useDelayGroupContext = (): GroupContext => useContext(FloatingDelayGroupContext);
|
|
57
|
+
|
|
58
|
+
export interface FloatingDelayGroupProps {
|
|
59
|
+
children?: OctaneNode;
|
|
60
|
+
/**
|
|
61
|
+
* The delay to use for the group.
|
|
62
|
+
*/
|
|
63
|
+
delay: Delay;
|
|
64
|
+
/**
|
|
65
|
+
* An optional explicit timeout to use for the group, which represents when
|
|
66
|
+
* grouping logic will no longer be active after the close delay completes.
|
|
67
|
+
* This is useful if you want grouping to “last” longer than the close delay,
|
|
68
|
+
* for example if there is no close delay at all.
|
|
69
|
+
*/
|
|
70
|
+
timeoutMs?: number;
|
|
71
|
+
}
|
|
29
72
|
|
|
30
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Provides context for a group of floating elements that should share a
|
|
75
|
+
* `delay`.
|
|
76
|
+
* @see https://floating-ui.com/docs/FloatingDelayGroup
|
|
77
|
+
*/
|
|
78
|
+
export function FloatingDelayGroup(props: FloatingDelayGroupProps): OctaneNode {
|
|
31
79
|
const children = props.children;
|
|
32
80
|
const delay = props.delay;
|
|
33
81
|
const timeoutMs = props.timeoutMs ?? 0;
|
|
34
82
|
|
|
35
83
|
const [state, setState] = useReducer(
|
|
36
|
-
(prev:
|
|
84
|
+
(prev: GroupState, next: Partial<GroupState>): GroupState => ({ ...prev, ...next }),
|
|
37
85
|
{
|
|
38
86
|
delay,
|
|
39
87
|
timeoutMs,
|
|
@@ -69,7 +117,7 @@ export function FloatingDelayGroup(props: any): any {
|
|
|
69
117
|
[state.currentId, state.isInstantPhase],
|
|
70
118
|
S('FloatingDelayGroup:eff'),
|
|
71
119
|
);
|
|
72
|
-
const value = useMemo(
|
|
120
|
+
const value = useMemo<GroupContext>(
|
|
73
121
|
() => ({ ...state, setState, setCurrentId }),
|
|
74
122
|
[state, setCurrentId],
|
|
75
123
|
S('FloatingDelayGroup:value'),
|
|
@@ -77,10 +125,20 @@ export function FloatingDelayGroup(props: any): any {
|
|
|
77
125
|
return createElement(FloatingDelayGroupContext.Provider, { value, children });
|
|
78
126
|
}
|
|
79
127
|
|
|
80
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Enables grouping when called inside a component that's a child of a
|
|
130
|
+
* `FloatingDelayGroup`.
|
|
131
|
+
* @see https://floating-ui.com/docs/FloatingDelayGroup
|
|
132
|
+
*/
|
|
133
|
+
export function useDelayGroup(
|
|
134
|
+
context: FloatingRootContext,
|
|
135
|
+
options?: UseGroupOptions,
|
|
136
|
+
slot?: symbol,
|
|
137
|
+
): GroupContext;
|
|
138
|
+
export function useDelayGroup(...args: any[]): GroupContext {
|
|
81
139
|
const [user, slot] = splitSlot(args);
|
|
82
|
-
const context = user[0];
|
|
83
|
-
const options = (user[1] as
|
|
140
|
+
const context = user[0] as FloatingRootContext;
|
|
141
|
+
const options = (user[1] as UseGroupOptions) ?? {};
|
|
84
142
|
|
|
85
143
|
const open = context.open;
|
|
86
144
|
const onOpenChange = context.onOpenChange;
|