Files
siftlode/frontend/src/components/AboutCell.tsx
T
peter 51f9c8d0ed feat(a11y): R9 focus foundation — glass shadow fix, focus-visible ring, highlight/selected split
S1 — recipe foundation:
- Move .glass/.glass-card/.glass-menu into @layer components so a JSX shadow-* utility overrides the
  glass box-shadow again; the ~9 dead shadow-* on glass surfaces (modals, menus, save-bar, chat) now
  paint their intended drop-shadow. Perf-mode and :hover overrides stay unlayered so they still win.
- Add a .selectable recipe: the hover tint menu/list rows already use, PLUS the app-wide
  focus-visible ring that was missing on non-input controls (the biggest a11y gap).
- Split the popover roving highlight from the selected state (AnchoredPopover): a selected/checked
  PopoverItem now gets a ring ONLY, so its resting paint (bg-accent fill / text-accent / checkbox)
  survives being highlighted instead of downgrading to a 35% tint or being recoloured. Fixes the 4
  review findings deferred from R10-S0 generically; drops the leftover per-consumer data-active ring
  in tableHeader.

S2 — worst offenders:
- Account-menu trigger + switch/add/sign-out rows adopt .selectable (visible keyboard focus).
- Saved-views rows: group-focus-within reveals the hover-only action buttons for keyboard users;
  aria-current on the active row; focus-visible ring on every button.
- The 3 invisible-focus inputs get an accent focus ring: SearchBar + ShareDialog via focus-within on
  the glass pill/row, AboutCell via focus-visible on the trigger.

Deferred to a later R9 sprint: the full unification of all ~14 selection idioms, a shared <NavItem>,
and .is-current/.chip recipes (no consumer yet in this pass).

Verified: tsc/eslint/prettier/knip/vitest (79) green; browser computed-style checks confirm the glass
shadow now wins, the focus outlines render (2px accent), and the split renders (selected ring-only,
unselected fill).
2026-08-08 04:27:51 +02:00

66 lines
2.6 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 "./ChannelAbout";
import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover";
/**
* 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 R9 (focus/a11y), not to the mechanical migration.
*/
export default function AboutCell({ text, channelId }: { text: string | null; channelId: string }) {
const preview = (text ?? "").trim();
const [open, setOpen] = useState(false);
const pop = useAnchoredPopover({
open: open && !!preview,
onOpenChange: setOpen,
mode: "hover",
offset: 6,
// Pop instantly like the old cell did; the close grace lets the cursor reach the card.
hoverDelay: { open: 0, close: 160 },
});
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={pop.refs.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/60 focus-visible:outline-offset-2"
>
{preview}
</span>
<PopoverSurface
popover={pop}
className="glass w-[480px] max-h-112 overflow-y-auto px-3.5 py-3 rounded-lg animate-[fadeIn_0.12s_ease]"
>
{/* 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>
</>
);
}