From 2a5660e01e1b2c012dd37b6cc00c9d69768ec54d Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 12 Jun 2026 17:39:08 +0200 Subject: [PATCH] feat(player): polish the description popover (opaque, portaled, opens upward) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The popover bled through (it used the translucent glass surface), was clipped by the modal card's overflow, and ran off the bottom of the viewport. Make it an opaque surface, render it through a portal to with fixed positioning so nothing clips it, and anchor it above the title so it grows upward where there's room. Add a small hover grace so the pointer can travel title → popover. --- frontend/src/components/PlayerModal.tsx | 52 +++++++++++++++++++++---- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/components/PlayerModal.tsx index d91e56c..56a0fc4 100644 --- a/frontend/src/components/PlayerModal.tsx +++ b/frontend/src/components/PlayerModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; import { useQuery } from "@tanstack/react-query"; import { Check, CheckCheck, X } from "lucide-react"; import { api, type Video } from "../lib/api"; @@ -71,8 +72,29 @@ export default function PlayerModal({ onState(video.id, next); }; - // Lazy description (fetched only when the title is hovered). + // Lazy description (fetched only when the title is hovered). The popover is + // portaled to with fixed positioning so the modal card's overflow-y-auto + // can't clip it. A small close grace lets the mouse travel title → popover. const [showDesc, setShowDesc] = useState(false); + // `bottom` anchors the popover just above the title so it grows upward (over the + // player) instead of downward off-screen / behind the OS taskbar. + const [descRect, setDescRect] = useState<{ left: number; bottom: number; width: number } | null>( + null + ); + const titleRef = useRef(null); + const closeTimer = useRef(undefined); + const openDesc = () => { + window.clearTimeout(closeTimer.current); + const el = titleRef.current; + if (el) { + const r = el.getBoundingClientRect(); + setDescRect({ left: r.left, bottom: window.innerHeight - r.top + 8, width: r.width }); + } + setShowDesc(true); + }; + const scheduleCloseDesc = () => { + closeTimer.current = window.setTimeout(() => setShowDesc(false), 150); + }; const detail = useQuery({ queryKey: ["video-detail", video.id], queryFn: () => api.videoDetail(video.id), @@ -91,6 +113,7 @@ export default function PlayerModal({ return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = prevOverflow; + window.clearTimeout(closeTimer.current); }; }, [onClose]); @@ -185,15 +208,28 @@ export default function PlayerModal({ {/* Title row — title (with hover description) on the left, Close on the right. */}
setShowDesc(true)} - onMouseLeave={() => setShowDesc(false)} + ref={titleRef} + className="min-w-0 flex-1" + onMouseEnter={openDesc} + onMouseLeave={scheduleCloseDesc} >

{video.title}

- {showDesc && ( -
+
+ {showDesc && + descRect && + createPortal( +
Description
@@ -206,9 +242,9 @@ export default function PlayerModal({ ) : (
No description.
)} -
+
, + document.body )} -