@tanstack/hotkeys 0.16.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/hotkeys",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Type-safe, framework-agnostic keyboard hotkey management for the browser",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"keyboard",
|
|
22
22
|
"shortcuts",
|
|
23
23
|
"keybindings",
|
|
24
|
-
"typescript"
|
|
24
|
+
"typescript",
|
|
25
|
+
"tanstack-intent"
|
|
25
26
|
],
|
|
26
27
|
"type": "module",
|
|
27
28
|
"types": "./dist/index.d.ts",
|
|
@@ -35,7 +36,8 @@
|
|
|
35
36
|
"node": ">=20"
|
|
36
37
|
},
|
|
37
38
|
"files": [
|
|
38
|
-
"dist"
|
|
39
|
+
"dist",
|
|
40
|
+
"skills"
|
|
39
41
|
],
|
|
40
42
|
"dependencies": {
|
|
41
43
|
"@tanstack/store": "^0.11.2"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-hotkeys
|
|
3
|
+
description: >-
|
|
4
|
+
Use when an app built with TanStack Hotkeys needs a keyboard shortcut cheat
|
|
5
|
+
sheet or help dialog, a command palette whose commands may have shortcuts, a
|
|
6
|
+
settings screen where users rebind, unbind, or reset shortcuts, recording the
|
|
7
|
+
shortcut a user presses, conflict checks between shortcuts, or saving custom
|
|
8
|
+
shortcuts to storage.
|
|
9
|
+
metadata:
|
|
10
|
+
purpose: >-
|
|
11
|
+
Declare every shortcut once with defineAppHotkeys, then build cheat sheets,
|
|
12
|
+
rebinding, conflict checks, and persistence from those declarations instead
|
|
13
|
+
of a parallel shortcut registry.
|
|
14
|
+
type: core
|
|
15
|
+
library: '@tanstack/hotkeys'
|
|
16
|
+
library_version: '0.16.0'
|
|
17
|
+
sources:
|
|
18
|
+
- src/app-hotkeys.ts
|
|
19
|
+
- src/hotkey-recorder.ts
|
|
20
|
+
- src/conflicts.ts
|
|
21
|
+
- TanStack/hotkeys:docs/framework/react/guides/app-hotkeys.md
|
|
22
|
+
- TanStack/hotkeys:docs/framework/react/guides/cheat-sheet-ui-helpers.md
|
|
23
|
+
- TanStack/hotkeys:docs/framework/react/guides/custom-hotkey-recording.md
|
|
24
|
+
- 'TanStack/hotkeys:packages/react-hotkeys/tests/skills/shortcut-settings/**'
|
|
25
|
+
requires:
|
|
26
|
+
- hotkeys-core
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
# App hotkeys
|
|
30
|
+
|
|
31
|
+
This skill builds on `hotkeys-core`. The adapter skill's app hotkeys reference shows the wiring in your framework.
|
|
32
|
+
|
|
33
|
+
App hotkeys are the library's shortcut catalog. Use them instead of a separate shortcut config, context, or hand-written cheat sheet. One module declares every shortcut. The root provider holds the user's custom hotkeys. Components register the declared app hotkeys with their callbacks. Plain hotkeys with `meta` are enough when a list only needs what is mounted right now.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { defineAppHotkeys } from '@tanstack/hotkeys'
|
|
37
|
+
|
|
38
|
+
export const appHotkeys = defineAppHotkeys({
|
|
39
|
+
'doc.save': { hotkey: 'Mod+S', meta: { name: 'Save' }, scope: 'document' },
|
|
40
|
+
'comment.post': {
|
|
41
|
+
hotkey: 'Mod+S',
|
|
42
|
+
meta: { name: 'Post comment' },
|
|
43
|
+
scope: 'comments',
|
|
44
|
+
},
|
|
45
|
+
'theme.toggle': { hotkey: null, meta: { name: 'Toggle theme' } },
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Augment `Register` in the adapter module you import from, such as `declare module '@tanstack/react-hotkeys' { interface Register { appHotkeys: typeof appHotkeys } }`, to type ids. Lit, Alpine, and Ember have no provider: call `provideAppHotkeys(appHotkeys, options)` once at startup.
|
|
50
|
+
|
|
51
|
+
## Pitfalls
|
|
52
|
+
|
|
53
|
+
- **Register the app hotkey, not its value.** Pass `appHotkeys['doc.save']`. Passing `.hotkey` registers the declared default, so the user's rebinding never applies, and the list reports the shortcut as `'inactive'`.
|
|
54
|
+
- **The provider reads `customHotkeys` once.** When saved shortcuts load asynchronously, render the provider after they arrive, or key it by user. Change shortcuts afterwards through the adapter's custom hotkeys API, not by updating the prop.
|
|
55
|
+
- **Save what `onCustomHotkeysChange` receives.** It holds only the shortcuts the user changed, so users still get new defaults. Saving every effective hotkey freezes today's defaults into their data.
|
|
56
|
+
- **Ids are a storage contract.** Saved data is keyed by id, and unknown ids are dropped with a warning, so renaming an id discards users' customizations.
|
|
57
|
+
- **`setCustomHotkey` returns conflicts and changes nothing when there are any.** Check the result before reporting success, or reject the key press earlier by calling `findConflicts` in the recorder's `validate`.
|
|
58
|
+
- **Declare scopes on app hotkeys that share keys.** An app hotkey without a scope, or in `'global'`, conflicts with every other app hotkey on the same keys, so rebinding is rejected.
|
|
59
|
+
- **Conflict checks treat physical and logical bindings as different.** A recorded `Mod+[KeyP]` doesn't conflict with a declared `Mod+P`, although Command+P runs both. Declaring rebindable defaults in the recorder's form, such as `Mod+[KeyP]`, makes them comparable.
|
|
60
|
+
- **A cheat sheet dialog usually gets an exclusive scope.** Register the hotkey that opened it inside the dialog too, so the same keys close it.
|
|
61
|
+
|
|
62
|
+
When building the "press a new shortcut" control, read [recording](references/recording.md).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Recording a shortcut
|
|
2
|
+
|
|
3
|
+
Use the adapter's recorder instead of a `keydown` listener that builds a string. The recorder already handles the hard parts:
|
|
4
|
+
|
|
5
|
+
- It pauses registered hotkeys while recording, so pressing `Mod+S` to record it doesn't save. Leave your hotkeys enabled.
|
|
6
|
+
- It records `Mod` rather than Command or Control, and key positions such as `Mod+[KeyS]`, so Option+S records `Alt+[KeyS]` instead of `ß`. Save the recorded value as it is; `formatForDisplay` hides the brackets.
|
|
7
|
+
- Escape cancels. Backspace or Delete removes the last step, or with nothing recorded calls `onClear`, which changes nothing on its own: decide whether clearing unbinds (`setCustomHotkey(id, null)`) or resets (`resetCustomHotkey(id)`).
|
|
8
|
+
- With `maxSteps: Infinity`, the user can record a sequence, which commits on Enter or after `idleTimeoutMs`.
|
|
9
|
+
- `detectConflicts: true` checks only hotkeys registered right now. For app hotkeys, call `findConflicts(id, hotkey)` in `validate` and return a message to reject; a rejected recording keeps listening.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hotkeys-core
|
|
3
|
+
description: >-
|
|
4
|
+
Use when adding, fixing, or reviewing keyboard shortcuts with TanStack
|
|
5
|
+
Hotkeys: replacing keydown listeners or another shortcut library, deciding
|
|
6
|
+
when a hotkey works (enabled, scopes, targets), dialogs that own the
|
|
7
|
+
keyboard, sequences, or testing hotkeys. Load the framework adapter's skill
|
|
8
|
+
with it.
|
|
9
|
+
metadata:
|
|
10
|
+
purpose: >-
|
|
11
|
+
Framework-neutral rules for TanStack Hotkeys: the work the library already
|
|
12
|
+
does, how hotkeys turn on and off, and the pitfalls that don't surface as
|
|
13
|
+
errors.
|
|
14
|
+
type: core
|
|
15
|
+
library: '@tanstack/hotkeys'
|
|
16
|
+
library_version: '0.16.0'
|
|
17
|
+
sources:
|
|
18
|
+
- src/hotkey-manager.ts
|
|
19
|
+
- src/match.ts
|
|
20
|
+
- TanStack/hotkeys:docs/guides/sensible-defaults.md
|
|
21
|
+
- TanStack/hotkeys:docs/framework/react/guides/hotkeys.md
|
|
22
|
+
- TanStack/hotkeys:docs/framework/react/guides/sequences.md
|
|
23
|
+
- TanStack/hotkeys:docs/framework/react/guides/hotkeys-provider-and-scopes.md
|
|
24
|
+
- 'TanStack/hotkeys:packages/react-hotkeys/tests/skills/editor-shortcuts/**'
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# TanStack Hotkeys
|
|
28
|
+
|
|
29
|
+
The library owns the keyboard. Register every shortcut with your framework adapter's hotkey API, such as `useHotkey`, `createHotkey`, `injectHotkey`, or `@hotkey`, in the component that owns the action. Import everything from the adapter package; it re-exports `@tanstack/hotkeys`. The adapter's skill covers its bindings.
|
|
30
|
+
|
|
31
|
+
## Use the library instead of rebuilding it
|
|
32
|
+
|
|
33
|
+
| Instead of | Use |
|
|
34
|
+
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| A `keydown` listener that checks `event.key`, `metaKey`, or `ctrlKey` | A registered hotkey such as `'Mod+S'`. Inside a widget's own key handler, `matchesKeyboardEvent(event, 'Mod+Enter')`. |
|
|
36
|
+
| `isMac` checks and `⌘`/`Ctrl` label maps | `Mod` in the hotkey, and `formatForDisplay(hotkey)` for labels |
|
|
37
|
+
| Skipping events from text fields, calling `preventDefault` | The defaults: single keys and Shift/Alt chords are skipped while typing, `Mod` chords and `Escape` still fire, and matched keys are prevented |
|
|
38
|
+
| A shortcut registry, service, context, or wrapper hook | The adapter API, called where each action lives. For one list of every shortcut, load `@tanstack/hotkeys#app-hotkeys`. |
|
|
39
|
+
| A "shortcuts enabled" flag or topmost-dialog tracking | Scopes, with `exclusive` for dialogs |
|
|
40
|
+
| A hand-written shortcut list for a help dialog | `meta: { name }` and the adapter's registrations list, or app hotkeys |
|
|
41
|
+
| `keydown`/`keyup` state that tracks held keys | The adapter's held-key and hint APIs |
|
|
42
|
+
| Capturing the next key press for a settings screen | The adapter's recorder |
|
|
43
|
+
|
|
44
|
+
A workaround loses what the library tracks: scopes stop suspending it, recorders stop pausing it, devtools stop listing it, and layout and platform handling disappear.
|
|
45
|
+
|
|
46
|
+
## Turn hotkeys on and off
|
|
47
|
+
|
|
48
|
+
Use the first mechanism that fits:
|
|
49
|
+
|
|
50
|
+
1. **Lifetime.** A hotkey registered in a component exists while that component is mounted. A dialog's `Escape` belongs in the dialog.
|
|
51
|
+
2. **`enabled`.** A condition inside a mounted component.
|
|
52
|
+
3. **Scope.** A group that turns on and off together, such as a dialog, pane, or tab. An `exclusive` scope suspends every other scope, including global hotkeys, while it is active.
|
|
53
|
+
4. **`target`.** Only while focus is inside one element, which must be focusable (`tabindex="0"`).
|
|
54
|
+
|
|
55
|
+
When nesting scopes, stacking dialogs, or keeping some hotkeys working during an exclusive scope, read [scopes](references/scopes.md).
|
|
56
|
+
|
|
57
|
+
## Pitfalls
|
|
58
|
+
|
|
59
|
+
- **A disabled hotkey doesn't block the browser.** With `enabled: false`, `Mod+S` opens the Save Page dialog. To skip the action, keep the hotkey enabled and return early from the callback.
|
|
60
|
+
- **Modifiers match exactly.** `ArrowDown` never fires while Shift is held, so register `Shift+ArrowDown` instead of checking `event.shiftKey`. Shift that a symbol needs is the exception: `?` matches Shift+/.
|
|
61
|
+
- **Chords repeat while held.** Set `requireReset: true` on toggles, or holding the keys flickers them.
|
|
62
|
+
- **Letters follow the typed character.** For position-based controls such as WASD, use physical bindings such as `'[KeyW]'`. Keep letters for mnemonic commands such as `Mod+S`.
|
|
63
|
+
- **Each open exclusive scope needs its own name.** A reusable dialog with a fixed scope name breaks when one dialog opens another: both share one scope and both handle `Escape`. Build the name per instance.
|
|
64
|
+
- **A chord on a sequence's first key runs every time the sequence starts.** With `G` and `['G', 'G']`, the first `G` runs the chord. Later keys belong to the sequence. To show "waiting for the next key", read `matchedStepCount` from the adapter's registrations list.
|
|
65
|
+
- **Single-character shortcuts need an off switch.** For WCAG 2.1.4, `K`, `?`, and `['G', 'H']` need a user setting passed as `enabled`, or an app hotkey users can remap.
|
|
66
|
+
- **react-hotkeys-hook names don't carry over.** `useHotkeys` and `HotkeysProvider` share names, not signatures: there's no dependency array, `preventDefault` is on by default, and sequences are arrays. Remove the old library, so two libraries don't handle the same keys.
|
|
67
|
+
- **Install only the adapter.** A separately installed `@tanstack/hotkeys` at another version is a second copy, and scopes don't apply across copies.
|
|
68
|
+
|
|
69
|
+
When writing tests for hotkeys, read [testing](references/testing.md).
|
|
70
|
+
|
|
71
|
+
## API discovery
|
|
72
|
+
|
|
73
|
+
Read the installed declarations for exact signatures: the adapter's `dist/*.d.ts`, then the core's `hotkey.types.d.ts` (hotkey strings) and `hotkey-manager.d.ts` (`HotkeyOptions`). With pnpm, the core package sits beside the adapter: `ls "$(realpath node_modules/@tanstack/react-hotkeys)/../hotkeys/dist"`, using your adapter's name.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Scopes
|
|
2
|
+
|
|
3
|
+
- A scope is a name, not an instance. Two mounted providers with the same scope share it, and it stays active until both are gone.
|
|
4
|
+
- Hotkeys outside every scoped provider are in `'global'`, which is always active unless an exclusive scope suspends it.
|
|
5
|
+
- When hotkeys in two active scopes match the same key, a nested scope beats the scopes around it; otherwise the most recently activated scope wins. Only the winning scope's hotkeys run.
|
|
6
|
+
- The same hotkey in two scopes is not a duplicate, so there's no conflict warning.
|
|
7
|
+
- An `exclusive` scope suspends every other scope except scopes nested in it and those in `exclusive: { allow: ['global'] }`. With several exclusive scopes active, the one with the highest precedence owns the keyboard, so a dialog opened from a dialog suspends the first until it closes.
|
|
8
|
+
- A provider with `enabled: false` turns off its scope and every scope nested in it while staying mounted, for tabs and panes that stay rendered.
|
|
9
|
+
- A hotkey's `scope` option, or an app hotkey's declared `scope`, overrides the provider's.
|
|
10
|
+
- Scopes follow the component tree, not the DOM, so a portaled dialog keeps its scope.
|
|
11
|
+
- The hotkey that opens an exclusive dialog is suspended with the rest of the page. To close the dialog with the same keys, register that hotkey inside the dialog too.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Testing hotkeys
|
|
2
|
+
|
|
3
|
+
- **Set the platform.** Test DOMs don't report a real platform (happy-dom on macOS detects `windows`), so `Mod` changes meaning between machines. Pass `platform: 'mac'` in the provider's `defaultOptions.hotkey`, or on the hotkey.
|
|
4
|
+
- **Reset between tests.** Call `HotkeyManager.resetInstance()`, plus `KeyStateTracker.resetInstance()` for held-key tests, in `afterEach`. Registrations and scope activations otherwise outlive a test that fails before unmounting.
|
|
5
|
+
- **Dispatch real events.** Use `new KeyboardEvent('keydown', { key: 's', code: 'KeyS', metaKey: true, bubbles: true, cancelable: true })`. Physical bindings such as `[KeyS]` match only `code`. Check `event.defaultPrevented` to assert the browser action was blocked.
|
|
6
|
+
- **Dispatch where focus is.** For a hotkey with a `target`, or to test typing in a field, focus the element and dispatch on it with `bubbles: true`.
|
|
7
|
+
- **Repeats and sequences.** `repeat: true` simulates holding a key; it fires chords again unless they set `requireReset`, and it never advances a sequence. Use fake timers to test a sequence's `timeout` (1000 ms by default).
|