diff --git a/web/src/lib/keyboard.test.ts b/web/src/lib/keyboard.test.ts index a4811327ba87905ead4335e18cd67d40e4e2bedb..f738143b9345c23bda11ba187dd983f6a4ed04f2 100644 --- a/web/src/lib/keyboard.test.ts +++ b/web/src/lib/keyboard.test.ts @@ -105,6 +105,28 @@ expect(opts.go).toHaveBeenCalledWith('health' satisfies RouteName) }) + it('g then h while palette is open → no go() (palette guards prevent navigation mid-query)', () => { + const opts = makeOpts() + opts.isPaletteOpen = vi.fn().mockReturnValue(true) + const ctrl = makeCtrl(opts) + + ctrl.onKeyDown(makeKey('g')) + ctrl.onKeyDown(makeKey('h')) + + expect(opts.go).not.toHaveBeenCalled() + }) + + it('g while target is → no gPending set, follow-up h does nothing', () => { + const opts = makeOpts() + const ctrl = makeCtrl(opts) + + const input = document.createElement('input') + ctrl.onKeyDown(makeKey('g', { target: input })) + ctrl.onKeyDown(makeKey('h', { target: input })) + + expect(opts.go).not.toHaveBeenCalled() + }) + it('teardown clears pending timer', () => { const opts = makeOpts() const ctrl = makeCtrl(opts) diff --git a/web/src/lib/keyboard.ts b/web/src/lib/keyboard.ts index 6e17c5499a65c0ef41bdd7e2e325f15fa0c45056..60e2a12274e5b99b5f5f4c7a62f9b78132ef5728 100644 --- a/web/src/lib/keyboard.ts +++ b/web/src/lib/keyboard.ts @@ -53,6 +53,15 @@ } return } + // After ⌘K and Esc, all remaining shortcuts (g-leader, j/k, Enter) + // are reserved for the global navigation surface. They must not fire + // while the palette has focus or while the user is typing into any + // editable element — typing "gh" into a search input would otherwise + // navigate home mid-query. + if (opts.isPaletteOpen() || isEditableTarget(e.target)) { + return + } + // g-prefix navigation: consume 'g' then route letter if (gPending) { let route: RouteName | null = null @@ -76,14 +85,13 @@ gTimer = setTimeout(clearG, G_TIMEOUT_MS) return } - // j / k — cursor movement (only when palette closed and target not editable) - if (!opts.isPaletteOpen() && !isEditableTarget(e.target)) { - if (e.key === 'j') { opts.cursor.move(1); return } - if (e.key === 'k') { opts.cursor.move(-1); return } - } + // j / k — cursor movement + if (e.key === 'j') { opts.cursor.move(1); return } + if (e.key === 'k') { opts.cursor.move(-1); return } - // Enter — activate cursor item when palette closed and target is body - if (e.key === 'Enter' && !opts.isPaletteOpen() && e.target === document.body) { + // Enter — activate cursor item only when target is the document body + // (not, e.g., a tab button that handles its own Enter). + if (e.key === 'Enter' && e.target === document.body) { opts.cursor.activate() return }