Keyboard shortcuts

View on GitHub

Register a global shortcut

Worked example: adding alt+i for "Go to my Issues" (a page that doesn't have a route yet, used purely to demonstrate the registration shape) — a shortcut that should work from anywhere in the app, not just one page.

Global shortcuts are commands. Since the command palette landed, ShortcutProvider registers no shortcuts of its own: every app-wide shortcut (alt+b, alt+p, ?, mod+k, /) is declared as an attribute of a command, and the palette registers the keybinding for you. That keeps the palette entry, its shortcut chip and the help view in sync from one definition.

When this is the right shape, vs. guide 1

Add it to useGlobalCommands (this guide) only for something that is genuinely global — navigation between top-level areas, or an action meaningful regardless of what page is open. An action specific to one page's content, like Sidebar.tsx's p or a per-issue action, belongs in that component via useCommands() with a shortcut (or useShortcuts() for a shortcut with no palette entry; see 01-register-a-component-scoped-shortcut.mdKeyboard shortcutsRegister a component-scoped shortcutWorked example: adding a n shortcut on the Projects index page that opens the "new project" flow — scoped to that page, unlike mod+k which should work ever…). Registering something page-specific globally would make it fire on pages where the action doesn't make sense.

Step — Add a command with a shortcut

File: resources/js/hooks/useGlobalCommands.ts

{
    id: 'nav.issues',
    title: 'Go to my Issues',
    group: 'navigation',
    icon: 'ListChecks',
    shortcut: 'alt+i',
    category: 'Navigation',
    perform: () => router.visit('/issues/mine'),
},

shortcut takes everything ShortcutDefinition.key does: 'alt+i', 'mod+k', an array (['c', 'ctrl+i']) or a sequence ('g i'). Add scope: 'project' | 'issue' for a shortcut that should win over a global one with the same key. when() hides the command and disables its shortcut together.

router.visit(...) (the Inertia client-side navigation call) is the right primitive here, not window.location.href = ... — it keeps the navigation inside Inertia's SPA-style routing instead of forcing a full page reload.

Tests

File: resources/js/hooks/useGlobalCommands.test.ts

The file mocks useCommands and reads back the commands the hook registered, so assert the command and run its perform():

test('go to my issues has its own shortcut and visits the page', () => {
    render();

    expect(byId('nav.issues').shortcut).toBe('alt+i');

    byId('nav.issues').perform?.();
    expect(mocks.router.visit).toHaveBeenCalledWith('/issues/mine');
});

The keydown-to-action path itself (including mod, Alt via event.code, scopes) is covered once in resources/js/context/CommandContext.test.tsx and resources/js/context/ShortcutContext.test.tsx; a new command does not need to repeat it.