Skip to content

Global Shortcuts

Problem

You need document-level save, undo, and palette shortcuts. They must stop while a modal owns interaction without remounting listeners whenever modal state changes.

Solution

Use global when(event) to re-evaluate application policy for every keyboard event.

ts
import { createKeymap } from '@vielzeug/keymap';

let modalOpen = false;

const map = createKeymap(
  [
    { id: 'palette', shortcut: 'ctrl+k', handler: () => openCommandPalette() },
    { id: 'save', shortcut: 'ctrl+s', handler: () => saveDocument() },
    { id: 'undo', shortcut: 'ctrl+z', handler: () => undo() },
    { id: 'redo', shortcut: 'ctrl+shift+z', handler: () => redo() },
    { id: 'sidebar', shortcut: 'ctrl+/', handler: () => toggleSidebar() },
  ],
  { when: () => !modalOpen },
);

const unmount = map.mount(document);

export function openModal() {
  modalOpen = true;
  showModal();
}

export function closeModal() {
  modalOpen = false;
  hideModal();
}

export function disposeShortcuts() {
  unmount();
  map.dispose();
}

Preserve Native Text Editing

Inspect event.composedPath() when global undo and redo must yield to browser behavior inside editable fields. This matches CRM app shell policy.

ts
const isTypingInField = (event: KeyboardEvent): boolean =>
  event.composedPath().some(
    (target) =>
      target instanceof HTMLElement &&
      (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target.isContentEditable),
  );

const editingMap = createKeymap(
  [
    { id: 'undo', shortcut: 'mod+z', handler: () => undo() },
    { id: 'redo', shortcut: 'mod+shift+z', handler: () => redo() },
  ],
  { when: (event) => !isTypingInField(event) },
);

Pitfalls

  • when() runs for every event. Read current state inside callback instead of storing a stale boolean outside it.
  • A global guard blocks every binding owned by that map. Create another createKeymap() owner with its own guard when some shortcuts remain active.
  • Call both the returned unmount() callback and map.dispose() during owner teardown.