@tanstack/angular-hotkeys 0.15.1 → 0.17.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/index.d.ts CHANGED
@@ -6,7 +6,8 @@ import { injectHeldKeyCodes } from "./injectHeldKeyCodes.js";
6
6
  import { injectKeyHold } from "./injectKeyHold.js";
7
7
  import { AngularHotkeyRecorder, injectHotkeyRecorder } from "./injectHotkeyRecorder.js";
8
8
  import { injectHotkeyRegistrations } from "./injectHotkeyRegistrations.js";
9
+ import { injectHotkeyScopesState } from "./injectHotkeyScopesState.js";
9
10
  import { injectHotkeyHint } from "./injectHotkeyHint.js";
10
11
  import { InjectCustomHotkeysResult, injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys } from "./injectAppHotkeys.js";
11
12
  export * from "@tanstack/hotkeys";
12
- 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, injectHotkeys, injectHotkeysContext, injectKeyHold, provideHotkeys };
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 };
package/dist/index.js CHANGED
@@ -6,9 +6,10 @@ import { injectHeldKeyCodes } from "./injectHeldKeyCodes.js";
6
6
  import { injectKeyHold } from "./injectKeyHold.js";
7
7
  import { injectHotkeyRecorder } from "./injectHotkeyRecorder.js";
8
8
  import { injectHotkeyRegistrations } from "./injectHotkeyRegistrations.js";
9
+ import { injectHotkeyScopesState } from "./injectHotkeyScopesState.js";
9
10
  import { injectHotkeyHint } from "./injectHotkeyHint.js";
10
11
  import { injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys } from "./injectAppHotkeys.js";
11
12
 
12
13
  export * from "@tanstack/hotkeys"
13
14
 
14
- export { HOTKEYS_INJECTION_TOKEN, injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys, injectDefaultHotkeysOptions, injectHeldKeyCodes, injectHeldKeys, injectHotkey, injectHotkeyHint, injectHotkeyRecorder, injectHotkeyRegistrations, injectHotkeys, injectHotkeysContext, injectKeyHold, provideHotkeys };
15
+ export { HOTKEYS_INJECTION_TOKEN, injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys, injectDefaultHotkeysOptions, injectHeldKeyCodes, injectHeldKeys, injectHotkey, injectHotkeyHint, injectHotkeyRecorder, injectHotkeyRegistrations, injectHotkeyScopesState, injectHotkeys, injectHotkeysContext, injectKeyHold, provideHotkeys };
@@ -3,10 +3,11 @@ import { Signal } from "@angular/core";
3
3
  //#region src/injectHotkeyHint.d.ts
4
4
  /**
5
5
  * Returns a signal indicating whether held modifiers reveal this shortcut.
6
- * Pass getters to track changing hotkeys and options. A sequence reveals its
7
- * hint from its first step, and an app hotkey uses its effective hotkey
8
- * (false while unbound). This helper does not register a shortcut or check
9
- * whether its target is focused.
6
+ * The hint shows only while the hotkey can fire: some registration of it is
7
+ * enabled and in an active scope. Pass getters to track changing hotkeys and
8
+ * options. A sequence reveals its hint from its first step, and an app hotkey
9
+ * matches its registrations by id and uses its effective hotkey (false while
10
+ * unbound). This helper does not register a shortcut. A hotkey on an element counts only while focus is inside it.
10
11
  */
11
12
  export declare function injectHotkeyHint(hotkey: RegisterableHotkey | AppHotkey | (() => RegisterableHotkey | AppHotkey), options?: HeldModifierOptions | (() => HeldModifierOptions)): Signal<boolean>;
12
13
  //#endregion
@@ -1,24 +1,27 @@
1
1
  import { injectHotkeysContext } from "./hotkeys-provider.js";
2
- import { getKeyStateTracker, isAppHotkey, matchesHeldModifiers, resolveAppHotkey } from "@tanstack/hotkeys";
2
+ import { getHotkeyManager, getKeyStateTracker, isAppHotkey, matchesHeldModifiers, resolveAppHotkey } from "@tanstack/hotkeys";
3
3
  import { computed } from "@angular/core";
4
4
  import { injectSelector } from "@tanstack/angular-store";
5
5
 
6
6
  //#region src/injectHotkeyHint.ts
7
7
  /**
8
8
  * Returns a signal indicating whether held modifiers reveal this shortcut.
9
- * Pass getters to track changing hotkeys and options. A sequence reveals its
10
- * hint from its first step, and an app hotkey uses its effective hotkey
11
- * (false while unbound). This helper does not register a shortcut or check
12
- * whether its target is focused.
9
+ * The hint shows only while the hotkey can fire: some registration of it is
10
+ * enabled and in an active scope. Pass getters to track changing hotkeys and
11
+ * options. A sequence reveals its hint from its first step, and an app hotkey
12
+ * matches its registrations by id and uses its effective hotkey (false while
13
+ * unbound). This helper does not register a shortcut. A hotkey on an element counts only while focus is inside it.
13
14
  */
14
15
  function injectHotkeyHint(hotkey, options = {}) {
15
16
  const context = injectHotkeysContext();
17
+ const manager = getHotkeyManager();
18
+ const registrations = injectSelector(manager.registrations, (state) => state);
16
19
  const held = injectSelector(getKeyStateTracker().store, (state) => state.heldKeys);
17
20
  return computed(() => {
18
21
  context.appHotkeysState();
19
22
  const value = typeof hotkey === "function" ? hotkey() : hotkey;
20
23
  const resolved = isAppHotkey(value) ? resolveAppHotkey(value, context.appHotkeysStore).hotkey : value;
21
- return resolved !== null && matchesHeldModifiers(resolved, held(), typeof options === "function" ? options() : options);
24
+ return resolved !== null && matchesHeldModifiers(resolved, held(), typeof options === "function" ? options() : options) && registrations().size > 0 && manager.hasActiveRegistration(value);
22
25
  });
23
26
  }
24
27
 
@@ -0,0 +1,27 @@
1
+ import { HotkeyScopesState } from "@tanstack/hotkeys";
2
+ import { Signal } from "@angular/core";
3
+ //#region src/injectHotkeyScopesState.d.ts
4
+ /**
5
+ * Angular inject-based API that returns a signal of the hotkey manager's
6
+ * scope state: the active scopes, the exclusive scope that owns the keyboard,
7
+ * and the scopes it allows or suspends. Updates when scopes activate or
8
+ * release.
9
+ *
10
+ * @returns Signal of the current scope state
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * @Component({
15
+ * template: `
16
+ * @if (scopes().exclusive; as exclusive) {
17
+ * <p>Shortcuts belong to {{ exclusive }}</p>
18
+ * }
19
+ * `,
20
+ * })
21
+ * export class KeyboardOwnerComponent {
22
+ * scopes = injectHotkeyScopesState()
23
+ * }
24
+ * ```
25
+ */
26
+ export declare function injectHotkeyScopesState(): Signal<HotkeyScopesState>;
27
+ //#endregion
@@ -0,0 +1,32 @@
1
+ import { getHotkeyManager } from "@tanstack/hotkeys";
2
+ import { injectSelector } from "@tanstack/angular-store";
3
+
4
+ //#region src/injectHotkeyScopesState.ts
5
+ /**
6
+ * Angular inject-based API that returns a signal of the hotkey manager's
7
+ * scope state: the active scopes, the exclusive scope that owns the keyboard,
8
+ * and the scopes it allows or suspends. Updates when scopes activate or
9
+ * release.
10
+ *
11
+ * @returns Signal of the current scope state
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * @Component({
16
+ * template: `
17
+ * @if (scopes().exclusive; as exclusive) {
18
+ * <p>Shortcuts belong to {{ exclusive }}</p>
19
+ * }
20
+ * `,
21
+ * })
22
+ * export class KeyboardOwnerComponent {
23
+ * scopes = injectHotkeyScopesState()
24
+ * }
25
+ * ```
26
+ */
27
+ function injectHotkeyScopesState() {
28
+ return injectSelector(getHotkeyManager().scopes, (state) => state);
29
+ }
30
+
31
+ //#endregion
32
+ export { injectHotkeyScopesState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/angular-hotkeys",
3
- "version": "0.15.1",
3
+ "version": "0.17.0",
4
4
  "description": "Angular adapter for TanStack Hotkeys",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -19,7 +19,8 @@
19
19
  "tanstack",
20
20
  "hotkeys",
21
21
  "keyboard",
22
- "shortcuts"
22
+ "shortcuts",
23
+ "tanstack-intent"
23
24
  ],
24
25
  "type": "module",
25
26
  "types": "./dist/index.d.ts",
@@ -32,11 +33,12 @@
32
33
  "node": ">=20"
33
34
  },
34
35
  "files": [
35
- "dist"
36
+ "dist",
37
+ "skills"
36
38
  ],
37
39
  "dependencies": {
38
40
  "@tanstack/angular-store": "^0.11.2",
39
- "@tanstack/hotkeys": "0.16.1"
41
+ "@tanstack/hotkeys": "0.18.0"
40
42
  },
41
43
  "peerDependencies": {
42
44
  "@angular/common": ">=19.0.0",
@@ -0,0 +1,128 @@
1
+ ---
2
+ name: angular-hotkeys
3
+ description: >-
4
+ Use when adding or fixing keyboard shortcuts in an Angular app with
5
+ @tanstack/angular-hotkeys: injectHotkey and injectHotkeys in an injection
6
+ context, signal-driven options, viewChild targets, provideHotkeys defaults
7
+ and component scopes, app hotkey injectors, recorders, held-key and hint
8
+ injectors, or replacing @HostListener keydown handlers.
9
+ metadata:
10
+ purpose: >-
11
+ Angular bindings for TanStack Hotkeys: inject functions, signal-driven
12
+ options, provideHotkeys scopes, and the Angular-specific mistakes to avoid.
13
+ type: framework
14
+ framework: angular
15
+ library: '@tanstack/angular-hotkeys'
16
+ library_version: '0.15.0'
17
+ sources:
18
+ - src/injectHotkey.ts
19
+ - src/injectHotkeys.ts
20
+ - src/hotkeys-provider.ts
21
+ - src/injectAppHotkeys.ts
22
+ - src/injectHotkeyRecorder.ts
23
+ - TanStack/hotkeys:docs/framework/angular/quick-start.md
24
+ - TanStack/hotkeys:docs/framework/angular/guides/provide-hotkeys-and-scopes.md
25
+ requires:
26
+ - '@tanstack/hotkeys#hotkeys-core'
27
+ ---
28
+
29
+ # TanStack Hotkeys in Angular
30
+
31
+ Before starting, load the `@tanstack/hotkeys#hotkeys-core` skill (`npx @tanstack/intent@latest load @tanstack/hotkeys#hotkeys-core`).
32
+
33
+ ```ts
34
+ import { Component, ElementRef, signal, viewChild } from '@angular/core'
35
+ import { injectHotkey } from '@tanstack/angular-hotkeys'
36
+
37
+ @Component({
38
+ selector: 'app-panel',
39
+ template: `<div #panel tabindex="0" (input)="isDirty.set(true)">Panel</div>`,
40
+ })
41
+ export class PanelComponent {
42
+ readonly isDirty = signal(false)
43
+ private readonly panel = viewChild<ElementRef<HTMLDivElement>>('panel')
44
+
45
+ constructor() {
46
+ injectHotkey(
47
+ 'Mod+S',
48
+ () => this.save(),
49
+ () => ({ enabled: this.isDirty() }),
50
+ )
51
+ injectHotkey(
52
+ 'Escape',
53
+ () => this.close(),
54
+ () => ({
55
+ target: this.panel()?.nativeElement ?? null,
56
+ }),
57
+ )
58
+ }
59
+
60
+ save() {}
61
+ close() {}
62
+ }
63
+ ```
64
+
65
+ ## API
66
+
67
+ | API | What it's for |
68
+ | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
+ | `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
+ | `injectHotkeys(definitions, commonOptions?)` | Register several hotkeys, or a function returning a list built from data, in one call. |
71
+ | `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. |
73
+ | `injectDefaultHotkeysOptions()` | Read the default options merged from every provider above. |
74
+ | `injectAppHotkeys()` | Get a signal of the declared app hotkeys with the user's custom hotkeys applied. |
75
+ | `injectAppHotkeyList()` | Get a signal listing every app hotkey with its status, for a cheat sheet or settings screen. |
76
+ | `injectCustomHotkeys()` | Read, change, and reset the user's custom hotkeys, and check a new one for conflicts. |
77
+ | `injectHotkeyRecorder(options)` | Record a chord or sequence the user presses, for a settings screen. |
78
+ | `injectHotkeyRegistrations()` | Get a signal listing every registered hotkey with its options, `meta`, and `status` (whether it's enabled and in an active scope). |
79
+ | `injectHotkeyScopesState()` | Get a signal of the active scopes and the exclusive scope that owns the keyboard. |
80
+ | `injectHotkeyHint(hotkey, options?)` | Get a signal of whether held modifiers should reveal a shortcut's hint. False unless a registration of the hotkey can fire: enabled, in an active scope, and with focus inside its element target. |
81
+ | `injectKeyHold(key)` | Get a signal of whether one key is held. |
82
+ | `injectHeldKeys()` | Get a signal of every key that's held right now. |
83
+ | `injectHeldKeyCodes()` | Get a signal mapping each held key to its physical `event.code`. |
84
+
85
+ The package also re-exports `@tanstack/hotkeys`, such as `formatForDisplay`, `matchesKeyboardEvent`, and `defineAppHotkeys`.
86
+
87
+ No provider is required. Call the inject functions in a constructor or field initializer.
88
+
89
+ ## A dialog that owns the keyboard
90
+
91
+ ```ts
92
+ import { Component, InjectionToken, inject, output } from '@angular/core'
93
+ import { injectHotkey, provideHotkeys } from '@tanstack/angular-hotkeys'
94
+
95
+ let nextDialogId = 0
96
+ const DIALOG_SCOPE = new InjectionToken<string>('DIALOG_SCOPE')
97
+
98
+ @Component({
99
+ selector: 'app-dialog',
100
+ providers: [
101
+ { provide: DIALOG_SCOPE, useFactory: () => `dialog-${++nextDialogId}` },
102
+ provideHotkeys({ scope: () => inject(DIALOG_SCOPE), exclusive: true }),
103
+ ],
104
+ template: `<div role="dialog"><ng-content /></div>`,
105
+ })
106
+ export class DialogComponent {
107
+ readonly closed = output()
108
+
109
+ constructor() {
110
+ injectHotkey('Escape', () => this.closed.emit())
111
+ }
112
+ }
113
+ ```
114
+
115
+ A component's `providers` apply to the component itself, so it can register its own scoped hotkeys. A fixed `scope: 'dialog'` would give every open dialog the same scope; the per-instance token gives each its own.
116
+
117
+ ## Pitfalls
118
+
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 `() => ({ ... })`.
120
+ - **Replace `@HostListener('document:keydown')` and `(keydown.control.s)` bindings with `injectHotkey`.**
121
+ - **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
+ - **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.
123
+
124
+ 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
+
126
+ ## API discovery and docs
127
+
128
+ Signatures: `node_modules/@tanstack/angular-hotkeys/dist/*.d.ts`. Guides: `https://tanstack.com/hotkeys/latest/docs/framework/angular/guides/<page>.md`, such as `hotkeys`, `provide-hotkeys-and-scopes`, or `app-hotkeys`. Devtools (Angular 21+): `hotkeysDevtoolsPlugin()` from `@tanstack/angular-hotkeys-devtools` with `provideTanStackDevtools`.
@@ -0,0 +1,80 @@
1
+ # App hotkeys in Angular
2
+
3
+ The `@tanstack/hotkeys#app-hotkeys` skill owns the rules. This is the Angular wiring.
4
+
5
+ ```ts
6
+ // app-hotkeys.ts
7
+ import { defineAppHotkeys } from '@tanstack/angular-hotkeys'
8
+
9
+ export const appHotkeys = defineAppHotkeys({
10
+ 'doc.save': { hotkey: 'Mod+S', meta: { name: 'Save' } },
11
+ 'help.shortcuts': { hotkey: 'Mod+/', meta: { name: 'Keyboard shortcuts' } },
12
+ })
13
+
14
+ declare module '@tanstack/angular-hotkeys' {
15
+ interface Register {
16
+ appHotkeys: typeof appHotkeys
17
+ }
18
+ }
19
+ ```
20
+
21
+ ```ts
22
+ import { provideHotkeys } from '@tanstack/angular-hotkeys'
23
+ import type { CustomHotkeys } from '@tanstack/angular-hotkeys'
24
+ import { appHotkeys } from './app-hotkeys'
25
+
26
+ const saved: CustomHotkeys = JSON.parse(localStorage.getItem('hotkeys') ?? '{}')
27
+
28
+ export const appConfig = {
29
+ providers: [
30
+ provideHotkeys({
31
+ appHotkeys,
32
+ customHotkeys: saved,
33
+ onCustomHotkeysChange: (custom) =>
34
+ localStorage.setItem('hotkeys', JSON.stringify(custom)),
35
+ }),
36
+ ],
37
+ }
38
+ ```
39
+
40
+ When saved shortcuts come from a server, load them before `bootstrapApplication`. `injectAppHotkeys()` returns a signal, so register inside a function: `injectHotkey(() => this.appHotkeys()['doc.save'], () => this.save())`. Build a cheat sheet from `injectAppHotkeyList()`.
41
+
42
+ A settings row records with `injectHotkeyRecorder` and saves through `injectCustomHotkeys`:
43
+
44
+ ```ts
45
+ import { Component, computed, input } from '@angular/core'
46
+ import {
47
+ formatForDisplay,
48
+ injectAppHotkeys,
49
+ injectCustomHotkeys,
50
+ injectHotkeyRecorder,
51
+ } from '@tanstack/angular-hotkeys'
52
+ import type { AppHotkeyId } from '@tanstack/angular-hotkeys'
53
+
54
+ @Component({
55
+ selector: 'app-shortcut-row',
56
+ template: `
57
+ {{ appHotkey().meta?.name }}:
58
+ {{ appHotkey().hotkey ? format(appHotkey().hotkey!) : 'Unassigned' }}
59
+ <button (click)="recorder.startRecording()">Change</button>
60
+ <button (click)="custom.resetCustomHotkey(id())">Reset</button>
61
+ `,
62
+ })
63
+ export class ShortcutRowComponent {
64
+ readonly id = input.required<AppHotkeyId>()
65
+ readonly appHotkeys = injectAppHotkeys()
66
+ readonly appHotkey = computed(() => this.appHotkeys()[this.id()])
67
+ readonly custom = injectCustomHotkeys()
68
+ readonly format = formatForDisplay
69
+
70
+ readonly recorder = injectHotkeyRecorder(() => ({
71
+ maxSteps: Infinity,
72
+ validate: (hotkey) => {
73
+ const [conflict] = this.custom.findConflicts(this.id(), hotkey)
74
+ return conflict ? `Used by ${conflict.meta?.name ?? conflict.id}` : true
75
+ },
76
+ onRecord: (hotkey) => this.custom.setCustomHotkey(this.id(), hotkey),
77
+ onClear: () => this.custom.setCustomHotkey(this.id(), null),
78
+ }))
79
+ }
80
+ ```