feat(person-mention): PR-B2 — read-mode rendering + hover card (issue #362) #371

Merged
marcel merged 18 commits from feat/person-mentions-issue-362-frontend-b2 into main 2026-04-29 13:37:06 +02:00
2 changed files with 43 additions and 0 deletions
Showing only changes of commit 3faac13533 - Show all commits

View File

@@ -143,7 +143,18 @@ function handleMentionLeave(event: Event) {
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) {
if (!isPlainPrimaryClick(event)) return;
const link = event.target as HTMLAnchorElement;
const personId = link.dataset.personId;
if (!personId) return;

View File

@@ -370,6 +370,38 @@ describe('TranscriptionReadView — person-mention rendering', () => {
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 () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ status: 404, ok: false, json: vi.fn() }));