@tanstack/angular-hotkeys 0.19.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/_zone.js ADDED
@@ -0,0 +1,14 @@
1
+ import { NgZone, inject } from "@angular/core";
2
+
3
+ //#region src/_zone.ts
4
+ /**
5
+ * Runs `fn` outside the Angular zone, so core listeners it attaches don't
6
+ * run change detection on every event. Zoneless apps call `fn` directly.
7
+ * Must be called in an injection context.
8
+ */
9
+ function runOutsideZone(fn) {
10
+ return inject(NgZone).runOutsideAngular(fn);
11
+ }
12
+
13
+ //#endregion
14
+ export { runOutsideZone };
@@ -1,9 +1,15 @@
1
1
  import { InjectHotkeyCommonOptions } from "./injectHotkey.js";
2
- import { AppHotkeys, AppHotkeysState, AppHotkeysStore, CustomHotkeys, HotkeyChordRecorderOptions, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
2
+ import { AppHotkeys, AppHotkeysState, AppHotkeysStore, CustomHotkeys, HotkeyChordRecorderOptions, HotkeyPlatform, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
3
3
  import { InjectionToken, Provider, Signal } from "@angular/core";
4
4
  import { HotkeysProviderScopeProps } from "@tanstack/hotkeys/adapter";
5
5
  //#region src/hotkeys-provider.d.ts
6
6
  export interface HotkeysProviderOptions {
7
+ /**
8
+ * The user's platform, for labels from `injectFormatForDisplay`, hotkeys,
9
+ * and recorders. Detected when unset. On a server, set it from the request
10
+ * with `detectPlatform(userAgent)`.
11
+ */
12
+ platform?: HotkeyPlatform;
7
13
  /**
8
14
  * Defaults for every hotkey registered by `injectHotkey` and `injectHotkeys`.
9
15
  * `requireReset` applies to chords and `timeout` applies to sequences.
@@ -14,8 +20,12 @@ export interface HotkeysProviderOptions {
14
20
  }
15
21
  /** Options for `provideHotkeys`. */
16
22
  export interface ProvideHotkeysOptions {
17
- /** Merged with the parent provider's defaults. */
18
- defaultOptions?: HotkeysProviderOptions;
23
+ /**
24
+ * Merged with the parent provider's defaults. A getter runs in the
25
+ * provider's injection context, so it can `inject(REQUEST)` to set
26
+ * `platform` from the request during server rendering.
27
+ */
28
+ defaultOptions?: MaybeGetter<HotkeysProviderOptions>;
19
29
  /**
20
30
  * See `HotkeysProviderScopeProps['scope']`. Getters, here and for
21
31
  * `exclusive` and `enabled`, run in the provider's injection context, so
@@ -62,7 +72,9 @@ export type MaybeGetter<T> = T | (() => T);
62
72
  * inherits app hotkeys and merges default options. With `scope`, hotkeys
63
73
  * injected in the component and its children fire only while it exists and
64
74
  * is enabled; with `exclusive`, only they fire. The scope activates when the
65
- * first hotkey in it is injected.
75
+ * first hotkey in it is injected. Angular creates the provider lazily, so a
76
+ * component with no hotkeys of its own calls `injectHotkeysContext()` to
77
+ * activate its scope.
66
78
  *
67
79
  * @example
68
80
  * ```ts
@@ -94,6 +106,21 @@ export type MaybeGetter<T> = T | (() => T);
94
106
  *
95
107
  * @example
96
108
  * ```ts
109
+ * // An overlay with no hotkeys that suspends the page's hotkeys.
110
+ * @Component({
111
+ * selector: 'app-loading-overlay',
112
+ * providers: [provideHotkeys({ scope: 'loading', exclusive: true })],
113
+ * template: `...`,
114
+ * })
115
+ * export class LoadingOverlay {
116
+ * constructor() {
117
+ * injectHotkeysContext()
118
+ * }
119
+ * }
120
+ * ```
121
+ *
122
+ * @example
123
+ * ```ts
97
124
  * // A scope that follows the providing component's input.
98
125
  * @Component({
99
126
  * selector: 'app-folder',
@@ -112,6 +139,10 @@ export type MaybeGetter<T> = T | (() => T);
112
139
  * ```
113
140
  */
114
141
  export declare function provideHotkeys(options?: ProvideHotkeysOptions): Provider;
142
+ /**
143
+ * Returns the nearest provider's context. Injecting it activates that
144
+ * provider's scope.
145
+ */
115
146
  export declare function injectHotkeysContext(): HotkeysContextValue;
116
147
  export declare function injectDefaultHotkeysOptions(): HotkeysProviderOptions;
117
148
  //#endregion
@@ -43,7 +43,7 @@ function createHotkeysContext(options) {
43
43
  appHotkeysState = state.asReadonly();
44
44
  }
45
45
  const enabled = computed(() => parent.enabled() && read(options.enabled) !== false);
46
- const defaultOptions = computed(() => mergeHotkeysProviderDefaults(parent.defaultOptions, options.defaultOptions, read(options.scope)));
46
+ const defaultOptions = computed(() => mergeHotkeysProviderDefaults(parent.defaultOptions, read(options.defaultOptions), read(options.scope)));
47
47
  const scopeKey = computed(() => getHotkeysProviderScopeKey({
48
48
  scope: read(options.scope),
49
49
  exclusive: read(options.exclusive),
@@ -71,7 +71,9 @@ function createHotkeysContext(options) {
71
71
  * inherits app hotkeys and merges default options. With `scope`, hotkeys
72
72
  * injected in the component and its children fire only while it exists and
73
73
  * is enabled; with `exclusive`, only they fire. The scope activates when the
74
- * first hotkey in it is injected.
74
+ * first hotkey in it is injected. Angular creates the provider lazily, so a
75
+ * component with no hotkeys of its own calls `injectHotkeysContext()` to
76
+ * activate its scope.
75
77
  *
76
78
  * @example
77
79
  * ```ts
@@ -103,6 +105,21 @@ function createHotkeysContext(options) {
103
105
  *
104
106
  * @example
105
107
  * ```ts
108
+ * // An overlay with no hotkeys that suspends the page's hotkeys.
109
+ * @Component({
110
+ * selector: 'app-loading-overlay',
111
+ * providers: [provideHotkeys({ scope: 'loading', exclusive: true })],
112
+ * template: `...`,
113
+ * })
114
+ * export class LoadingOverlay {
115
+ * constructor() {
116
+ * injectHotkeysContext()
117
+ * }
118
+ * }
119
+ * ```
120
+ *
121
+ * @example
122
+ * ```ts
106
123
  * // A scope that follows the providing component's input.
107
124
  * @Component({
108
125
  * selector: 'app-folder',
@@ -126,6 +143,10 @@ function provideHotkeys(options = {}) {
126
143
  useFactory: () => createHotkeysContext(options)
127
144
  };
128
145
  }
146
+ /**
147
+ * Returns the nearest provider's context. Injecting it activates that
148
+ * provider's scope.
149
+ */
129
150
  function injectHotkeysContext() {
130
151
  return inject(HOTKEYS_INJECTION_TOKEN);
131
152
  }
package/dist/index.d.ts CHANGED
@@ -9,5 +9,6 @@ import { injectHotkeyRegistrations } from "./injectHotkeyRegistrations.js";
9
9
  import { injectHotkeyScopesState } from "./injectHotkeyScopesState.js";
10
10
  import { injectHotkeyHint } from "./injectHotkeyHint.js";
11
11
  import { InjectCustomHotkeysResult, injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys } from "./injectAppHotkeys.js";
12
+ import { injectFormatForDisplay } from "./injectFormatForDisplay.js";
12
13
  export * from "@tanstack/hotkeys";
13
- export { AngularHotkeyRecorder, HOTKEYS_INJECTION_TOKEN, HotkeysContextValue, HotkeysProviderOptions, InjectAppHotkeyOptions, InjectCustomHotkeysResult, InjectHotkeyChordOptions, InjectHotkeyCommonOptions, InjectHotkeyDefinition, InjectHotkeyOptions, InjectHotkeyOptionsFor, InjectHotkeySequenceOptions, InjectHotkeyTarget, MaybeGetter, ProvideHotkeysOptions, injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys, injectDefaultHotkeysOptions, injectHeldKeyCodes, injectHeldKeys, injectHotkey, injectHotkeyHint, injectHotkeyRecorder, injectHotkeyRegistrations, injectHotkeyScopesState, injectHotkeys, injectHotkeysContext, injectKeyHold, provideHotkeys };
14
+ export { AngularHotkeyRecorder, HOTKEYS_INJECTION_TOKEN, HotkeysContextValue, HotkeysProviderOptions, InjectAppHotkeyOptions, InjectCustomHotkeysResult, InjectHotkeyChordOptions, InjectHotkeyCommonOptions, InjectHotkeyDefinition, InjectHotkeyOptions, InjectHotkeyOptionsFor, InjectHotkeySequenceOptions, InjectHotkeyTarget, MaybeGetter, ProvideHotkeysOptions, injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys, injectDefaultHotkeysOptions, injectFormatForDisplay, injectHeldKeyCodes, injectHeldKeys, injectHotkey, injectHotkeyHint, injectHotkeyRecorder, injectHotkeyRegistrations, injectHotkeyScopesState, injectHotkeys, injectHotkeysContext, injectKeyHold, provideHotkeys };
package/dist/index.js CHANGED
@@ -9,7 +9,8 @@ import { injectHotkeyRegistrations } from "./injectHotkeyRegistrations.js";
9
9
  import { injectHotkeyScopesState } from "./injectHotkeyScopesState.js";
10
10
  import { injectHotkeyHint } from "./injectHotkeyHint.js";
11
11
  import { injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys } from "./injectAppHotkeys.js";
12
+ import { injectFormatForDisplay } from "./injectFormatForDisplay.js";
12
13
 
13
14
  export * from "@tanstack/hotkeys"
14
15
 
15
- export { HOTKEYS_INJECTION_TOKEN, injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys, injectDefaultHotkeysOptions, injectHeldKeyCodes, injectHeldKeys, injectHotkey, injectHotkeyHint, injectHotkeyRecorder, injectHotkeyRegistrations, injectHotkeyScopesState, injectHotkeys, injectHotkeysContext, injectKeyHold, provideHotkeys };
16
+ export { HOTKEYS_INJECTION_TOKEN, injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys, injectDefaultHotkeysOptions, injectFormatForDisplay, injectHeldKeyCodes, injectHeldKeys, injectHotkey, injectHotkeyHint, injectHotkeyRecorder, injectHotkeyRegistrations, injectHotkeyScopesState, injectHotkeys, injectHotkeysContext, injectKeyHold, provideHotkeys };
@@ -0,0 +1,25 @@
1
+ import { formatForDisplay } from "@tanstack/hotkeys";
2
+ //#region src/injectFormatForDisplay.d.ts
3
+ /**
4
+ * Returns `formatForDisplay` with the provider's `platform` as the default,
5
+ * so labels match the user's platform. Call it in an injection context, such
6
+ * as a field initializer; the returned function works anywhere, including
7
+ * templates. An explicit `platform` option wins.
8
+ *
9
+ * Server-rendered apps should set the provider's `defaultOptions.platform`
10
+ * from the request user agent, such as
11
+ * `detectPlatform(inject(REQUEST)?.headers.get('user-agent'))`.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * @Component({
16
+ * selector: 'app-save-button',
17
+ * template: `<kbd>{{ format('Mod+S') }}</kbd>`,
18
+ * })
19
+ * export class SaveButton {
20
+ * format = injectFormatForDisplay()
21
+ * }
22
+ * ```
23
+ */
24
+ export declare function injectFormatForDisplay(): typeof formatForDisplay;
25
+ //#endregion
@@ -0,0 +1,35 @@
1
+ import { injectHotkeysContext } from "./hotkeys-provider.js";
2
+ import { formatForDisplay } from "@tanstack/hotkeys";
3
+
4
+ //#region src/injectFormatForDisplay.ts
5
+ /**
6
+ * Returns `formatForDisplay` with the provider's `platform` as the default,
7
+ * so labels match the user's platform. Call it in an injection context, such
8
+ * as a field initializer; the returned function works anywhere, including
9
+ * templates. An explicit `platform` option wins.
10
+ *
11
+ * Server-rendered apps should set the provider's `defaultOptions.platform`
12
+ * from the request user agent, such as
13
+ * `detectPlatform(inject(REQUEST)?.headers.get('user-agent'))`.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * @Component({
18
+ * selector: 'app-save-button',
19
+ * template: `<kbd>{{ format('Mod+S') }}</kbd>`,
20
+ * })
21
+ * export class SaveButton {
22
+ * format = injectFormatForDisplay()
23
+ * }
24
+ * ```
25
+ */
26
+ function injectFormatForDisplay() {
27
+ const context = injectHotkeysContext();
28
+ return ((hotkey, options) => formatForDisplay(hotkey, {
29
+ ...options,
30
+ platform: options?.platform ?? context.defaultOptions.platform
31
+ }));
32
+ }
33
+
34
+ //#endregion
35
+ export { injectFormatForDisplay };
@@ -1,4 +1,5 @@
1
1
  import { injectSelector } from "./_injectSelector.js";
2
+ import { runOutsideZone } from "./_zone.js";
2
3
  import { getKeyStateTracker } from "@tanstack/hotkeys";
3
4
 
4
5
  //#region src/injectHeldKeyCodes.ts
@@ -26,7 +27,7 @@ import { getKeyStateTracker } from "@tanstack/hotkeys";
26
27
  * ```
27
28
  */
28
29
  function injectHeldKeyCodes() {
29
- const tracker = getKeyStateTracker();
30
+ const tracker = runOutsideZone(getKeyStateTracker);
30
31
  return injectSelector(tracker.store, (state) => state.heldCodes);
31
32
  }
32
33
 
@@ -1,4 +1,5 @@
1
1
  import { injectSelector } from "./_injectSelector.js";
2
+ import { runOutsideZone } from "./_zone.js";
2
3
  import { getKeyStateTracker } from "@tanstack/hotkeys";
3
4
 
4
5
  //#region src/injectHeldKeys.ts
@@ -21,7 +22,7 @@ import { getKeyStateTracker } from "@tanstack/hotkeys";
21
22
  * ```
22
23
  */
23
24
  function injectHeldKeys() {
24
- const tracker = getKeyStateTracker();
25
+ const tracker = runOutsideZone(getKeyStateTracker);
25
26
  return injectSelector(tracker.store, (state) => state.heldKeys);
26
27
  }
27
28
 
@@ -1,12 +1,13 @@
1
1
  import { AppHotkey, HotkeyCallback, HotkeyCallbackContextFor, RegisterableHotkey, Target } from "@tanstack/hotkeys";
2
+ import { ElementRef, Signal } from "@angular/core";
2
3
  import { AdapterAppHotkeyOptions, AdapterChordOptions, AdapterCommonOptions, AdapterHotkeyOptions, AdapterOptionsFor, AdapterSequenceOptions } from "@tanstack/hotkeys/adapter";
3
4
  //#region src/injectHotkey.d.ts
4
5
  /**
5
- * The element to listen on. A `null` target skips registration, so pass a
6
- * getter such as `() => ({ target: this.element() })` for elements that
7
- * attach after the view renders.
6
+ * The element to listen on: an element, an `ElementRef`, or a signal of
7
+ * either, such as `viewChild<ElementRef>('panel')`. A `null` target, or a
8
+ * signal of `undefined`, skips registration until the element exists.
8
9
  */
9
- export type InjectHotkeyTarget = Target | null;
10
+ export type InjectHotkeyTarget = Target | ElementRef<Target> | Signal<Target | ElementRef<Target> | null | undefined> | null;
10
11
  /** Options shared by chords and sequences. */
11
12
  export type InjectHotkeyOptions = AdapterHotkeyOptions<InjectHotkeyTarget>;
12
13
  /** Options for a chord such as `'Mod+S'`. */
@@ -1,5 +1,6 @@
1
1
  import { injectHotkeysContext } from "./hotkeys-provider.js";
2
2
  import { injectSelector } from "./_injectSelector.js";
3
+ import { runOutsideZone } from "./_zone.js";
3
4
  import { getHotkeyManager, getKeyStateTracker, isAppHotkey, matchesHeldModifiers, resolveAppHotkey } from "@tanstack/hotkeys";
4
5
  import { computed } from "@angular/core";
5
6
 
@@ -17,7 +18,7 @@ function injectHotkeyHint(hotkey, options = {}) {
17
18
  const context = injectHotkeysContext();
18
19
  const manager = getHotkeyManager();
19
20
  const registrations = injectSelector(manager.registrations, (state) => state);
20
- const held = injectSelector(getKeyStateTracker().store, (state) => state.heldKeys);
21
+ const held = injectSelector(runOutsideZone(getKeyStateTracker).store, (state) => state.heldKeys);
21
22
  return computed(() => {
22
23
  context.appHotkeysState();
23
24
  const value = typeof hotkey === "function" ? hotkey() : hotkey;
@@ -1,23 +1,33 @@
1
1
  import { injectDefaultHotkeysOptions } from "./hotkeys-provider.js";
2
2
  import { HotkeyRecorder } from "@tanstack/hotkeys";
3
- import { DestroyRef, computed, effect, inject, linkedSignal, untracked } from "@angular/core";
3
+ import { DestroyRef, NgZone, computed, effect, inject, linkedSignal, untracked } from "@angular/core";
4
4
  import { withDefaults } from "@tanstack/hotkeys/adapter";
5
5
 
6
6
  //#region src/injectHotkeyRecorder.ts
7
7
  function injectHotkeyRecorder(options) {
8
8
  const defaultOptions = injectDefaultHotkeysOptions();
9
9
  const destroyRef = inject(DestroyRef);
10
- const recorderSignal = computed(() => untracked(() => {
11
- const resolvedOptions = typeof options === "function" ? options() : options;
12
- return new HotkeyRecorder(withDefaults(defaultOptions.hotkeyRecorder, resolvedOptions));
13
- }));
10
+ const zone = inject(NgZone);
11
+ const resolve = () => {
12
+ const resolved = withDefaults(defaultOptions.hotkeyRecorder, typeof options === "function" ? options() : options);
13
+ for (const key of [
14
+ "onRecord",
15
+ "onCancel",
16
+ "onClear",
17
+ "onReject"
18
+ ]) {
19
+ const callback = resolved[key];
20
+ if (callback) resolved[key] = (...args) => zone.run(callback, null, args);
21
+ }
22
+ return resolved;
23
+ };
24
+ const recorderSignal = computed(() => untracked(() => new HotkeyRecorder(resolve())));
14
25
  const recorderStore = computed(() => untracked(() => recorderSignal().store));
15
26
  const isRecording = injectLazyStore(recorderStore, (state) => state.isRecording);
16
27
  const steps = injectLazyStore(recorderStore, (state) => state.steps);
17
28
  const recordedHotkey = injectLazyStore(recorderStore, (state) => state.recordedHotkey);
18
29
  effect(() => {
19
- const resolved = typeof options === "function" ? options() : options;
20
- recorderSignal().setOptions(withDefaults(defaultOptions.hotkeyRecorder, resolved));
30
+ recorderSignal().setOptions(resolve());
21
31
  });
22
32
  destroyRef.onDestroy(() => {
23
33
  recorderSignal().destroy();
@@ -26,7 +36,7 @@ function injectHotkeyRecorder(options) {
26
36
  isRecording,
27
37
  steps,
28
38
  recordedHotkey,
29
- startRecording: () => recorderSignal().start(),
39
+ startRecording: () => zone.runOutsideAngular(() => recorderSignal().start()),
30
40
  stopRecording: () => recorderSignal().stop(),
31
41
  cancelRecording: () => recorderSignal().cancel(),
32
42
  commitRecording: () => recorderSignal().commit()
@@ -13,7 +13,9 @@ export type InjectHotkeyDefinition = AdapterHotkeyDefinition<InjectHotkeyTarget>
13
13
  * Options merge in this order: provider defaults, app hotkey declaration,
14
14
  * commonOptions, per-hotkey options. Each option applies only to the kinds
15
15
  * that support it. Unbound app hotkeys and empty sequences register nothing.
16
- * A `null` target skips registration until it is set.
16
+ * A target may be an element, an `ElementRef`, or a signal of either, such
17
+ * as `viewChild('panel')`. A `null` target, or a signal of `undefined`, skips
18
+ * registration until the element exists.
17
19
  *
18
20
  * @example
19
21
  * ```ts
@@ -1,5 +1,5 @@
1
1
  import { injectHotkeysContext } from "./hotkeys-provider.js";
2
- import { DestroyRef, effect, inject, untracked } from "@angular/core";
2
+ import { DestroyRef, ElementRef, NgZone, effect, inject, untracked } from "@angular/core";
3
3
  import { createHotkeyBindings, prepareHotkeyDefinitions } from "@tanstack/hotkeys/adapter";
4
4
 
5
5
  //#region src/injectHotkeys.ts
@@ -10,7 +10,9 @@ import { createHotkeyBindings, prepareHotkeyDefinitions } from "@tanstack/hotkey
10
10
  * Options merge in this order: provider defaults, app hotkey declaration,
11
11
  * commonOptions, per-hotkey options. Each option applies only to the kinds
12
12
  * that support it. Unbound app hotkeys and empty sequences register nothing.
13
- * A `null` target skips registration until it is set.
13
+ * A target may be an element, an `ElementRef`, or a signal of either, such
14
+ * as `viewChild('panel')`. A `null` target, or a signal of `undefined`, skips
15
+ * registration until the element exists.
14
16
  *
15
17
  * @example
16
18
  * ```ts
@@ -22,6 +24,7 @@ import { createHotkeyBindings, prepareHotkeyDefinitions } from "@tanstack/hotkey
22
24
  */
23
25
  function injectHotkeys(hotkeys, commonOptions = {}) {
24
26
  const context = injectHotkeysContext();
27
+ const zone = inject(NgZone);
25
28
  const bindings = createHotkeyBindings();
26
29
  inject(DestroyRef).onDestroy(() => bindings.destroy());
27
30
  effect(() => {
@@ -30,9 +33,16 @@ function injectHotkeys(hotkeys, commonOptions = {}) {
30
33
  appHotkeysStore: context.appHotkeysStore,
31
34
  defaults: context.defaultOptions.hotkey,
32
35
  commonOptions: typeof commonOptions === "function" ? commonOptions() : commonOptions,
33
- resolveTarget: (target) => target
36
+ resolveTarget: (target) => {
37
+ const value = typeof target === "function" ? target() : target;
38
+ return (value instanceof ElementRef ? value.nativeElement : value) ?? null;
39
+ }
34
40
  });
35
- untracked(() => bindings.update(definitions));
41
+ for (const definition of definitions) {
42
+ const callback = definition.callback;
43
+ definition.callback = (...args) => zone.run(callback, null, args);
44
+ }
45
+ untracked(() => zone.runOutsideAngular(() => bindings.update(definitions)));
36
46
  });
37
47
  }
38
48
 
@@ -1,4 +1,5 @@
1
1
  import { injectSelector } from "./_injectSelector.js";
2
+ import { runOutsideZone } from "./_zone.js";
2
3
  import { getKeyStateTracker } from "@tanstack/hotkeys";
3
4
  import { computed } from "@angular/core";
4
5
 
@@ -37,7 +38,7 @@ import { computed } from "@angular/core";
37
38
  * ```
38
39
  */
39
40
  function injectKeyHold(key) {
40
- const tracker = getKeyStateTracker();
41
+ const tracker = runOutsideZone(getKeyStateTracker);
41
42
  const heldKeysSelector = injectSelector(tracker.store, (state) => state.heldKeys);
42
43
  return computed(() => {
43
44
  const normalizedKey = (typeof key === "function" ? key() : key).toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/angular-hotkeys",
3
- "version": "0.19.1",
3
+ "version": "0.21.0",
4
4
  "description": "Angular adapter for TanStack Hotkeys",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -37,7 +37,7 @@
37
37
  "skills"
38
38
  ],
39
39
  "dependencies": {
40
- "@tanstack/hotkeys": "0.22.0"
40
+ "@tanstack/hotkeys": "0.23.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@angular/common": ">=19.0.0",
@@ -47,7 +47,8 @@
47
47
  "@angular/common": "^22.2.1",
48
48
  "@angular/core": "^22.2.1",
49
49
  "@angular/platform-browser-dynamic": "^22.2.1",
50
- "typescript": "6.0.3"
50
+ "typescript": "6.0.3",
51
+ "zone.js": "~0.16.3"
51
52
  },
52
53
  "scripts": {
53
54
  "clean": "premove ./build ./dist",
@@ -48,13 +48,8 @@ export class PanelComponent {
48
48
  () => this.save(),
49
49
  () => ({ enabled: this.isDirty() }),
50
50
  )
51
- injectHotkey(
52
- 'Escape',
53
- () => this.close(),
54
- () => ({
55
- target: this.panel()?.nativeElement ?? null,
56
- }),
57
- )
51
+ // A viewChild signal target registers once the element renders.
52
+ injectHotkey('Escape', () => this.close(), { target: this.panel })
58
53
  }
59
54
 
60
55
  save() {}
@@ -69,8 +64,9 @@ export class PanelComponent {
69
64
  | `injectHotkey(hotkey, callback, options?)` | Register one chord, sequence, or app hotkey for the component's or service's lifetime; options can be a function that reads signals. |
70
65
  | `injectHotkeys(definitions, commonOptions?)` | Register several hotkeys, or a function returning a list built from data, in one call. |
71
66
  | `provideHotkeys(options)` | Set default options, a scope (`exclusive` for dialogs), `enabled`, and app hotkeys in the app config or a component's `providers`. |
72
- | `injectHotkeysContext()` | Read the nearest provided defaults and enabled state, or `null` without a provider. |
67
+ | `injectHotkeysContext()` | Read the nearest provided defaults and enabled state. Activates that provider's scope. |
73
68
  | `injectDefaultHotkeysOptions()` | Read the default options merged from every provider above. |
69
+ | `injectFormatForDisplay()` | Get `formatForDisplay` with the provider's `platform` filled in, for labels in server-rendered apps. Call it in an injection context. |
74
70
  | `injectAppHotkeys()` | Get a signal of the declared app hotkeys with the user's custom hotkeys applied. |
75
71
  | `injectAppHotkeyList()` | Get a signal listing every app hotkey with its status, for a cheat sheet or settings screen. |
76
72
  | `injectCustomHotkeys()` | Read, change, and reset the user's custom hotkeys, and check a new one for conflicts. |
@@ -116,10 +112,13 @@ A component's `providers` apply to the component itself, so it can register its
116
112
 
117
113
  ## Pitfalls
118
114
 
119
- - **Pass options as a function.** `{ enabled: this.isDirty() }` is read once, in the constructor. A `viewChild` read there is `undefined`, and an undefined target means `document`. Use `() => ({ ... })`.
115
+ - **Pass options as a function.** `{ enabled: this.isDirty() }` is read once, in the constructor. Use `() => ({ ... })`.
116
+ - **Pass the `viewChild` signal as `target`, not its value.** `this.panel()` in the constructor is `undefined`, and an undefined target means `document`. `{ target: this.panel }` waits for the element.
117
+ - **An exclusive provider with no hotkeys never activates.** Angular creates providers lazily. Call `injectHotkeysContext()` in the component's constructor to activate the scope.
120
118
  - **Replace `@HostListener('document:keydown')` and `(keydown.control.s)` bindings with `injectHotkey`.**
121
119
  - **Register runtime lists in the constructor.** `injectHotkey` throws outside an injection context, such as in `ngOnInit`. For hotkeys from data, use `injectHotkeys(() => this.items().map(...))`.
122
120
  - **Call the inject functions directly.** A shortcut service or `injectShortcut(keys: string)` wrapper has to cast strings to hotkeys, which turns off type checking and drops sequences, signal options, and app hotkeys.
121
+ - **Server-rendered labels need the user's platform.** On the server, `formatForDisplay` can't see the user's OS, so it renders Linux labels that differ after hydration for Mac and Windows users. Pass `provideHotkeys({ defaultOptions: () => ({ platform: detectPlatform(inject(REQUEST)?.headers.get('user-agent')) }) })`, and format labels with `format = injectFormatForDisplay()` as a field, then `format(hotkey)` in the template. Keep plain `formatForDisplay` for labels that render only in the browser.
123
122
 
124
123
  When declaring app hotkeys, building a cheat sheet, or letting users rebind shortcuts, load `@tanstack/hotkeys#app-hotkeys` and read [app hotkeys in Angular](references/app-hotkeys.md).
125
124