Deep-review R6 — the small items: - The video card's channel name was still a <button> (pre-existing): ctrl/middle-click did nothing there while the title above it and the player's channel name honoured them. It renders through ChannelNameLink now, which grew a forwarded ref (the clamp measurement), pass-through attributes, a `tooltip`, and an opt-in `stopPropagation` so no click reaches the card's row. - ChannelNameLink rendered an empty, still-tabbable <a> for a channel with no title yet (a stub first seen through a live search; pre-existing at this site). It falls back to the translated "Channel". - The feed's SortKey→SortOption mapping existed twice in Feed.tsx; one `feedSortOptions` in feedSort.ts serves both modes (DIRECTIONLESS is module-private again). - The backfill refresh's comment claimed a one-page cost. It refetches every loaded page of the grid, and that is the right price: backfilled videos are older, so they land below the head. Tests for call sites that could be deleted with every lane green: the channel page running the backfill refresh (knip had been blinded by the hook test's import), Feed handing the grid the live results in the chosen key AND direction, the card using the link. Plus the alt-click arm of the shared click predicate, and a plex filter-chip test that claimed it made no request while firing a real one. Verified on localdev: the feed's channel names are anchors (same 14 px muted look), a plain click opens the channel in-app. 7 mutations, each red on its assertion.
53 lines
2.2 KiB
TypeScript
53 lines
2.2 KiB
TypeScript
import { forwardRef, type AnchorHTMLAttributes } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { isPlainLeftClick } from "../lib/nav";
|
|
import { urlWithParams } from "../lib/urlState";
|
|
|
|
// A channel name that is a REAL link. The player showed it as a `<button>`, so the browser's own
|
|
// gestures did nothing on it: middle-click and Ctrl/Cmd-click opened no tab, on a thing that looks
|
|
// exactly like a link (the YouTube icon right next to it IS an anchor and does open one). The video
|
|
// card's channel name had the same `<button>`, so it renders through here too.
|
|
//
|
|
// The href is the same URL `openChannel` pushes — `?chan=<id>` on the current page — so a new tab
|
|
// lands where the in-app navigation would have. A plain left click is still handled in-app, because
|
|
// the SPA path does more than navigate (the player has to close first and reclaim its own
|
|
// Back-to-close history entry; see closeThenNavigate).
|
|
type Props = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href" | "onClick" | "title"> & {
|
|
channelId: string;
|
|
title: string | null | undefined;
|
|
/** The in-app navigation, run only for a plain left click. */
|
|
onOpenInApp: () => void;
|
|
/** The native tooltip (`title` is taken by the channel's name). */
|
|
tooltip?: string;
|
|
/** Keep EVERY click on the link — the browser's own gestures too — from reaching a clickable
|
|
* ancestor (the video card's row). */
|
|
stopPropagation?: boolean;
|
|
};
|
|
|
|
const ChannelNameLink = forwardRef<HTMLAnchorElement, Props>(function ChannelNameLink(
|
|
{ channelId, title, onOpenInApp, tooltip, stopPropagation, ...rest },
|
|
ref,
|
|
) {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<a
|
|
{...rest}
|
|
ref={ref}
|
|
href={urlWithParams({ chan: channelId })}
|
|
title={tooltip}
|
|
onClick={(e) => {
|
|
if (stopPropagation) e.stopPropagation();
|
|
if (!isPlainLeftClick(e)) return; // let the browser open its tab/window
|
|
e.preventDefault();
|
|
onOpenInApp();
|
|
}}
|
|
>
|
|
{/* A channel first seen through a live search can have no title yet. An empty anchor would
|
|
still be in the tab order, announced by nothing but its href. */}
|
|
{title || t("player.channel")}
|
|
</a>
|
|
);
|
|
});
|
|
|
|
export default ChannelNameLink;
|