An Angular command palette should open on Ctrl+K anywhere on the page, but (document:keydown.ctrl.k) never fires and Mac users expect Cmd+K; what is wrong and how do you fix it?
answer
- the plugin declines unknown words
- falls through to a literal event name
- control and meta are separate chords
- browser also owns this shortcut
basics
~20 sAngular's key filter only knows the modifier word control, so ctrl makes the key plugin decline the name and a listener for a nonexistent event is registered. Bind keydown.control.k and keydown.meta.k on document and call preventDefault().
solid answer
~40 s`KeyEventsPlugin` recognises only `alt`, `control`, `meta` and `shift`. With `ctrl` left over it returns `null`, and the catch-all `DomEventsPlugin` registers a listener for an event literally named `keydown.ctrl.k`, which the browser never fires, so nothing errors and nothing happens. The fix is `(document:keydown.control.k)` plus `(document:keydown.meta.k)`, because Cmd sets `metaKey`, not `ctrlKey`, and the filter needs an exact modifier match. Call `preventDefault()` in the handler, since browsers use Ctrl+K and Cmd+K themselves. The `document:` target makes it global; Angular removes the listener when the declaring view is destroyed. Also decide what happens when focus is in a text field, and remember that under strict templates `$event` on a key filter is typed `Event`.
code
ts · 28 linesimport { Component, signal } from '@angular/core';
@Component({
selector: 'app-shell',
template: `
<header>
<button
type="button"
(click)="isOpen.set(true)"
(document:keydown.control.k)="onShortcut($event)"
(document:keydown.meta.k)="onShortcut($event)"
>Search</button>
</header>
@if (isOpen()) {
<dialog open>
<input aria-label="Command" (keydown.escape)="isOpen.set(false)" />
</dialog>
}
`,
})
export class Shell {
isOpen = signal(false);
onShortcut(event: Event) {
event.preventDefault();
this.isOpen.set(true);
}
}go deeper
Recall the four modifier words and that a document: prefix makes a listener global.
Explain the exact modifier match and why Cmd+K on macOS needs a separate meta binding.
Diagnose the silent failure: the key plugin declining the name and the DOM plugin listening for a literal event that never fires; add preventDefault and pick the owning view.
Set a shortcut policy for the app: one owner for global chords, platform-specific modifiers, conflicts with browser and assistive-technology shortcuts.
## Why the binding silently does nothing Angular resolves every event name through `EventManager`, which asks each plugin registered under `EVENT_MANAGER_PLUGINS` whether it `supports()` the name. Custom plugins are asked first, then `KeyEventsPlugin`, and `DomEventsPlugin` last because it accepts everything. `KeyEventsPlugin.parseEventName` splits `keydown.ctrl.k` on dots, takes `keydown` as the base event and `k` as the key, and removes the modifiers it knows: **`alt`, `control`, `meta`, `shift`**. `ctrl` is not one of them. A part is left over, so the method returns `null`, and the plugin declines the name rather than throwing. That lets another plugin try. `DomEventsPlugin` accepts any name and calls `document.addEventListener('keydown.ctrl.k', ...)`. The browser happily registers a listener for an event with that exact name, and never fires one. There is **no compile error, no runtime error and no warning**: the palette just never opens. The same thing happens with `cmd`, `option` or a typo such as `contrl`. ## Fixing the chord The corrected bindings: ```html <button type="button" (click)="open()" (document:keydown.control.k)="onShortcut($event)" (document:keydown.meta.k)="onShortcut($event)" > Search </button> ``` Why two bindings: - On macOS the Command key sets `metaKey`; on Windows and Linux the Ctrl key sets `ctrlKey`. - The filter compares the **exact** set of held modifiers, so one binding cannot cover both. `control.k` does not match a press that builds `meta.k`, and vice versa. - The same exactness means Ctrl+Shift+K does not open the palette, which is usually right. If the shortcut should follow a physical key position rather than the letter on non-Latin layouts, use the code form: `(document:keydown.code.control.keyk)`. For a letter shortcut, matching the key is the more common choice. ## The handler ```ts onShortcut(event: Event) { event.preventDefault(); // browsers use Ctrl+K / Cmd+K for their own search this.isOpen.set(true); } ``` Points worth making in the interview: 1. **`preventDefault()` is required.** Several browsers bind Ctrl+K or Cmd+K to focusing their own search or address bar. Without it both things happen. 2. **Type the parameter `Event`.** Under `strictTemplates` the name `keydown.control.k` is not in TypeScript's event map, so `$event` is `Event`; a `KeyboardEvent` parameter fails type checking. 3. **Use a signal for the open state.** The listener already marks its view dirty, but a signal keeps the palette correct even if the open call later moves into async code. 4. **Decide about text fields.** A `document` listener also fires while the user types in an input. For Ctrl+K that is usually wanted (it is the point of a global shortcut); for single-letter shortcuts it is not, and the handler should check the event's target. ## Where to declare it, and its lifetime A global target (`window:`, `document:` or `body:`) attaches the listener to that object instead of the element it is written on. The listener still **belongs to the declaring view**: Angular stores its cleanup and removes it when that view is destroyed. Two consequences: - Put the binding in a component that lives as long as the shortcut should work, typically the app shell. If it sits inside an `@if` block or a lazily routed page, the shortcut only works while that view exists. - Two components that both bind `document:keydown.control.k` both fire. Global target listeners are not coalesced, so decide on one owner. The same global-target syntax is accepted in a component's `host` metadata, which is the more common place for an app-wide shortcut; how host listeners are declared is a separate subject. ## Debugging checklist | Symptom | Likely cause | |---|---| | Handler never runs, no error | Unknown modifier word (`ctrl`, `cmd`) fell through to a literal DOM event name | | Works on Windows, not on Mac | Only `control` bound; Mac users press Cmd, which sets `metaKey` | | Palette opens and browser search opens too | No `preventDefault()` | | Stops working after navigation | Binding lives in a view that was destroyed | | Type error on the handler | Parameter typed `KeyboardEvent`; key filter `$event` is `Event` |
- Why does Angular not report an error for (keydown.ctrl.k)?The key plugin returns null for names it cannot parse on purpose, so another plugin can claim them. `DomEventsPlugin` supports every name and registers a native listener for the literal string `keydown.ctrl.k`. From the browser's point of view that is a valid custom event name, so there is nothing to reject; it simply never fires.
- If two Angular components both bind (document:keydown.control.k), what happens?Both handlers run. Listeners on global targets are registered separately and are not coalesced, and each is cleaned up with its own view. Give the shortcut one owner, usually the app shell or a shortcut service, and let the others react to shared state.
- How would you stop a single-letter Angular document shortcut from firing while the user types in an input?Check the event target in the handler and return early when it is an `input`, `textarea`, `select` or content-editable element. A filter in the binding name cannot express this, because key filters only describe the key and modifiers.
saying these in an interview costs you the question
- ctrl and control are interchangeable in Angular key filters
- One control.k binding also covers Cmd+K on macOS
- An unknown modifier in the binding name causes a compile error
- A document: listener outlives its component and must be removed manually
- preventDefault is unnecessary because the page handles the shortcut first