From f91227dada3d57a1b4ab9c421a69fad409f7f42b Mon Sep 17 00:00:00 2001 From: Anso Date: Mon, 22 Jun 2026 19:47:53 -0400 Subject: [PATCH] fix(search): don't open the command palette via Cmd/Ctrl+K while typing (#1414) The global command palette's Cmd/Ctrl+K shortcut fired even when a text field or the code editor held focus, stealing focus mid-edit and, on macOS, clobbering the native Ctrl+K delete-to-end-of-line. Guard the handler with isInputFocused() so the shortcut opens search only when the user is not typing in a field; the toolbar search icon still opens it from anywhere. Add unit tests for the keyboard path. --- .../src/components/GlobalCommandPalette.tsx | 5 +++ .../__tests__/GlobalCommandPalette.test.tsx | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/frontend/src/components/GlobalCommandPalette.tsx b/frontend/src/components/GlobalCommandPalette.tsx index 62eb7a2e..b7579be3 100644 --- a/frontend/src/components/GlobalCommandPalette.tsx +++ b/frontend/src/components/GlobalCommandPalette.tsx @@ -18,6 +18,7 @@ import { } from '@/components/ui/command'; import { DialogDescription, DialogTitle } from '@/components/ui/dialog'; import { useNodes, type Node } from '@/context/NodeContext'; +import { isInputFocused } from '@/lib/keyboard-guards'; import { cn } from '@/lib/utils'; import { useCrossNodeStackSearch, @@ -51,6 +52,10 @@ export function GlobalCommandPaletteProvider({ children }: { children: ReactNode // Let cmdk's own Ctrl+K handling take precedence when focus is already inside a palette const target = e.target as HTMLElement | null; if (target?.closest('[cmdk-root]')) return; + // Don't hijack Cmd/Ctrl+K while the user is typing in a field or the code + // editor: stealing focus mid-edit is surprising, and on macOS Ctrl+K is the + // native delete-to-end-of-line. Search still opens elsewhere, or via the icon. + if (isInputFocused()) return; e.preventDefault(); setOpen(prev => !prev); } diff --git a/frontend/src/components/__tests__/GlobalCommandPalette.test.tsx b/frontend/src/components/__tests__/GlobalCommandPalette.test.tsx index 3f69f5e5..5583b6b2 100644 --- a/frontend/src/components/__tests__/GlobalCommandPalette.test.tsx +++ b/frontend/src/components/__tests__/GlobalCommandPalette.test.tsx @@ -62,6 +62,18 @@ function type(value: string) { fireEvent.change(screen.getByPlaceholderText('Search the app...'), { target: { value } }); } +// Focus a real text input for the duration of `run`, then always detach it. +function withFocusedInput(run: (input: HTMLInputElement) => void) { + const input = document.createElement('input'); + document.body.appendChild(input); + input.focus(); + try { + run(input); + } finally { + document.body.removeChild(input); + } +} + beforeEach(() => { hookReturn = { hits: [], failedNodes: [], loading: false }; nodesValue = { @@ -208,4 +220,36 @@ describe('GlobalCommandPalette', () => { type('zzzz'); expect(screen.getByText('Searching...')).toBeInTheDocument(); }); + + it('opens on Ctrl+K when no field is focused', () => { + renderPalette(); + fireEvent.keyDown(document.body, { key: 'k', ctrlKey: true }); + expect(screen.getByPlaceholderText('Search the app...')).toBeInTheDocument(); + }); + + it('ignores Ctrl+K while a text field is focused (no focus hijack)', () => { + renderPalette(); + withFocusedInput(input => { + fireEvent.keyDown(input, { key: 'k', ctrlKey: true }); + expect(screen.queryByPlaceholderText('Search the app...')).not.toBeInTheDocument(); + }); + }); + + it('ignores Cmd+K (metaKey) while a text field is focused', () => { + renderPalette(); + withFocusedInput(input => { + fireEvent.keyDown(input, { key: 'k', metaKey: true }); + expect(screen.queryByPlaceholderText('Search the app...')).not.toBeInTheDocument(); + }); + }); + + it('does not close the open palette when Ctrl+K is pressed inside it', () => { + // The [cmdk-root] early-return must run before the input-focus guard, so the + // app handler never toggles the palette closed while focus is in its own input. + renderPalette(); + open(); + const paletteInput = screen.getByPlaceholderText('Search the app...'); + fireEvent.keyDown(paletteInput, { key: 'k', ctrlKey: true }); + expect(screen.getByPlaceholderText('Search the app...')).toBeInTheDocument(); + }); });