fix(person-mention): respect modified-click and middle-click for new-tab nav
Felix #7: handleMentionClick unconditionally preventDefault'd and goto'd, breaking ctrl-click / cmd-click / shift-click / alt-click / middle-click — "open in new tab" is a real workflow for researchers comparing two persons. Add isPlainPrimaryClick() guard. Modified clicks fall through to the browser's default anchor handling (the <a href="/persons/{id}"> opens in the new tab as expected). Plain left-clicks still SPA-navigate via goto(). Three new tests assert ctrl-click, meta-click, and middle-click are not preventDefault'd. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -143,7 +143,18 @@ function handleMentionLeave(event: Event) {
|
|||||||
activeCard = null;
|
activeCard = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modified clicks (ctrl/meta/shift/alt) and middle-clicks must fall through to
|
||||||
|
* the browser's default anchor behaviour so users can open the person page in
|
||||||
|
* a new tab/window. Felix #7. Only the plain primary-button click navigates
|
||||||
|
* via SPA goto().
|
||||||
|
*/
|
||||||
|
function isPlainPrimaryClick(event: MouseEvent): boolean {
|
||||||
|
return event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleMentionClick(event: MouseEvent) {
|
async function handleMentionClick(event: MouseEvent) {
|
||||||
|
if (!isPlainPrimaryClick(event)) return;
|
||||||
const link = event.target as HTMLAnchorElement;
|
const link = event.target as HTMLAnchorElement;
|
||||||
const personId = link.dataset.personId;
|
const personId = link.dataset.personId;
|
||||||
if (!personId) return;
|
if (!personId) return;
|
||||||
|
|||||||
@@ -370,6 +370,38 @@ describe('TranscriptionReadView — person-mention rendering', () => {
|
|||||||
expect(card).toBeNull();
|
expect(card).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('lets ctrl-click and meta-click fall through so users can open in a new tab', async () => {
|
||||||
|
render(TranscriptionReadView, {
|
||||||
|
blocks: [mentionBlock],
|
||||||
|
onParagraphClick: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = document.querySelector('a.person-mention')! as HTMLAnchorElement;
|
||||||
|
|
||||||
|
// ctrl-click (Linux/Win "open in new tab")
|
||||||
|
const ctrlClick = new MouseEvent('click', { bubbles: true, cancelable: true, ctrlKey: true });
|
||||||
|
const ctrlPrevented = !link.dispatchEvent(ctrlClick);
|
||||||
|
expect(ctrlPrevented).toBe(false);
|
||||||
|
|
||||||
|
// meta-click (macOS "open in new tab")
|
||||||
|
const metaClick = new MouseEvent('click', { bubbles: true, cancelable: true, metaKey: true });
|
||||||
|
const metaPrevented = !link.dispatchEvent(metaClick);
|
||||||
|
expect(metaPrevented).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets middle-click fall through so users can open in a background tab', async () => {
|
||||||
|
render(TranscriptionReadView, {
|
||||||
|
blocks: [mentionBlock],
|
||||||
|
onParagraphClick: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = document.querySelector('a.person-mention')! as HTMLAnchorElement;
|
||||||
|
// button === 1 is middle mouse button
|
||||||
|
const middleClick = new MouseEvent('click', { bubbles: true, cancelable: true, button: 1 });
|
||||||
|
const prevented = !link.dispatchEvent(middleClick);
|
||||||
|
expect(prevented).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('degrades to plain unlinked text when the person fetch returns 404', async () => {
|
it('degrades to plain unlinked text when the person fetch returns 404', async () => {
|
||||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ status: 404, ok: false, json: vi.fn() }));
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ status: 404, ok: false, json: vi.fn() }));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user