fix(player): a link that opens the cut text is scrolled back into view

Focus had scrolled the clamped box itself; opened, the link dropped by that much, out of a scrolling panel's sight. DESIGN states where Close sits in the stacked layout; a stale test comment goes.
This commit is contained in:
2026-10-09 23:46:40 +02:00
parent 2cff4ad954
commit 77638f7a0b
5 changed files with 21 additions and 7 deletions
@@ -424,8 +424,6 @@ describe("PlayerModal — the description", () => {
});
});
// C3: the queue's side arrows sit in a gutter of the card's row, not outside it, so no window cuts
// them off (outside the card, a 1024 window cut them and a portrait tablet lost them).
// The full-height strips beside the card were a second Previous / Next that cost the video 7rem of
// width (owner): the panel's transport row and Shift+← / → step on their own.
describe("PlayerModal — no side strips", () => {
+2 -2
View File
@@ -671,8 +671,8 @@ export default function PlayerModal({
// The card's width budget (its maxWidth, below): the video's, and the side panel's when wide.
const fitWidth = `(100dvh - 3rem - ${below}px) * 16 / 9`;
const panelWidth = panelShut ? PANEL_STRIP : SIDE_PANEL;
// One Close, icon only, in the card's top-right corner: the wide panel's head (or its strip), the
// stacked card's title row.
// One Close, icon only: at the top right of the wide panel's head (or its strip), at the end of the
// stacked card's title row under the video.
const closeButton = (
<IconButton
variant="ghost"
@@ -65,8 +65,13 @@ describe("ClampedText", () => {
// Chrome: 45 px), so on screen the link sits inside the box: only the text's own top tells.
clamp.scrollTop = 240;
vi.spyOn(screen.getByText("cut"), "getBoundingClientRect").mockReturnValue(rect(130, 150));
// Opened, the link drops back to its place in the text, as far as the box had scrolled: in a
// scrolling panel that is out of sight, so it is brought back into view.
const reveal = vi.fn();
screen.getByText("cut").scrollIntoView = reveal;
act(() => screen.getByText("cut").focus());
expect(screen.getByText("Less").getAttribute("aria-expanded")).toBe("true");
expect(reveal).toHaveBeenCalledWith({ block: "nearest" });
} finally {
tall.mockRestore();
box.mockRestore();
+11 -1
View File
@@ -49,12 +49,22 @@ export function ClampedText({
}, [text, expanded]);
// Measured from the text's top, not against the box on screen: focus scrolls the clamped box itself
// to bring the link into view (overflow:hidden still scrolls), so on screen it looked "in sight".
const revealing = useRef<Element | null>(null);
const revealFocused = (e: FocusEvent<HTMLDivElement>) => {
const box = e.currentTarget;
if (expanded || e.target === box) return;
const top = box.getBoundingClientRect().top - box.scrollTop;
if (e.target.getBoundingClientRect().bottom - top > box.clientHeight + 1) setExpanded(true);
if (e.target.getBoundingClientRect().bottom - top <= box.clientHeight + 1) return;
revealing.current = e.target;
setExpanded(true);
};
// That scroll was the box's own: opened, the link drops back to its place in the text, as far below
// as the box had scrolled, and out of a scrolling panel's sight. Brought back into view.
useLayoutEffect(() => {
const el = revealing.current;
revealing.current = null;
if (expanded && el?.isConnected) el.scrollIntoView({ block: "nearest" });
}, [expanded]);
return (
<div className={className}>
<div