- A press that begins while a field in the row is edited only ends the editing (rowOpen latches it at pointerdown); the card layout's row press is now tested. - A related video reset in the player forgets the session's stored position. - Removing the playing video passes over known-dead items and steps the way it went. - Prev/Next off only because of dead items stay focusable and say why (HU/EN). - The docked bar's scroller leaves room for focus rings. - A focus-opened About card is not closed by its own scroll into view. - EN "Clear watch progress" for the video reset, distinct from a page's "Reset view". - The signed-out title read goes to the page yt-dlp read; the retitle writes are conditional. - Tests: the cap of three errors, the bar's own Fullscreen, the group glyph, the backfill's reads.
89 lines
4.0 KiB
TypeScript
89 lines
4.0 KiB
TypeScript
import { useState } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { qk } from "../../lib/queryKeys";
|
|
import { api } from "../../lib/api";
|
|
import ChannelAboutContent from "../../features/channel/ChannelAbout";
|
|
import { PopoverSurface } from "../../components/ui/AnchoredPopover";
|
|
import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
|
import { SURFACE } from "../../components/ui/surfaces";
|
|
|
|
/**
|
|
* A channel-description cell: one truncated line in the table, revealing the FULL channel "About"
|
|
* (the same content as the detail page's About tab — description, links, country, language, topics,
|
|
* keywords) in a glass hover card. The detail is fetched ON DEMAND on hover/focus, sharing the
|
|
* ["channel", id] react-query cache with the channel page, so the list stays light. The card is
|
|
* interactive (scrollable, links clickable) — the platform's safePolygon lets the cursor travel the
|
|
* gap, replacing the hand-rolled 160 ms hide-timer bridge. Shared by the subscribed + discovery
|
|
* tables.
|
|
*
|
|
* The card keeps the platform's default hover role (`tooltip`); making this rich, interactive panel
|
|
* properly keyboard-reachable belongs to the focus/a11y work, not to the mechanical migration.
|
|
*/
|
|
// How long the pointer rests on a cell before its card opens.
|
|
const ABOUT_REST_MS = 500;
|
|
|
|
export default function AboutCell({ text, channelId }: { text: string | null; channelId: string }) {
|
|
const preview = (text ?? "").trim();
|
|
const [open, setOpen] = useState(false);
|
|
// Opened by the keyboard's focus: the scroll that brings a focused cell into view is that focus's
|
|
// own, and would put the card away the moment it opened — so only a pointer's card goes on a scroll.
|
|
const [byFocus, setByFocus] = useState(false);
|
|
|
|
const pop = useAnchoredPopover({
|
|
open: open && !!preview,
|
|
onOpenChange: (next, _event, reason) => {
|
|
setByFocus(next && reason === "focus");
|
|
setOpen(next);
|
|
},
|
|
mode: "hover",
|
|
offset: 6,
|
|
// Opens once the pointer rests on the cell, not as it crosses one: scrolling the table under a
|
|
// still pointer, or moving down the column, used to pop card after card. The close grace lets
|
|
// the cursor reach the card; a scroll puts an open one away.
|
|
hoverDelay: { open: ABOUT_REST_MS, close: 160 },
|
|
dismissOnScroll: !byFocus,
|
|
// Cap the panel to the space between the trigger and the viewport edge (publishes --fit-max-h) so a
|
|
// long About never runs its top off-screen — at high browser zoom the short viewport made a fixed
|
|
// max-height overflow above the top edge, where the scroll container can't reach.
|
|
fitHeight: true,
|
|
});
|
|
|
|
const { setReference } = pop.refs;
|
|
|
|
const { data: ch } = useQuery({
|
|
queryKey: qk.channel(channelId),
|
|
queryFn: () => api.channelDetail(channelId),
|
|
enabled: open && !!channelId,
|
|
staleTime: 5 * 60_000,
|
|
});
|
|
|
|
if (!preview) return <span className="text-muted">—</span>;
|
|
|
|
return (
|
|
<>
|
|
<span
|
|
ref={setReference}
|
|
{...pop.getReferenceProps()}
|
|
tabIndex={0}
|
|
className="block max-w-88 truncate text-muted cursor-help rounded-sm outline-hidden focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2"
|
|
>
|
|
{preview}
|
|
</span>
|
|
<PopoverSurface
|
|
popover={pop}
|
|
className={`${SURFACE.popover} w-120 overflow-y-auto px-3.5 py-3 animate-[fadeIn_0.12s_ease]`}
|
|
// min(design cap, available space): keeps the 28rem cap on a tall viewport, shrinks to fit on a
|
|
// short one (high zoom). --fit-max-h is set by the fitHeight `size` middleware once positioned.
|
|
style={{ maxHeight: "min(28rem, var(--fit-max-h, 28rem))" }}
|
|
>
|
|
{/* Full About once the detail loads; until then the list's truncated description. */}
|
|
{ch ? (
|
|
<ChannelAboutContent ch={ch} />
|
|
) : (
|
|
<p className="text-sm whitespace-pre-wrap wrap-break-word leading-relaxed">{preview}</p>
|
|
)}
|
|
</PopoverSurface>
|
|
</>
|
|
);
|
|
}
|