Keyboard shortcuts

View on GitHub

Keyboard 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

  1. 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 way Sidebar.tsx's p ("Create project") already does.
  2. 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 shortcut the same way alt+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.