diff --git a/src/components/SearchPalette.tsx b/src/components/SearchPalette.tsx index a8bca51..e7e1b61 100644 --- a/src/components/SearchPalette.tsx +++ b/src/components/SearchPalette.tsx @@ -197,6 +197,11 @@ export function SearchPalette({ isOpen, onClose, onSelect, initialQuery = '' }: trapTabKey(e, panelRef.current) } + /* Closed palette must not render at all: the backdrop is `position: fixed; + inset: 0`, so rendering it unconditionally left the modal permanently on + screen (and ate every click on the page underneath). */ + if (!isOpen) return null + return (
{ it('renders a modal dialog with the panel focused when open', () => { @@ -168,3 +169,41 @@ describe('Sheet (Overlay skin)', () => { expect(last).toHaveFocus() }) }) + +describe('SearchPalette (Overlay behavior)', () => { + it('renders nothing while closed and no backdrop is left over the page', () => { + const { rerender } = render( + {}} onSelect={() => {}} />, + ) + expect(screen.queryByRole('dialog')).toBeNull() + expect(document.querySelector('.search-palette-backdrop')).toBeNull() + expect(document.body.style.overflow).toBe('') + + rerender( {}} onSelect={() => {}} />) + expect(screen.getByRole('dialog')).toHaveClass('search-palette-modal') + }) + + it('closes on Escape / backdrop click and unlocks body scroll', () => { + function Controlled() { + const [open, setOpen] = useState(true) + return ( + <> + + setOpen(false)} onSelect={() => {}} /> + + ) + } + const { container } = render() + expect(document.body.style.overflow).toBe('hidden') + + fireEvent.keyDown(window, { key: 'Escape' }) + expect(screen.queryByRole('dialog')).toBeNull() + expect(document.body.style.overflow).toBe('') + + fireEvent.click(screen.getByRole('button', { name: 'Open' })) + expect(screen.getByRole('dialog')).toBeInTheDocument() + fireEvent.click(container.querySelector('.search-palette-backdrop')!) + expect(screen.queryByRole('dialog')).toBeNull() + expect(document.body.style.overflow).toBe('') + }) +})