From 86f5f9320e77fbd8d5a4bb2d068ccd83704b4a46 Mon Sep 17 00:00:00 2001 From: Zayooo Date: Tue, 8 Sep 2026 14:04:08 +0200 Subject: [PATCH] fix(Combobox): make the portalled menu clickable inside a modal dialog (MET-3088) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the menu moved to a Headless UI portal it renders under `document.body`, outside the dialog. Radix's modal `Dialog` sets `pointer-events: none` on `body` while open and restores `auto` only on its own content element, so the menu inherited `none`: it opened and rendered, but every click passed through to the dialog behind it. Keyboard selection still worked, which is what gave it away. Re-enable pointer events on `.positioner`, the portalled root, so the whole menu subtree is hit-testable again. That alone lets the click reach Radix, which reads a pointerdown on the menu as an interaction outside the dialog and closes it mid-selection. So `DialogV2` now treats a pointerdown inside a Headless UI portal as inside the dialog and prevents the dismissal. Affects every dialog that hosts a Combobox — 13 components in the app. --- .../Combobox/Combobox.module.css | 8 ++++++++ src/code-components/DialogV2/DialogV2.tsx | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/code-components/Combobox/Combobox.module.css b/src/code-components/Combobox/Combobox.module.css index 151456ab..ff72ce50 100644 --- a/src/code-components/Combobox/Combobox.module.css +++ b/src/code-components/Combobox/Combobox.module.css @@ -36,6 +36,14 @@ .positioner { transition: opacity 0.1s ease-in; overflow: visible !important; + /* + * The menu is portalled to `document.body`. A Radix modal dialog sets + * `pointer-events: none` on `body` while open and restores `auto` only on its + * own content element, so a menu opened from inside a dialog would render but + * ignore every click. `pointer-events` inherits, so re-enabling it here covers + * the whole menu subtree. Harmless outside a dialog. + */ + pointer-events: auto; } .positioner[data-closed] { diff --git a/src/code-components/DialogV2/DialogV2.tsx b/src/code-components/DialogV2/DialogV2.tsx index c1aa3ba2..555ad598 100644 --- a/src/code-components/DialogV2/DialogV2.tsx +++ b/src/code-components/DialogV2/DialogV2.tsx @@ -62,6 +62,10 @@ export function DialogV2({ event.preventDefault(); }} onPointerDownOutside={(event) => { + if (isHeadlessUiPortalClick(event)) { + event.preventDefault(); + return; + } preventEventIfScrollbarClick(event); onPointerDownOutside?.(event); }} @@ -74,6 +78,19 @@ export function DialogV2({ ); } +/** + * A Headless UI menu (Combobox, Listbox, Menu) opened from inside the dialog is + * portalled to `document.body`, so Radix sees a click on one of its options as a + * click outside the dialog and closes it mid-selection. Treat those clicks as + * inside. + */ +function isHeadlessUiPortalClick(event: PointerDownOutsideEvent) { + const target = event.target; + return ( + target instanceof Element && !!target.closest("[data-headlessui-portal]") + ); +} + /** * Based on https://github.com/tailwindlabs/headlessui/pull/1333/files#diff-d095a5f3fa3ad7f5ff99576cb61e5d75a979a6b7d5557f8a092f5d5c8c0c34deR49 */