Keyboard shortcuts
View on GitHubKeyboard shortcuts
Every keyboard shortcut in Orbit — ? for help, mod+k, alt+p,
a page-scoped p for "create project" — goes through one system:
ShortcutContext. There's no per-component keydown listener
anywhere in the app; a single global listener resolves every keypress
against a runtime-built registry, and the help modal (?) is
generated entirely from that same registry — there's no separate,
hand-maintained shortcuts-list to keep in sync.
Guides, in the order you'd actually need them
- Register a component-scoped shortcutKeyboard 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…
— worked example adding a shortcut that only makes sense while a
specific page/component is mounted, using the
useShortcuts()hook the same waySidebar.tsx'sp("Create project") already does. - Register a global shortcutKeyboard shortcutsRegister a global shortcutWorked 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 t…
— worked example adding a shortcut that should exist everywhere in
the app, declared as a global command with a
shortcutthe same wayalt+p/alt+b/?already are.
The architecture in one paragraph
ShortcutProvider (resources/js/context/ShortcutContext.tsx) holds
one array of ShortcutDefinitions (key, description, category,
action, optional disabled) in state — nothing is hardcoded into
the keydown handler itself. register()/registerBatch() add to that
array and return an unregister function, meant to be called from a
useEffect cleanup (or, more commonly, through the useShortcuts(definitions)
hook, which does exactly that for you — pass an array, get the
context back, and registration/cleanup is handled on mount/unmount and
whenever the array reference changes). A single window.addEventListener('keydown', ..., true)
(capture phase, so it runs before a focused input could swallow the
event) builds a pressedKey string from the event's modifier flags
(ctrl+/alt+/meta+/shift+ prefixes) and separately tracks a
"combo" of recent non-modifier keypresses within a 500ms window (for
multi-key sequences like g d or g p, which the global navigation
commands register — the longest run of recent keys wins, so g p beats
the single-key p), then looks for a ShortcutDefinition whose key
matches either form. Shortcuts are ignored entirely while
an <input>/<textarea>/contenteditable element has focus, except
Escape. ShortcutProvider registers nothing itself: app-wide shortcuts are
attributes of commands (CommandContext, useGlobalCommands), which
hand their shortcut to registerBatch() while they are registered.
The help modal (ShortcutHelpModal, opened by ?, a read-only
reference) reads shortcuts from the same
context and groups/searches whatever's currently registered — it has
no knowledge of any shortcut beyond what's live in the array at the
moment it renders.
