Keyboard Shortcuts
A global keyboard-shortcut registry you inject. Register a chord with a handler, hold the returned token for its lifetime, and dispose it to unregister, the same engine-handle idiom used across the brain.
Press mod + J, or ?, anywhere on the page.
Fired times.
Last chord:
@inject KeyboardShortcutService Shortcuts
@implements IDisposable
@code {
private IDisposable? _reg;
protected override void OnAfterRender(bool firstRender)
{
if (!firstRender) return;
_reg = Shortcuts.Register(new ShortcutOptions("mod+k"), OpenPaletteAsync);
}
public void Dispose() => _reg?.Dispose();
}How it works
KeyboardShortcutService is registered scoped by AddNavius() (the same DI shape as the toast manager). Inject it anywhere. The one hard problem it solves: a global shortcut that must call event.preventDefault() (to stop the browser's own Ctrl+K or "/" handling) has to do so synchronously, before any Blazor interop round trip resolves. So the service pushes the effective chord table down to a single JS keydown listener; JS decides preventDefault off that table and then dispatches the matched chord back to C# for handler execution.
Registering a chord
Inject the service, register on first render, and dispose the token on teardown. A handler that throws is swallowed so one bad handler cannot tear down the shared listener.
@using Navius.Primitives
@inject KeyboardShortcutService Shortcuts
@implements IDisposable
@code {
private IDisposable? _save, _help;
protected override void OnAfterRender(bool firstRender)
{
if (!firstRender) return;
_save = Shortcuts.Register(new ShortcutOptions("mod+s"), SaveAsync);
_help = Shortcuts.Register(new ShortcutOptions("shift+?"), ShowHelpAsync);
}
public void Dispose()
{
_save?.Dispose();
_help?.Dispose();
}
}Chord syntax
A chord is a normalized, case-insensitive string of modifiers and one key joined by +. Modifiers are ctrl, alt, shift, meta and mod. mod fans out to both ctrl and meta, so one registration is cross-platform. This is an MVP: single chords only (no g d-style sequences).
| Data attribute | Values |
|---|---|
| mod+k | Ctrl+K on Windows/Linux, Cmd+K on macOS (mod fans out to both) |
| shift+? | Shift plus the ? key |
| ctrl+alt+delete | Multiple explicit modifiers plus a key |
| escape | A bare key, no modifiers |
Scoping
A shortcut registered with a non-null Scope only fires while that scope is pushed; global (null-scope) shortcuts always fire. Dialogs and command palettes push a scope on open and dispose it on close, so their shortcuts are live only while they are.
// On dialog open: push a scope and register a scoped Escape-like chord.
var scope = Shortcuts.PushScope("command-palette");
var reg = Shortcuts.Register(new ShortcutOptions("mod+enter", Scope: "command-palette"), RunAsync);
// On close: dispose both (the shortcut stops firing, the scope pops).
reg.Dispose();
scope.Dispose();API Reference
KeyboardShortcutService
Injected (scoped, via AddNavius). Register chords and push scopes; both return an IDisposable token you dispose to undo.
| Prop | Type | Default |
|---|---|---|
| Register(ShortcutOptions options, Func<Task> handler) | IDisposable | - |
| PushScope(string scope) | IDisposable | - |
| OnShortcut(string chord) | Task (JSInvokable) | - |
| DisposeAsync() | ValueTask | - |
ShortcutOptions
The record passed to Register. A null Scope means global.
| Prop | Type | Default |
|---|---|---|
| Chord | string | required |
| Scope | string? | null (global) |
| PreventDefault | bool | true |
| AllowInInputs | bool | false |
| Repeat | bool | false |