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.
This commit is contained in:
Anso
2026-06-22 19:47:53 -04:00
committed by GitHub
parent 1c5b2715b8
commit f91227dada
2 changed files with 49 additions and 0 deletions
@@ -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);
}
@@ -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();
});
});