Atomic Angular — Doc interne

Keyboard shortcuts

Register a global or page-scoped keyboard shortcut that works on every layout, with a self-documenting help dialog and no manual cleanup.

A keyboard shortcut usually means three things to get right at once: comparing the right kind of key across keyboard layouts, not stealing a keystroke a text field or another component needs, and keeping a help list that does not drift from what is actually registered. KeyboardShortcutsService is one keydown listener, one registry, and a [keyShortcut] directive for the common case — built to replace an older engine that could not be made correct on a non-US layout.

Concept — physical vs. character key

event.key is the character a key produces — layout-dependent, so "1" unshifted is & on an AZERTY keyboard. event.code is the physical position — layout-independent. Digits are compared by event.code here specifically because a written Alt+1 should reach the same physical key everywhere; every other key is compared by event.key, since a printed letter is what a person actually reads off their keyboard.

Minimal example

A shortcut on a button, registered by the directive
app.config.ts
import type { ApplicationConfig } from "@angular/core";
import { provideKeyboardShortcuts } from "@sinequa/atomic-angular";

export const appConfig: ApplicationConfig = {
  providers: [provideKeyboardShortcuts()],
};
sample.component.ts
import { Component } from "@angular/core";
import { ShortcutDirective } from "@sinequa/atomic-angular";

@Component({
  selector: "sample-component",
  imports: [ShortcutDirective],
  template: `
    <button keyShortcut="Alt+3" shortcutLabelKey="shortcuts.zones.filters" (click)="focusFilters()">
      Go to the filters
    </button>
  `,
})
export class SampleComponent {
  protected focusFilters() {
    /* ... */
  }
}

[keyShortcut] registers the combination, writes aria-keyshortcuts on the host so assistive technology announces it, and activates the host when the combination fires — the same (click) serves the mouse, the Tab-and-Enter path, and the shortcut, in one code path. The registration disappears with the element, which is what gives a page-scoped shortcut with nothing to clean up by hand.

How it works

yes no element inside an open modal, or global: true otherwise, a modal is open document keydown, capture phase Typing guard, IME, AltGr, dead key, auto-repeat? Ignored canonicalize(event) Top of the matching combo's stack run(event) preventDefault / stopPropagation (default true)

Capture phase, deliberately: several components in this library stop propagation on keydown of their own (ngCombobox on Enter and the arrows, a galactik ListItem on Delete, dialogs on Escape) — a bubble-phase listener would be at their mercy. The service only ever consumes an event once a registration actually matches, so it does not steal a key a component below it wants.

One stack per combination: the last registration wins, and the previous one comes back once it unregisters — modeling what a panel opening over a page should do to the shortcuts underneath it.

Recipes

Registering without an element

For a shortcut with no control behind it — a help dialog, a diagnostics panel.

A global help shortcut
app-shell.component.ts
import { Component, DestroyRef, inject } from "@angular/core";
import { KeyboardShortcutsDialog, KeyboardShortcutsService } from "@sinequa/atomic-angular";

@Component({
  selector: "app-shell",
  template: ``,
})
export class AppShellComponent {
  private readonly keyboard = inject(KeyboardShortcutsService);

  constructor() {
    // destroyRef passed explicitly: a forgotten global shortcut is a mute bug,
    // so the lifetime it is tied to is made visible at the call site.
    this.keyboard.register(
      {
        combo: ["Alt+0", "Shift+?"],
        labelKey: "shortcuts.help",
        global: true,
        run: () => void KeyboardShortcutsDialog.call(),
      },
      { destroyRef: inject(DestroyRef) },
    );
  }
}

KeyboardShortcutsDialog lists whatever is currently registered, grouped by categoryKey — it observes the live registry rather than a hand-written list, which is what keeps the help dialog from drifting away from what actually works. Opened from a page that registers nothing, it lists nothing, deliberately, rather than a list half of which does not apply.

Options

Prop

Type

[keyShortcut] mirrors this as inputs: keyShortcut (the combo, required), shortcutLabelKey, shortcutCategoryKey, shortcutDisabled (unregisters while true), shortcutHidden, shortcutGlobal.

Pitfalls

What's next

On this page