From aad2e01777f05f2037ed463e05cee9e8fc2a4283 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:10:27 +0100 Subject: [PATCH] fix: print a contact even when the browser blocks pop-ups --- .../contacts/__tests__/contact-print.test.ts | 26 +++++++++++++ components/contacts/contact-print.ts | 39 +++++++++++++++++-- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/components/contacts/__tests__/contact-print.test.ts b/components/contacts/__tests__/contact-print.test.ts index e229e4100..f5f277a9f 100644 --- a/components/contacts/__tests__/contact-print.test.ts +++ b/components/contacts/__tests__/contact-print.test.ts @@ -76,6 +76,32 @@ describe('printContact', () => { expect(printWindow.document.write).not.toHaveBeenCalled(); }); + it('prints from a hidden frame in the page when the browser blocks the print window', async () => { + open.mockReturnValue(null); + await printContact({ ...base, emails: { e: { address: 'ann@example.com' } } } as ContactCard, LABELS); + + const frame = document.querySelector('iframe[data-contact-print-frame]') as HTMLIFrameElement; + expect(frame).not.toBeNull(); + expect(frame.srcdoc).toContain('

Ann Lee

'); + expect(frame.srcdoc).toContain('
ann@example.com
'); + vi.spyOn(frame.contentWindow!, 'focus').mockImplementation(() => {}); + const print = vi.spyOn(frame.contentWindow!, 'print').mockImplementation(() => {}); + + // The empty start page some browsers load first is not printed. + frame.dispatchEvent(new Event('load')); + expect(print).not.toHaveBeenCalled(); + + frame.contentDocument!.body.innerHTML = '

Ann Lee

'; + frame.dispatchEvent(new Event('load')); + expect(print).toHaveBeenCalledOnce(); + + // Gone once the print dialog closes. + vi.useFakeTimers({ toFake: ['setTimeout'] }); + frame.contentWindow!.dispatchEvent(new Event('afterprint')); + vi.runAllTimers(); + expect(document.querySelector('iframe[data-contact-print-frame]')).toBeNull(); + }); + it('prints the headings and dates in the language of the app', async () => { const de = printLabels((key) => ({ 'detail.emails': 'E-Mail-Adressen', 'detail.anniversaries': 'Jahrestage', 'detail.anniversary_birth': 'Geburtstag' } as Record)[key] ?? key, 'de'); await printContact({ diff --git a/components/contacts/contact-print.ts b/components/contacts/contact-print.ts index 24fa9e706..d34a51286 100644 --- a/components/contacts/contact-print.ts +++ b/components/contacts/contact-print.ts @@ -84,15 +84,44 @@ export function printLabels(t: (key: string) => string, locale: string): PrintLa }; } +const PRINT_FRAME_ATTR = "data-contact-print-frame"; + +/** + * Prints the sheet from a hidden frame in this page, for when the browser + * blocked the print window. The frame goes away after printing, or when the + * next sheet is printed. + */ +function printInFrame(html: string): void { + document.querySelectorAll(`iframe[${PRINT_FRAME_ATTR}]`).forEach((f) => f.remove()); + const frame = document.createElement("iframe"); + frame.setAttribute(PRINT_FRAME_ATTR, ""); + frame.setAttribute("aria-hidden", "true"); + frame.tabIndex = -1; + frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;visibility:hidden"; + // Printed once the sheet and its photo have loaded. Some browsers report + // the frame's empty start page first; that one is skipped. + let printed = false; + frame.addEventListener("load", () => { + const frameWindow = frame.contentWindow; + if (printed || !frameWindow || !frame.contentDocument?.querySelector("h1")) return; + printed = true; + frameWindow.addEventListener("afterprint", () => setTimeout(() => frame.remove(), 0), { once: true }); + frameWindow.focus(); + frameWindow.print(); + }); + frame.srcdoc = html; + document.body.appendChild(frame); +} + export async function printContact(contact: ContactCard, labels: PrintLabels, displayName?: string): Promise { // Open the window before anything is awaited: browsers allow the popup only - // while the click that asked for it is being handled. + // while the click that asked for it is being handled. When the popup is + // blocked, the sheet is printed from a hidden frame instead. const printWindow = window.open("", "_blank"); - if (!printWindow) return; const name = displayName || getContactDisplayName(contact); const photoUri = await printablePhotoUri(contact); - if (printWindow.closed) return; + if (printWindow?.closed) return; const emails = contact.emails ? Object.values(contact.emails) : []; const phones = contact.phones ? Object.values(contact.phones) : []; const orgs = contact.organizations ? Object.values(contact.organizations) : []; @@ -173,6 +202,10 @@ ${rows.join("")} `; + if (!printWindow) { + printInFrame(html); + return; + } printWindow.document.write(html); printWindow.document.close(); printWindow.focus();