v3: a feed card opens in a new tab on a middle click, wherever you point — the cards are clickable elements rather than links, so only the title, the one real anchor, answered a middle click; the rest of the card, the play button included, did nothing, and a middle press there started the browser's autoscroll instead. A shared useDumpCardLink now gives DumpCard and JournalCard an auxclick that opens the dump in a new tab, with the middle press's default suppressed so autoscroll doesn't swallow it, and a ctrl/cmd/shift click does the same — caught ahead of the play button, so a modified click on a playable card opens it rather than playing it, while a plain click still plays. Anchors inside a card keep their own target: the title, category chips, and a source link's ↗ still open what they point at.
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
64
src/hooks/useDumpCardLink.ts
Normal file
64
src/hooks/useDumpCardLink.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { MouseEvent } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import type { Dump } from "../model.ts";
|
||||
import { dumpUrl } from "../utils/urls.ts";
|
||||
import { markDumpVisited } from "../utils/visited.ts";
|
||||
|
||||
function isOverLink(e: MouseEvent) {
|
||||
return (e.target as Element).closest("a") !== null;
|
||||
}
|
||||
|
||||
function wantsNewTab(e: MouseEvent) {
|
||||
return e.ctrlKey || e.metaKey || e.shiftKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* A feed card is a clickable element rather than a link, so it gets none of a
|
||||
* link's behaviour for free. This gives it back: a middle click (or a
|
||||
* ctrl/cmd/shift click) anywhere on the card opens the dump in a new tab, even
|
||||
* over a play button whose plain click starts the media. Anchors inside the
|
||||
* card (the title, a source link, category chips) keep their own target.
|
||||
*/
|
||||
export function useDumpCardLink(dump: Pick<Dump, "id" | "slug">) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
function openInNewTab() {
|
||||
markDumpVisited(dump.id);
|
||||
globalThis.open(dumpUrl(dump), "_blank", "noopener");
|
||||
}
|
||||
|
||||
function go() {
|
||||
markDumpVisited(dump.id);
|
||||
navigate(dumpUrl(dump));
|
||||
}
|
||||
|
||||
return {
|
||||
/** Navigates in place. */
|
||||
go,
|
||||
/** For click handlers that do something other than navigate (play):
|
||||
* returns true when the click was a new-tab request and was handled. */
|
||||
handledAsNewTab(e: MouseEvent) {
|
||||
if (!wantsNewTab(e) || isOverLink(e)) return false;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openInNewTab();
|
||||
return true;
|
||||
},
|
||||
cardProps: {
|
||||
onClick(e: MouseEvent) {
|
||||
if (wantsNewTab(e)) openInNewTab();
|
||||
else go();
|
||||
},
|
||||
// Windows and Linux browsers start autoscroll on a middle press, which
|
||||
// eats the auxclick that follows.
|
||||
onMouseDown(e: MouseEvent) {
|
||||
if (e.button === 1 && !isOverLink(e)) e.preventDefault();
|
||||
},
|
||||
onAuxClick(e: MouseEvent) {
|
||||
if (e.button !== 1 || isOverLink(e)) return;
|
||||
e.preventDefault();
|
||||
openInNewTab();
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user