From 57cf55cc48d536589b975ac1a0d7880c0144662d Mon Sep 17 00:00:00 2001 From: khannurien Date: Sun, 21 Jun 2026 14:47:25 +0000 Subject: [PATCH] v3: fixed votes/likes popover placement --- src/App.css | 4 +-- src/components/UserListPopover.tsx | 53 +++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/App.css b/src/App.css index 3c11fa8..a066c4e 100644 --- a/src/App.css +++ b/src/App.css @@ -2190,9 +2190,9 @@ body.has-player .fab-new { } .user-list-popover { - /* position/top/left set inline (fixed, anchored to the trigger's viewport rect) */ + /* position/top/bottom/left/max-height set inline — anchored to the + trigger's viewport rect, flipping above it when there isn't room below */ min-width: 180px; - max-height: 280px; overflow-y: auto; background: var(--color-surface); border: 2px solid var(--color-border-subtle); diff --git a/src/components/UserListPopover.tsx b/src/components/UserListPopover.tsx index 597ad88..82c230e 100644 --- a/src/components/UserListPopover.tsx +++ b/src/components/UserListPopover.tsx @@ -31,18 +31,59 @@ type State = loadingMore: boolean; }; +interface Placement { + left: number; + top?: number; + bottom?: number; + maxHeight: number; +} + +const PREFERRED_MAX_HEIGHT = 280; +const VIEWPORT_MARGIN = 8; +const MIN_USABLE_HEIGHT = 100; + export function UserListPopover( { anchorRef, onClose, fetchUrl }: UserListPopoverProps, ) { const { token } = useAuth(); const [state, setState] = useState({ status: "loading" }); - const [rect, setRect] = useState(null); + const [placement, setPlacement] = useState(null); const popoverRef = useRef(null); // Position via the anchor's viewport rect (mirrors Tooltip.tsx) — fixed + // portaled to so the popover can't be clipped by a card's overflow. + // Flips above the anchor (anchored from the viewport bottom edge, so it + // grows upward) when there isn't enough room below, so a long list never + // overflows past the bottom of the viewport. useEffect(() => { - setRect(anchorRef.current?.getBoundingClientRect() ?? null); + const anchorRect = anchorRef.current?.getBoundingClientRect(); + if (!anchorRect) { + setPlacement(null); + return; + } + const spaceBelow = globalThis.innerHeight - anchorRect.bottom - + VIEWPORT_MARGIN; + const spaceAbove = anchorRect.top - VIEWPORT_MARGIN; + + if (spaceBelow >= MIN_USABLE_HEIGHT || spaceBelow >= spaceAbove) { + setPlacement({ + left: anchorRect.left, + top: anchorRect.bottom + 4, + maxHeight: Math.max( + MIN_USABLE_HEIGHT, + Math.min(PREFERRED_MAX_HEIGHT, spaceBelow), + ), + }); + } else { + setPlacement({ + left: anchorRect.left, + bottom: globalThis.innerHeight - anchorRect.top + 4, + maxHeight: Math.max( + MIN_USABLE_HEIGHT, + Math.min(PREFERRED_MAX_HEIGHT, spaceAbove), + ), + }); + } }, [anchorRef]); useEffect(() => { @@ -131,7 +172,7 @@ export function UserListPopover( }); }; - if (!rect) return null; + if (!placement) return null; return createPortal(
{state.status === "loading" && (