fix(ui): bound dropdowns during scroll

This commit is contained in:
Codex
2026-07-27 14:36:35 +03:00
parent a8c8bfd89a
commit c5a6a58948
8 changed files with 100 additions and 14 deletions
+23 -7
View File
@@ -37,8 +37,18 @@ export function computeFloatingPosition({
viewportHeight = typeof window === "undefined" ? 1080 : window.innerHeight,
viewportPadding = 8,
}: FloatingPositionOptions): FloatingPosition {
const roomBelow = viewportHeight - anchor.bottom - offset - viewportPadding;
const roomAbove = anchor.top - offset - viewportPadding;
const viewportAvailableHeight = Math.max(
0,
viewportHeight - viewportPadding * 2,
);
const roomBelow = Math.min(
viewportAvailableHeight,
Math.max(0, viewportHeight - anchor.bottom - offset - viewportPadding),
);
const roomAbove = Math.min(
viewportAvailableHeight,
Math.max(0, anchor.top - offset - viewportPadding),
);
const requestedTop = placement.startsWith("top");
const shouldFlip = requestedTop
? surfaceHeight > roomAbove && roomBelow > roomAbove
@@ -56,12 +66,18 @@ export function computeFloatingPosition({
);
const availableHeight = resolvedPlacement.startsWith("top") ? roomAbove : roomBelow;
const maxHeight = Math.max(96, availableHeight);
const maxHeight = Math.min(
viewportAvailableHeight,
Math.max(Math.min(96, viewportAvailableHeight), availableHeight),
);
const visibleHeight = Math.min(surfaceHeight, maxHeight);
const top = resolvedPlacement.startsWith("top")
? Math.max(viewportPadding, anchor.top - offset - visibleHeight)
: Math.min(anchor.bottom + offset, viewportHeight - visibleHeight - viewportPadding);
const desiredTop = resolvedPlacement.startsWith("top")
? anchor.top - offset - visibleHeight
: anchor.bottom + offset;
const top = Math.min(
Math.max(viewportPadding, desiredTop),
Math.max(viewportPadding, viewportHeight - visibleHeight - viewportPadding),
);
return { top, left, maxHeight, resolvedPlacement };
}
+10 -1
View File
@@ -41,6 +41,16 @@ export function createFloatingLayer({
const reposition = () => {
if (!openState) return;
const anchor = trigger.getBoundingClientRect();
const anchorVisible = (
anchor.bottom > 0
&& anchor.top < window.innerHeight
&& anchor.right > 0
&& anchor.left < window.innerWidth
);
if (!anchorVisible) {
close();
return;
}
const measured = surface.getBoundingClientRect();
const surfaceWidth = Math.max(minWidth, matchTriggerWidth ? anchor.width : 0, measured.width);
const surfaceHeight = Math.max(1, surface.scrollHeight);
@@ -128,4 +138,3 @@ export function createFloatingLayer({
},
};
}
+11 -1
View File
@@ -67,6 +67,16 @@ export function Dropdown({
const anchorNode = anchorElement ?? triggerElement;
if (!anchorNode || !surfaceRef.current) return;
const anchor = anchorNode.getBoundingClientRect();
const anchorVisible = (
anchor.bottom > 0
&& anchor.top < window.innerHeight
&& anchor.right > 0
&& anchor.left < window.innerWidth
);
if (!anchorVisible) {
close();
return;
}
const measured = surfaceRef.current.getBoundingClientRect();
const surfaceWidth = typeof width === "number"
? width
@@ -86,7 +96,7 @@ export function Dropdown({
maxHeight: position.maxHeight,
visibility: "visible",
});
}, [anchorElement, minWidth, offset, placement, triggerElement, width]);
}, [anchorElement, close, minWidth, offset, placement, triggerElement, width]);
useLayoutEffect(() => {
if (!isOpen) return;