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 1/3] 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("")}