@tanstack/angular-hotkeys 0.15.1 → 0.16.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/angular-hotkeys",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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.
|
|
41
|
+
"@tanstack/hotkeys": "0.17.0"
|
|
40
42
|
},
|
|
41
43
|
"peerDependencies": {
|
|
42
44
|
"@angular/common": ">=19.0.0",
|
|
@@ -0,0 +1,127 @@
|
|
|
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 and `meta`. |
|
|
79
|
+
| `injectHotkeyHint(hotkey, options?)` | Get a signal of whether held modifiers should reveal a shortcut's hint. |
|
|
80
|
+
| `injectKeyHold(key)` | Get a signal of whether one key is held. |
|
|
81
|
+
| `injectHeldKeys()` | Get a signal of every key that's held right now. |
|
|
82
|
+
| `injectHeldKeyCodes()` | Get a signal mapping each held key to its physical `event.code`. |
|
|
83
|
+
|
|
84
|
+
The package also re-exports `@tanstack/hotkeys`, such as `formatForDisplay`, `matchesKeyboardEvent`, and `defineAppHotkeys`.
|
|
85
|
+
|
|
86
|
+
No provider is required. Call the inject functions in a constructor or field initializer.
|
|
87
|
+
|
|
88
|
+
## A dialog that owns the keyboard
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { Component, InjectionToken, inject, output } from '@angular/core'
|
|
92
|
+
import { injectHotkey, provideHotkeys } from '@tanstack/angular-hotkeys'
|
|
93
|
+
|
|
94
|
+
let nextDialogId = 0
|
|
95
|
+
const DIALOG_SCOPE = new InjectionToken<string>('DIALOG_SCOPE')
|
|
96
|
+
|
|
97
|
+
@Component({
|
|
98
|
+
selector: 'app-dialog',
|
|
99
|
+
providers: [
|
|
100
|
+
{ provide: DIALOG_SCOPE, useFactory: () => `dialog-${++nextDialogId}` },
|
|
101
|
+
provideHotkeys({ scope: () => inject(DIALOG_SCOPE), exclusive: true }),
|
|
102
|
+
],
|
|
103
|
+
template: `<div role="dialog"><ng-content /></div>`,
|
|
104
|
+
})
|
|
105
|
+
export class DialogComponent {
|
|
106
|
+
readonly closed = output()
|
|
107
|
+
|
|
108
|
+
constructor() {
|
|
109
|
+
injectHotkey('Escape', () => this.closed.emit())
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
## Pitfalls
|
|
117
|
+
|
|
118
|
+
- **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 `() => ({ ... })`.
|
|
119
|
+
- **Replace `@HostListener('document:keydown')` and `(keydown.control.s)` bindings with `injectHotkey`.**
|
|
120
|
+
- **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(...))`.
|
|
121
|
+
- **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.
|
|
122
|
+
|
|
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).
|
|
124
|
+
|
|
125
|
+
## API discovery and docs
|
|
126
|
+
|
|
127
|
+
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
|
+
```
|