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 )} -