@tanstack/angular-hotkeys 0.14.5 → 0.15.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/README.md CHANGED
@@ -51,7 +51,7 @@
51
51
  # TanStack Hotkeys
52
52
 
53
53
  > [!NOTE]
54
- > TanStack Hotkeys is alpha. We are actively developing the library and are open to feedback and contributions.
54
+ > TanStack Hotkeys is in beta. We are actively developing the library and are open to feedback and contributions.
55
55
 
56
56
  Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objects, a cross-platform `Mod` key, a singleton Hotkey Manager, and utilities for cheatsheet UIs—built to stay SSR-friendly.
57
57
 
@@ -1,6 +1,7 @@
1
1
  import { InjectHotkeyCommonOptions } from "./injectHotkey.js";
2
2
  import { AppHotkeys, AppHotkeysState, AppHotkeysStore, CustomHotkeys, HotkeyChordRecorderOptions, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
3
3
  import { InjectionToken, Provider, Signal } from "@angular/core";
4
+ import { HotkeysProviderScopeProps } from "@tanstack/hotkeys/adapter";
4
5
  //#region src/hotkeys-provider.d.ts
5
6
  export interface HotkeysProviderOptions {
6
7
  /**
@@ -13,7 +14,19 @@ export interface HotkeysProviderOptions {
13
14
  }
14
15
  /** Options for `provideHotkeys`. */
15
16
  export interface ProvideHotkeysOptions {
17
+ /** Merged with the parent provider's defaults. */
16
18
  defaultOptions?: HotkeysProviderOptions;
19
+ /**
20
+ * See `HotkeysProviderScopeProps['scope']`. Getters, here and for
21
+ * `exclusive` and `enabled`, run in the provider's injection context, so
22
+ * they can `inject()` the component that provides them and read its
23
+ * signals.
24
+ */
25
+ scope?: MaybeGetter<HotkeysProviderScopeProps['scope']>;
26
+ /** See `HotkeysProviderScopeProps['exclusive']`. */
27
+ exclusive?: MaybeGetter<HotkeysProviderScopeProps['exclusive']>;
28
+ /** See `HotkeysProviderScopeProps['enabled']`. */
29
+ enabled?: MaybeGetter<HotkeysProviderScopeProps['enabled']>;
17
30
  /**
18
31
  * Every app hotkey, from `defineAppHotkeys`. Listed by
19
32
  * `injectAppHotkeyList` whether or not they are registered.
@@ -28,16 +41,27 @@ export interface ProvideHotkeysOptions {
28
41
  onCustomHotkeysChange?: (customHotkeys: CustomHotkeys) => void;
29
42
  }
30
43
  export interface HotkeysContextValue {
44
+ /** Merged with every ancestor provider's defaults, including `hotkey.scope`. */
31
45
  readonly defaultOptions: HotkeysProviderOptions;
46
+ /** Whether this provider and every ancestor are enabled. */
47
+ readonly enabled: Signal<boolean>;
32
48
  readonly appHotkeysStore: AppHotkeysStore | null;
33
49
  /** Tracks app hotkeys and custom hotkeys so signals and effects re-run. */
34
50
  readonly appHotkeysState: Signal<AppHotkeysState | null>;
35
51
  }
36
52
  export declare const HOTKEYS_INJECTION_TOKEN: InjectionToken<HotkeysContextValue>;
53
+ /** A value, or a signal or getter that returns it. */
54
+ export type MaybeGetter<T> = T | (() => T);
37
55
  /**
38
56
  * Provides default options, app hotkeys, and the user's custom hotkeys to an
39
57
  * injector, such as the application or a component.
40
58
  *
59
+ * Provide it in a component to create a hotkey scope. A nested provider
60
+ * inherits app hotkeys and merges default options. With `scope`, hotkeys
61
+ * injected in the component and its children fire only while it exists and
62
+ * is enabled; with `exclusive`, only they fire. The scope activates when the
63
+ * first hotkey in it is injected.
64
+ *
41
65
  * @example
42
66
  * ```ts
43
67
  * bootstrapApplication(App, {
@@ -51,6 +75,39 @@ export declare const HOTKEYS_INJECTION_TOKEN: InjectionToken<HotkeysContextValue
51
75
  * ],
52
76
  * })
53
77
  * ```
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * @Component({
82
+ * selector: 'app-edit-dialog',
83
+ * providers: [provideHotkeys({ scope: 'edit-dialog', exclusive: true })],
84
+ * template: `...`,
85
+ * })
86
+ * export class EditDialog {
87
+ * constructor() {
88
+ * injectHotkey('Escape', () => this.close())
89
+ * }
90
+ * }
91
+ * ```
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * // A scope that follows the providing component's input.
96
+ * @Component({
97
+ * selector: 'app-folder',
98
+ * providers: [
99
+ * provideHotkeys({
100
+ * scope: () => inject(Folder).name(),
101
+ * enabled: () => inject(Folder).current(),
102
+ * }),
103
+ * ],
104
+ * template: `...`,
105
+ * })
106
+ * export class Folder {
107
+ * readonly name = input.required<string>()
108
+ * readonly current = input(false)
109
+ * }
110
+ * ```
54
111
  */
55
112
  export declare function provideHotkeys(options?: ProvideHotkeysOptions): Provider;
56
113
  export declare function injectHotkeysContext(): HotkeysContextValue;
@@ -1,20 +1,73 @@
1
1
  import { AppHotkeysStore, publishAppHotkeysStore } from "@tanstack/hotkeys";
2
- import { DestroyRef, InjectionToken, inject, signal } from "@angular/core";
2
+ import { DestroyRef, InjectionToken, Injector, computed, effect, inject, runInInjectionContext, signal } from "@angular/core";
3
+ import { activateHotkeysProviderScope, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults } from "@tanstack/hotkeys/adapter";
3
4
 
4
5
  //#region src/hotkeys-provider.ts
5
6
  const NO_APP_HOTKEYS = signal(null).asReadonly();
7
+ const ROOT_CONTEXT = {
8
+ defaultOptions: {},
9
+ enabled: signal(true).asReadonly(),
10
+ appHotkeysStore: null,
11
+ appHotkeysState: NO_APP_HOTKEYS
12
+ };
6
13
  const HOTKEYS_INJECTION_TOKEN = new InjectionToken("HOTKEYS_INJECTION_TOKEN", {
7
14
  providedIn: "root",
8
- factory: () => ({
9
- defaultOptions: {},
10
- appHotkeysStore: null,
11
- appHotkeysState: NO_APP_HOTKEYS
12
- })
15
+ factory: () => ROOT_CONTEXT
13
16
  });
17
+ /** Creates a provider's context. Runs in an injection context. */
18
+ function createHotkeysContext(options) {
19
+ const injector = inject(Injector);
20
+ const read = (value) => typeof value === "function" ? runInInjectionContext(injector, value) : value;
21
+ const parent = inject(HOTKEYS_INJECTION_TOKEN, {
22
+ skipSelf: true,
23
+ optional: true
24
+ }) ?? ROOT_CONTEXT;
25
+ const destroyRef = inject(DestroyRef);
26
+ const { appHotkeys, customHotkeys, onCustomHotkeysChange } = options;
27
+ let store = null;
28
+ let appHotkeysState = parent.appHotkeysState;
29
+ if (appHotkeys) {
30
+ store = new AppHotkeysStore(appHotkeys, {
31
+ customHotkeys,
32
+ onCustomHotkeysChange
33
+ });
34
+ const state = signal(store.store.state);
35
+ const subscription = store.store.subscribe(() => state.set(store.store.state));
36
+ destroyRef.onDestroy(() => subscription.unsubscribe());
37
+ if (typeof document !== "undefined") destroyRef.onDestroy(publishAppHotkeysStore(store));
38
+ appHotkeysState = state.asReadonly();
39
+ }
40
+ const enabled = computed(() => parent.enabled() && read(options.enabled) !== false);
41
+ const defaultOptions = computed(() => mergeHotkeysProviderDefaults(parent.defaultOptions, options.defaultOptions, read(options.scope)));
42
+ const scopeKey = computed(() => getHotkeysProviderScopeKey({
43
+ scope: read(options.scope),
44
+ exclusive: read(options.exclusive),
45
+ active: enabled(),
46
+ parent: parent.defaultOptions.hotkey?.scope
47
+ }));
48
+ if (typeof document !== "undefined") effect((onCleanup) => {
49
+ const key = scopeKey();
50
+ if (key) onCleanup(activateHotkeysProviderScope(key));
51
+ });
52
+ return {
53
+ get defaultOptions() {
54
+ return defaultOptions();
55
+ },
56
+ enabled,
57
+ appHotkeysStore: store ?? parent.appHotkeysStore,
58
+ appHotkeysState
59
+ };
60
+ }
14
61
  /**
15
62
  * Provides default options, app hotkeys, and the user's custom hotkeys to an
16
63
  * injector, such as the application or a component.
17
64
  *
65
+ * Provide it in a component to create a hotkey scope. A nested provider
66
+ * inherits app hotkeys and merges default options. With `scope`, hotkeys
67
+ * injected in the component and its children fire only while it exists and
68
+ * is enabled; with `exclusive`, only they fire. The scope activates when the
69
+ * first hotkey in it is injected.
70
+ *
18
71
  * @example
19
72
  * ```ts
20
73
  * bootstrapApplication(App, {
@@ -28,32 +81,44 @@ const HOTKEYS_INJECTION_TOKEN = new InjectionToken("HOTKEYS_INJECTION_TOKEN", {
28
81
  * ],
29
82
  * })
30
83
  * ```
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * @Component({
88
+ * selector: 'app-edit-dialog',
89
+ * providers: [provideHotkeys({ scope: 'edit-dialog', exclusive: true })],
90
+ * template: `...`,
91
+ * })
92
+ * export class EditDialog {
93
+ * constructor() {
94
+ * injectHotkey('Escape', () => this.close())
95
+ * }
96
+ * }
97
+ * ```
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * // A scope that follows the providing component's input.
102
+ * @Component({
103
+ * selector: 'app-folder',
104
+ * providers: [
105
+ * provideHotkeys({
106
+ * scope: () => inject(Folder).name(),
107
+ * enabled: () => inject(Folder).current(),
108
+ * }),
109
+ * ],
110
+ * template: `...`,
111
+ * })
112
+ * export class Folder {
113
+ * readonly name = input.required<string>()
114
+ * readonly current = input(false)
115
+ * }
116
+ * ```
31
117
  */
32
118
  function provideHotkeys(options = {}) {
33
119
  return {
34
120
  provide: HOTKEYS_INJECTION_TOKEN,
35
- useFactory: () => {
36
- const defaultOptions = options.defaultOptions ?? {};
37
- if (!options.appHotkeys) return {
38
- defaultOptions,
39
- appHotkeysStore: null,
40
- appHotkeysState: NO_APP_HOTKEYS
41
- };
42
- const store = new AppHotkeysStore(options.appHotkeys, {
43
- customHotkeys: options.customHotkeys,
44
- onCustomHotkeysChange: options.onCustomHotkeysChange
45
- });
46
- const state = signal(store.store.state);
47
- const subscription = store.store.subscribe(() => state.set(store.store.state));
48
- const destroyRef = inject(DestroyRef);
49
- destroyRef.onDestroy(() => subscription.unsubscribe());
50
- if (typeof document !== "undefined") destroyRef.onDestroy(publishAppHotkeysStore(store));
51
- return {
52
- defaultOptions,
53
- appHotkeysStore: store,
54
- appHotkeysState: state.asReadonly()
55
- };
56
- }
121
+ useFactory: () => createHotkeysContext(options)
57
122
  };
58
123
  }
59
124
  function injectHotkeysContext() {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { InjectAppHotkeyOptions, InjectHotkeyChordOptions, InjectHotkeyCommonOptions, InjectHotkeyOptions, InjectHotkeyOptionsFor, InjectHotkeySequenceOptions, InjectHotkeyTarget, injectHotkey } from "./injectHotkey.js";
2
- import { HOTKEYS_INJECTION_TOKEN, HotkeysContextValue, HotkeysProviderOptions, ProvideHotkeysOptions, injectDefaultHotkeysOptions, injectHotkeysContext, provideHotkeys } from "./hotkeys-provider.js";
2
+ import { HOTKEYS_INJECTION_TOKEN, HotkeysContextValue, HotkeysProviderOptions, MaybeGetter, ProvideHotkeysOptions, injectDefaultHotkeysOptions, injectHotkeysContext, provideHotkeys } from "./hotkeys-provider.js";
3
3
  import { InjectHotkeyDefinition, injectHotkeys } from "./injectHotkeys.js";
4
4
  import { injectHeldKeys } from "./injectHeldKeys.js";
5
5
  import { injectHeldKeyCodes } from "./injectHeldKeyCodes.js";
@@ -9,4 +9,4 @@ import { injectHotkeyRegistrations } from "./injectHotkeyRegistrations.js";
9
9
  import { injectHotkeyHint } from "./injectHotkeyHint.js";
10
10
  import { InjectCustomHotkeysResult, injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys } from "./injectAppHotkeys.js";
11
11
  export * from "@tanstack/hotkeys";
12
- export { AngularHotkeyRecorder, HOTKEYS_INJECTION_TOKEN, HotkeysContextValue, HotkeysProviderOptions, InjectAppHotkeyOptions, InjectCustomHotkeysResult, InjectHotkeyChordOptions, InjectHotkeyCommonOptions, InjectHotkeyDefinition, InjectHotkeyOptions, InjectHotkeyOptionsFor, InjectHotkeySequenceOptions, InjectHotkeyTarget, ProvideHotkeysOptions, injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys, injectDefaultHotkeysOptions, injectHeldKeyCodes, injectHeldKeys, injectHotkey, injectHotkeyHint, injectHotkeyRecorder, injectHotkeyRegistrations, injectHotkeys, injectHotkeysContext, injectKeyHold, provideHotkeys };
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 };
@@ -7,9 +7,9 @@ import { Signal } from "@angular/core";
7
7
  *
8
8
  * @example
9
9
  * ```ts
10
- * hotkeys = injectAppHotkeys()
10
+ * appHotkeys = injectAppHotkeys()
11
11
  * constructor() {
12
- * injectHotkey(() => this.hotkeys()['doc.save'], () => this.save())
12
+ * injectHotkey(() => this.appHotkeys()['doc.save'], () => this.save())
13
13
  * }
14
14
  * ```
15
15
  */
@@ -17,9 +17,9 @@ function injectRequiredStore(name) {
17
17
  *
18
18
  * @example
19
19
  * ```ts
20
- * hotkeys = injectAppHotkeys()
20
+ * appHotkeys = injectAppHotkeys()
21
21
  * constructor() {
22
- * injectHotkey(() => this.hotkeys()['doc.save'], () => this.save())
22
+ * injectHotkey(() => this.appHotkeys()['doc.save'], () => this.save())
23
23
  * }
24
24
  * ```
25
25
  */
@@ -1,4 +1,4 @@
1
- import { AppHotkey, HotkeyCallback, RegisterableHotkey, Target } from "@tanstack/hotkeys";
1
+ import { AppHotkey, HotkeyCallback, HotkeyCallbackContextFor, RegisterableHotkey, Target } from "@tanstack/hotkeys";
2
2
  import { AdapterAppHotkeyOptions, AdapterChordOptions, AdapterCommonOptions, AdapterHotkeyOptions, AdapterOptionsFor, AdapterSequenceOptions } from "@tanstack/hotkeys/adapter";
3
3
  //#region src/injectHotkey.d.ts
4
4
  /**
@@ -43,5 +43,5 @@ type MaybeGetter<T> = T | (() => T);
43
43
  * ```
44
44
  */
45
45
  export declare function injectHotkey<T extends AppHotkey>(hotkey: MaybeGetter<T>, callback: HotkeyCallback, options?: MaybeGetter<InjectAppHotkeyOptions<T>>): void;
46
- export declare function injectHotkey<T extends RegisterableHotkey>(hotkey: MaybeGetter<T>, callback: HotkeyCallback, options?: MaybeGetter<InjectHotkeyOptionsFor<T>>): void;
46
+ export declare function injectHotkey<const T extends RegisterableHotkey>(hotkey: MaybeGetter<T>, callback: HotkeyCallback<HotkeyCallbackContextFor<T>>, options?: MaybeGetter<InjectHotkeyOptionsFor<T>>): void;
47
47
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/angular-hotkeys",
3
- "version": "0.14.5",
3
+ "version": "0.15.0",
4
4
  "description": "Angular adapter for TanStack Hotkeys",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "@tanstack/angular-store": "^0.11.2",
39
- "@tanstack/hotkeys": "0.15.1"
39
+ "@tanstack/hotkeys": "0.16.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@angular/common": ">=19.0.0",