code-docs/architecture/keyboard-shortcuts.md
How keyboard shortcuts flow from YAML config through build validation, the engine, and the client to the browser.
YAML Config Build Engine Client
─────────── ───── ────── ──────
events: lowdefySchema.js Events.js ShortcutManager
onClick: validates shortcut initEvent() stores registers listener
shortcut: mod+K ───► is string | string[] ───► shortcut on event ───► via tinykeys
try: [...] buildEvents.js │
warns on browser ▼
default conflicts keypress
│
visibility check
│
triggerEvent()
│
ShortcutBadge
renders ⌘ K
@lowdefy/build)lowdefySchema.jsThe event long-form object schema includes shortcut alongside try, catch, and debounce:
shortcut: {
anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }];
}
buildEvents.jsDuring build, buildEvents.js:
shortcut is a string or array of strings.mod+n, mod+t, mod+w, mod+r, mod+q, mod+l) — these can't be reliably overridden.@lowdefy/engine)Events.jsinitEvent() extracts shortcut from the event config object and stores it on the runtime event state:
shortcut: type.isObject(actions) ? actions.shortcut ?? null : null;
The shortcut is read-only metadata. The Events class doesn't use it for triggering — it's consumed by the client's ShortcutManager and by blocks for rendering ShortcutBadge.
@lowdefy/client)createShortcutManager.jsFactory function that creates the global keyboard shortcut manager.
Lifecycle:
events.*.shortcut strings, normalizes them to tinykeys format, and registers a single global keydown listener via tinykeys().triggerEvent() on the matching event.Shortcut normalization:
Lowdefy uses a readable format (mod+K, shift+Enter). Tinykeys uses $mod+k, Shift+Enter. The normalizer:
mod → $modEscape, Enter, Backspace, etc.)g i → tinykeys sequence format)Input field suppression:
Non-modifier shortcuts (single keys like Escape, /) are suppressed when focus is in <input>, <textarea>, or contentEditable elements. Modifier shortcuts (mod+K, alt+N) fire regardless of focus.
Browser defaults:
When a shortcut handler fires, it calls event.preventDefault() to suppress the browser's default action for that key combination.
createShortcutBadge.jsFactory function that creates a React component for rendering keyboard shortcut indicators.
Registration: Created in initLowdefyContext.js and passed to blocks via lowdefy._internal.Components.ShortcutBadge. Blocks import it from their props (alongside Icon and Link).
Props:
| Prop | Type | Description |
|---|---|---|
shortcut | string | string[] | null | Shortcut string(s) to display |
Platform detection:
Uses navigator.platform to detect Mac vs Windows/Linux. Renders platform-appropriate symbols:
| Modifier | Mac | Windows/Linux |
|---|---|---|
mod | ⌘ | Ctrl |
shift | ⇧ | Shift |
alt | ⌥ | Alt |
ctrl | ⌃ | Ctrl |
Special key display: Escape → Esc, Enter → ↵, Backspace → ⌫, Delete → ⌦, arrows → ←↑↓→, Tab → ⇥, Space → ␣.
Rendering:
Each key in the combination is rendered as a <kbd> element with style.module.css classes (.shortcut-badge, .shortcut-kbd, .shortcut-then). Multiple shortcuts (array) render as separate badge groups.
Styling: style.module.css provides:
.shortcut-badge — Inline flex container with small font.shortcut-kbd — Individual key with background fill and border.shortcut-then — "then" text separator for key sequencesBlocks that render ShortcutBadge follow a two-line pattern:
const { ShortcutBadge } = lowdefy._internal.Components;
// In the render:
<ShortcutBadge shortcut={events?.onClick?.shortcut} />;
Current blocks with ShortcutBadge support: Button, Anchor, Tag, Search.
The Search block has additional shortcut logic — it accepts a properties.shortcut value (default mod+k) and registers its own keyboard listener for opening the search modal, independent of the event shortcut system.
useItemShortcuts.jsA React hook in blocks-antd for keyboard shortcuts on list items (Select options, Dropdown menu items). Separate from the global ShortcutManager — handles per-item matching within a block's internal item list.
buildEvents.js collects all shortcuts on a page and warns on duplicates.preventDefault() suppresses the browser action, but some combinations (mod+N, mod+T, mod+W) may not be overridable.$mod for platform-aware modifier. Chosen for size and simplicity.