The overlay history stack let a navigation land ABOVE open overlays and "buried" their entries with a counter. Measured in Chrome: Shift+W over About navigated under the still-open dialog, the next Back both undid the navigation and closed About, and the Back after that did nothing; Shift+W on Home with the player open did nothing at all. The counter could also be spent by a different overlay closing first, and callers that could not report whether they pushed were assumed to have. setPage and openChannel now close every open overlay and push only once the history is back in sync (one entry per live overlay, no programmatic back() in flight, no reconcile queued), so nothing is ever buried and the whole mechanism goes: navigateOverOverlays, closeThenNavigate, buryUnder, setPage's boolean, and the three test files that pinned them. The palette closes only itself before running a row, so a theme picked over the player keeps the player. Shift+W on Home with the player open now closes it (owner decision 2026-09-09). The wait is on state, not on "a popstate": the old 250 ms fallback could run with back()s still in flight. An overlay that refuses to close within 1.5 s drops the command instead of being navigated out from under.
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 lib/history.closeOverlaysThenNavigate).
|
|
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;
|