Skip to content

Commit da3cc0d

Browse files
committed
feat(combobox): add useFloatingMenu hook for menu positioning
1 parent ee7217f commit da3cc0d

2 files changed

Lines changed: 65 additions & 0 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useRef } from "react";
2+
3+
// Jest mock for useFloatingMenu: returns stable refs and a marker style so specs can
4+
// assert wiring without depending on a real layout engine (jsdom has none).
5+
export function useFloatingMenu(_open: boolean): any {
6+
const reference = useRef<HTMLDivElement>(null);
7+
const floating = useRef<HTMLDivElement>(null);
8+
return {
9+
refs: {
10+
setReference: reference,
11+
setFloating: floating
12+
},
13+
floatingStyles: {
14+
"--this-is-mocked-from-unit-tests": "true"
15+
},
16+
isPositioned: true
17+
};
18+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { autoUpdate, flip, offset, size, useFloating } from "@floating-ui/react";
2+
import { useMemo } from "react";
3+
4+
// Menu can grow to at most this height; floating-ui shrinks it further when the
5+
// available viewport space is smaller. Mirrors the previous SCSS cap.
6+
const MAX_MENU_HEIGHT = 320;
7+
// Gap between the input and the menu, and breathing room from the viewport edge.
8+
const MENU_OFFSET = 4;
9+
const VIEWPORT_PADDING = 8;
10+
11+
export function useFloatingMenu(open: boolean): ReturnType<typeof useFloating> {
12+
const middleware = useMemo(
13+
() => [
14+
offset(MENU_OFFSET),
15+
flip({
16+
crossAxis: false,
17+
fallbackStrategy: "bestFit",
18+
padding: VIEWPORT_PADDING
19+
}),
20+
size({
21+
padding: VIEWPORT_PADDING,
22+
apply({ rects, elements, availableHeight }) {
23+
Object.assign(elements.floating.style, {
24+
width: `${rects.reference.width}px`,
25+
maxHeight: `${Math.min(availableHeight, MAX_MENU_HEIGHT)}px`
26+
});
27+
}
28+
})
29+
],
30+
[]
31+
);
32+
33+
const result = useFloating({
34+
open,
35+
placement: "bottom-start",
36+
strategy: "fixed",
37+
middleware,
38+
whileElementsMounted: autoUpdate
39+
});
40+
41+
const floatingStyles =
42+
open && result.isPositioned
43+
? result.floatingStyles
44+
: { ...result.floatingStyles, visibility: "hidden" as const };
45+
46+
return { ...result, floatingStyles };
47+
}

0 commit comments

Comments
 (0)