M web/src/lib/keyboard.test.ts => web/src/lib/keyboard.test.ts +22 -0
@@ 105,6 105,28 @@ describe('g + letter navigation', () => {
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 <input> → 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)
M web/src/lib/keyboard.ts => web/src/lib/keyboard.ts +15 -7
@@ 53,6 53,15 @@ export function createKeyboardController(opts: KeyboardOpts): {
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 @@ export function createKeyboardController(opts: KeyboardOpts): {
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
}