@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.
@@ -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
- export const FloatingListContext = createContext<any>({
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 function FloatingList(props: any): any {
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<any>(), S('FloatingList:nodes'));
71
+ const [nodes, setNodes] = useState(() => new Set<Node>(), S('FloatingList:nodes'));
44
72
  const register = useCallback(
45
- (node: any) => {
46
- setNodes((prevSet: Set<any>) => new Set(prevSet).add(node));
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: any) => {
53
- setNodes((prevSet: Set<any>) => {
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 any) ?? {};
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<any>(null, subSlot(slot, 'index'));
90
- const componentRef = useRef<any>(null, subSlot(slot, 'ref'));
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: any) => {
138
+ (node: HTMLElement | null) => {
94
139
  componentRef.current = node;
95
140
  if (index !== null) {
96
141
  elementsRef.current[index] = node;
@@ -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 function FloatingOverlay(props: any): any {
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
  }
@@ -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: any = {
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(props: any): any {
46
- const [role, setRole] = useState<any>(undefined, S('FocusGuard:role'));
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: any = {
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
- export const PortalContext = createContext<any>(null);
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 function useFloatingPortalNode(...args: any[]): any {
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 any) ?? {};
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<any>(null, subSlot(slot, 'node'));
87
- const portalNodeRef = useRef<any>(null, subSlot(slot, 'noderef'));
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 = root || portalContext?.portalNode;
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 function FloatingPortal(props: any): any {
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([{ id, root }, S('FloatingPortal:node')]);
155
- const [focusManagerState, setFocusManagerState] = useState<any>(null, S('FloatingPortal:fms'));
156
- const beforeOutsideRef = useRef<any>(null, S('FloatingPortal:bo'));
157
- const afterOutsideRef = useRef<any>(null, S('FloatingPortal:ao'));
158
- const beforeInsideRef = useRef<any>(null, S('FloatingPortal:bi'));
159
- const afterInsideRef = useRef<any>(null, S('FloatingPortal:ai'));
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: any) {
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: any) => {
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: any) => {
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(options: any, slot: symbol | undefined): any {
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<any>({}, subSlot(slot, 'data'));
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?: string) => {
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 any) ?? {};
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(null, subSlot(slot, 'posref'));
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<any>(null, subSlot(slot, 'domrefref'));
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: any) => {
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: any) => {
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<any>({
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
- export const useDelayGroupContext = () => useContext(FloatingDelayGroupContext);
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
- export function FloatingDelayGroup(props: any): any {
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: any, next: any) => ({ ...prev, ...next }),
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
- export function useDelayGroup(...args: any[]): any {
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 any) ?? {};
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;