diff --git a/packages/command-palette/src/__tests__/command-palette.test.tsx b/packages/command-palette/src/__tests__/command-palette.test.tsx index 84e1a5bf..e0f3d0e4 100644 --- a/packages/command-palette/src/__tests__/command-palette.test.tsx +++ b/packages/command-palette/src/__tests__/command-palette.test.tsx @@ -85,7 +85,7 @@ describe('CommandRegistryProvider', () => { ]); return useCommands(); }, - { wrapper: wrapper() }, + { wrapper: wrapper() } ); expect(result.current.map(c => c.id).sort()).toEqual(['temp', 'temp2']); unmount(); @@ -96,9 +96,7 @@ describe('CommandRegistryProvider', () => { }); it('throws when used outside provider', () => { - expect(() => renderHook(() => useCommands())).toThrow( - /CommandRegistryProvider/, - ); + expect(() => renderHook(() => useCommands())).toThrow(/CommandRegistryProvider/); }); }); @@ -128,7 +126,7 @@ describe('CommandPalette', () => { render( {}} /> - , + ); expect(screen.queryByTestId('bl-cmdk')).toBeNull(); }); @@ -137,7 +135,7 @@ describe('CommandPalette', () => { render( {}} /> - , + ); expect(screen.getByTestId('bl-cmdk-panel')).toBeDefined(); expect(screen.getByTestId('bl-cmdk-item-new-task')).toBeDefined(); @@ -149,7 +147,7 @@ describe('CommandPalette', () => { render( {}} /> - , + ); expect(screen.queryByTestId('bl-cmdk-item-gated')).toBeNull(); }); @@ -158,7 +156,7 @@ describe('CommandPalette', () => { render( {}} /> - , + ); const dialog = screen.getByTestId('bl-cmdk'); fireEvent.keyDown(dialog, { key: 'Tab' }); @@ -171,7 +169,7 @@ describe('CommandPalette', () => { render( {}} /> - , + ); fireEvent.change(screen.getByTestId('bl-cmdk-input'), { target: { value: 'task' }, @@ -184,10 +182,10 @@ describe('CommandPalette', () => { render( - , + ); fireEvent.keyDown(screen.getByTestId('bl-cmdk'), { key: 'Enter' }); - expect((seed[0].run as ReturnType)).toHaveBeenCalledOnce(); + expect(seed[0].run as ReturnType).toHaveBeenCalledOnce(); expect(onClose).toHaveBeenCalled(); }); @@ -197,7 +195,7 @@ describe('CommandPalette', () => { render( - , + ); fireEvent.keyDown(screen.getByTestId('bl-cmdk'), { key: 'Tab' }); fireEvent.keyDown(screen.getByTestId('bl-cmdk'), { key: 'Enter' }); @@ -209,7 +207,7 @@ describe('CommandPalette', () => { render( {}} /> - , + ); const dialog = screen.getByTestId('bl-cmdk'); fireEvent.keyDown(dialog, { key: 'ArrowDown' }); @@ -225,7 +223,7 @@ describe('CommandPalette', () => { render( - , + ); fireEvent.keyDown(document, { key: 'Escape' }); expect(onClose).toHaveBeenCalledOnce(); @@ -235,7 +233,7 @@ describe('CommandPalette', () => { const { rerender } = render( {}} initialMode="ask-ai" /> - , + ); expect(screen.getByTestId('bl-cmdk-ask-ai')).toBeDefined(); @@ -247,7 +245,7 @@ describe('CommandPalette', () => { initialMode="ask-ai" askAiPanel={q =>
{q || 'idle'}
} /> - , + ); expect(screen.getByTestId('custom-ai').textContent).toBe('idle'); }); @@ -256,12 +254,12 @@ describe('CommandPalette', () => { render( {}} /> - , + ); const row = screen.getByTestId('bl-cmdk-item-archive'); expect(row.getAttribute('aria-disabled')).toBe('true'); fireEvent.click(row); - expect((seed[3].run as ReturnType)).not.toHaveBeenCalled(); + expect(seed[3].run as ReturnType).not.toHaveBeenCalled(); }); it('recents persist to localStorage when a command is run', () => { @@ -294,7 +292,7 @@ describe('CommandPalette', () => { render( {}} /> - , + ); fireEvent.click(screen.getByTestId('bl-cmdk-item-new-task')); expect(mem['bl-cmdk-recents']).toContain('new-task'); @@ -315,16 +313,12 @@ describe('useCommandPalette', () => { expect(result.current.open).toBe(false); act(() => { - window.dispatchEvent( - new KeyboardEvent('keydown', { key: 'k', metaKey: true }), - ); + window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })); }); expect(result.current.open).toBe(true); act(() => { - window.dispatchEvent( - new KeyboardEvent('keydown', { key: 'k', metaKey: true }), - ); + window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })); }); expect(result.current.open).toBe(false); @@ -335,6 +329,19 @@ describe('useCommandPalette', () => { expect(result.current.open).toBe(false); }); + it('ignores keydown events with an undefined key (autofill/IME) without throwing', () => { + const { result } = renderHook(() => useCommandPalette()); + expect(() => + act(() => { + // Some events (autofill, IME) dispatch keydown with no `key` set. + const ev = new KeyboardEvent('keydown', { metaKey: true }); + Object.defineProperty(ev, 'key', { value: undefined }); + window.dispatchEvent(ev); + }) + ).not.toThrow(); + expect(result.current.open).toBe(false); + }); + it('show/hide/toggle imperative helpers', () => { const { result } = renderHook(() => useCommandPalette({ hotkey: null })); act(() => result.current.show()); @@ -348,9 +355,7 @@ describe('useCommandPalette', () => { }); it('respects defaultOpen', () => { - const { result } = renderHook(() => - useCommandPalette({ defaultOpen: true, hotkey: null }), - ); + const { result } = renderHook(() => useCommandPalette({ defaultOpen: true, hotkey: null })); expect(result.current.open).toBe(true); }); }); diff --git a/packages/command-palette/src/useCommandPalette.ts b/packages/command-palette/src/useCommandPalette.ts index f739287a..93fc78c5 100644 --- a/packages/command-palette/src/useCommandPalette.ts +++ b/packages/command-palette/src/useCommandPalette.ts @@ -32,10 +32,9 @@ export interface UseCommandPaletteHelpers { * inputs unless the user explicitly holds the modifier. */ export function useCommandPalette( - options: UseCommandPaletteOptions = {}, + options: UseCommandPaletteOptions = {} ): UseCommandPaletteHelpers { - const { hotkey = { key: 'k', meta: true, ctrl: true }, defaultOpen = false } = - options; + const { hotkey = { key: 'k', meta: true, ctrl: true }, defaultOpen = false } = options; const [open, setOpen] = useState(defaultOpen); const show = useCallback(() => setOpen(true), []); @@ -45,6 +44,9 @@ export function useCommandPalette( useEffect(() => { if (!hotkey) return; const handler = (e: KeyboardEvent) => { + // Some keydown events (autofill, IME/composition, certain extensions) arrive + // with an undefined `key`; guard so we never crash on `.toLowerCase()`. + if (!e.key || !hotkey.key) return; if (e.key.toLowerCase() !== hotkey.key.toLowerCase()) return; const wantMeta = hotkey.meta ?? false; const wantCtrl = hotkey.ctrl ?? false;