From 67af53672c7c41f04a196dd82938cb5f05427902 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 3 Sep 2026 09:09:07 -0700 Subject: [PATCH] fix(ui): keep modal popovers interactive --- .../components/combobox/combobox.dom.test.tsx | 35 +++++++++++++++++++ .../emcn/src/components/popover/popover.tsx | 10 +++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/emcn/src/components/combobox/combobox.dom.test.tsx b/packages/emcn/src/components/combobox/combobox.dom.test.tsx index 9e999cfedb9..bd9100de247 100644 --- a/packages/emcn/src/components/combobox/combobox.dom.test.tsx +++ b/packages/emcn/src/components/combobox/combobox.dom.test.tsx @@ -12,8 +12,13 @@ import { act, type ReactNode, useState } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it, vi } from 'vitest' +import { InsideModalContext } from '../modal/modal' import { Combobox } from './combobox' +vi.mock('next/navigation', () => ({ + usePathname: () => '/workspace/workspace-1/home', +})) + let root: Root | null = null let container: HTMLDivElement | null = null @@ -42,6 +47,12 @@ function click(node: HTMLElement) { }) } +function mouseDown(node: HTMLElement) { + act(() => { + node.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })) + }) +} + function press(node: HTMLElement, key: string) { act(() => { node.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true })) @@ -60,6 +71,7 @@ afterEach(() => { container?.remove() root = null container = null + document.body.removeAttribute('style') vi.restoreAllMocks() }) @@ -72,6 +84,28 @@ describe('Combobox onOpenChange', () => { expect(container?.querySelector('[role="listbox"]')).not.toBeNull() }) + it('keeps portaled options interactive inside modal content', () => { + const onChange = vi.fn() + render( + + + + ) + + click(trigger()) + + const option = [...document.querySelectorAll('[role="option"]')].find( + ({ textContent }) => textContent === 'Alpha' + ) + if (!option) throw new Error('Alpha option was not rendered') + expect(document.body.style.pointerEvents).toBe('none') + expect(getComputedStyle(option).pointerEvents).toBe('auto') + + mouseDown(option) + + expect(onChange).toHaveBeenCalledWith('alpha') + }) + it('uses the overlay label for the interactive overflow layer', () => { render( { click(trigger()) expect(onOpenChange).toHaveBeenCalledWith(true) + expect(document.body.style.pointerEvents).toBe('') }) it('reports the close a second trigger click causes', () => { diff --git a/packages/emcn/src/components/popover/popover.tsx b/packages/emcn/src/components/popover/popover.tsx index 8efe883c9d4..8c549184f3b 100644 --- a/packages/emcn/src/components/popover/popover.tsx +++ b/packages/emcn/src/components/popover/popover.tsx @@ -56,6 +56,7 @@ import { createPortal } from 'react-dom' import { Check, ChevronLeft, ChevronRight, Search } from '../../icons' import { cn } from '../../lib/cn' import { chipActiveSurfaceClass, chipHoverSurfaceClass } from '../chip/chip-chrome' +import { InsideModalContext } from '../modal/modal' import { TOOLTIP_MAX_WIDTH_PX, TOOLTIP_SURFACE_CLASS } from '../tooltip/tooltip-styles' type PopoverSize = 'sm' | 'md' @@ -209,8 +210,10 @@ const Popover: React.FC = ({ colorScheme = 'default', open, onOpenChange, + modal, ...props }) => { + const insideModal = React.useContext(InsideModalContext) const [currentFolder, setCurrentFolder] = React.useState(null) const [folderTitle, setFolderTitle] = React.useState(null) const [onFolderSelect, setOnFolderSelect] = React.useState<(() => void) | null>(null) @@ -329,7 +332,12 @@ const Popover: React.FC = ({ return ( - + {children}