From eba71d9be494d9646a592f414770de34219f6398 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 1 Sep 2026 13:17:48 +1000 Subject: [PATCH] fix: prevent dialogs from closing on blur (#3314) --- .../e2e/templates/template-bulk-send.spec.ts | 80 +++++++++++++++++++ packages/ui/primitives/dropdown-menu.tsx | 13 +++ patches/@radix-ui+react-menu+2.1.24.patch | 44 ++++++++++ 3 files changed, 137 insertions(+) create mode 100644 packages/app-tests/e2e/templates/template-bulk-send.spec.ts create mode 100644 patches/@radix-ui+react-menu+2.1.24.patch diff --git a/packages/app-tests/e2e/templates/template-bulk-send.spec.ts b/packages/app-tests/e2e/templates/template-bulk-send.spec.ts new file mode 100644 index 000000000..70ec298ac --- /dev/null +++ b/packages/app-tests/e2e/templates/template-bulk-send.spec.ts @@ -0,0 +1,80 @@ +import { prisma } from '@documenso/prisma'; +import { seedTeam } from '@documenso/prisma/seed/teams'; +import { seedTemplate } from '@documenso/prisma/seed/templates'; +import { expect, test } from '@playwright/test'; +import { EnvelopeType } from '@prisma/client'; + +import { apiSignin } from '../fixtures/authentication'; +import { openDropdownMenu } from '../fixtures/generic'; + +test('[TEMPLATES]: bulk send via CSV from the table action dropdown', async ({ page }) => { + const { team, owner } = await seedTeam(); + + await seedTemplate({ + title: 'Bulk send template', + userId: owner.id, + teamId: team.id, + }); + + const uniqueRecipientEmail = `bulk-send-${Date.now()}@documenso.com`; + + await apiSignin({ + page, + email: owner.email, + redirectPath: `/t/${team.url}/templates`, + }); + + const actionBtn = page + .getByRole('row', { name: 'Bulk send template' }) + .getByRole('cell', { name: 'Use Template' }) + .getByRole('button') + .nth(1); + + await openDropdownMenu(page, actionBtn); + + await page.getByText('Bulk Send via CSV').click(); + + const dialog = page.getByRole('dialog').filter({ hasText: 'Bulk Send Template via CSV' }); + + await expect(dialog).toBeVisible(); + + // Opening the native file picker blurs the window. Radix dropdown menus close themselves on + // window blur, which used to unmount this dialog when it was nested inside the menu content, + // silently discarding the user's file selection. + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + + await expect(dialog).toBeVisible(); + + const csv = ['recipient_1_email,recipient_1_name', `${uniqueRecipientEmail},Bulk Recipient`].join('\n'); + + await dialog.locator('input[type="file"]').setInputFiles({ + name: 'bulk-send.csv', + mimeType: 'text/csv', + buffer: Buffer.from(csv), + }); + + await expect(dialog.getByText('bulk-send.csv')).toBeVisible(); + + await dialog.getByRole('button', { name: 'Upload and Process' }).click(); + + await expect(page.getByText('Your bulk send has been initiated').first()).toBeVisible(); + + // The bulk send runs as a background job, so poll for the created document. + await expect + .poll( + async () => + await prisma.envelope.count({ + where: { + type: EnvelopeType.DOCUMENT, + teamId: team.id, + recipients: { + some: { + email: uniqueRecipientEmail, + }, + }, + }, + }), + { timeout: 30_000 }, + ) + .toBe(1); +}); diff --git a/packages/ui/primitives/dropdown-menu.tsx b/packages/ui/primitives/dropdown-menu.tsx index 98109c460..9c5bdd16e 100644 --- a/packages/ui/primitives/dropdown-menu.tsx +++ b/packages/ui/primitives/dropdown-menu.tsx @@ -4,6 +4,19 @@ import * as React from 'react'; import { cn } from '../lib/utils'; +/** + * Note: The underlying @radix-ui/react-menu package is patched, see + * patches/@radix-ui+react-menu+2.1.24.patch. + * + * Since @radix-ui/react-menu 2.1.18, Radix closes any open menu when the window fires a + * "blur" event. Opening a native file picker blurs the window, which would close the + * menu and unmount anything rendered within its content, such as dialogs containing + * file inputs, silently discarding the user's file selection. The patch removes that + * behaviour since there is currently no native prop to opt out of it, see + * https://github.com/radix-ui/primitives/issues/3618. + * + * Regression test: packages/app-tests/e2e/templates/template-bulk-send.spec.ts + */ const DropdownMenu = DropdownMenuPrimitive.Root; const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; diff --git a/patches/@radix-ui+react-menu+2.1.24.patch b/patches/@radix-ui+react-menu+2.1.24.patch new file mode 100644 index 000000000..4b5dfe850 --- /dev/null +++ b/patches/@radix-ui+react-menu+2.1.24.patch @@ -0,0 +1,44 @@ +diff --git a/node_modules/@radix-ui/react-menu/dist/index.js b/node_modules/@radix-ui/react-menu/dist/index.js +index b3f6113..d8ec2e3 100644 +--- a/node_modules/@radix-ui/react-menu/dist/index.js ++++ b/node_modules/@radix-ui/react-menu/dist/index.js +@@ -139,9 +139,14 @@ var Menu = /* @__PURE__ */ __name((props) => { + if (!open) { + return; + } +- const handleBlur = /* @__PURE__ */ __name(() => handleOpenChange(false), "handleBlur"); +- window.addEventListener("blur", handleBlur); +- return () => window.removeEventListener("blur", handleBlur); ++ // DOCUMENSO PATCH: @radix-ui/react-menu 2.1.18+ closes open menus when the window ++ // fires a "blur" event. Opening a native file picker blurs the window, which closes ++ // the menu and unmounts anything rendered within its content, such as dialogs that ++ // contain file inputs, silently discarding the user's file selection. This restores ++ // the pre-2.1.18 behaviour. Regression test: e2e/templates/template-bulk-send.spec.ts ++ // const handleBlur = () => handleOpenChange(false); ++ // window.addEventListener("blur", handleBlur); ++ // return () => window.removeEventListener("blur", handleBlur); + }, [open, handleOpenChange]); + return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PopperPrimitive.Root, { ...popperScope, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( + MenuProvider, +diff --git a/node_modules/@radix-ui/react-menu/dist/index.mjs b/node_modules/@radix-ui/react-menu/dist/index.mjs +index cd4303a..68865e3 100644 +--- a/node_modules/@radix-ui/react-menu/dist/index.mjs ++++ b/node_modules/@radix-ui/react-menu/dist/index.mjs +@@ -73,9 +73,14 @@ var Menu = /* @__PURE__ */ __name((props) => { + if (!open) { + return; + } +- const handleBlur = /* @__PURE__ */ __name(() => handleOpenChange(false), "handleBlur"); +- window.addEventListener("blur", handleBlur); +- return () => window.removeEventListener("blur", handleBlur); ++ // DOCUMENSO PATCH: @radix-ui/react-menu 2.1.18+ closes open menus when the window ++ // fires a "blur" event. Opening a native file picker blurs the window, which closes ++ // the menu and unmounts anything rendered within its content, such as dialogs that ++ // contain file inputs, silently discarding the user's file selection. This restores ++ // the pre-2.1.18 behaviour. Regression test: e2e/templates/template-bulk-send.spec.ts ++ // const handleBlur = () => handleOpenChange(false); ++ // window.addEventListener("blur", handleBlur); ++ // return () => window.removeEventListener("blur", handleBlur); + }, [open, handleOpenChange]); + return /* @__PURE__ */ jsx(PopperPrimitive.Root, { ...popperScope, children: /* @__PURE__ */ jsx( + MenuProvider,