From e4b993ae196515a46717ef7e3360aac4de80bf63 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Tue, 24 Mar 2026 07:47:54 -0500 Subject: [PATCH 1/4] feat: useHotkeySequences hooks --- .changeset/plural-sequences.md | 10 + docs/config.json | 76 +++++ docs/framework/angular/guides/sequences.md | 28 ++ .../functions/injectHotkeySequences.md | 54 ++++ docs/framework/angular/reference/index.md | 2 + .../InjectHotkeySequenceDefinition.md | 48 +++ docs/framework/preact/guides/sequences.md | 15 + .../reference/functions/useHotkeySequences.md | 67 +++++ docs/framework/preact/reference/index.md | 2 + .../interfaces/UseHotkeySequenceDefinition.md | 46 +++ docs/framework/react/guides/sequences.md | 15 + .../reference/functions/useHotkeySequences.md | 67 +++++ docs/framework/react/reference/index.md | 2 + .../interfaces/UseHotkeySequenceDefinition.md | 46 +++ docs/framework/solid/guides/sequences.md | 15 + .../functions/createHotkeySequences.md | 64 ++++ docs/framework/solid/reference/index.md | 2 + .../CreateHotkeySequenceDefinition.md | 46 +++ docs/framework/svelte/guides/sequences.md | 15 + .../functions/createHotkeySequences.md | 41 +++ .../createHotkeySequencesAttachment.md | 43 +++ docs/framework/svelte/reference/index.md | 3 + .../CreateHotkeySequenceDefinition.md | 46 +++ docs/framework/vue/guides/sequences.md | 17 ++ docs/framework/vue/quick-start.md | 2 + .../reference/functions/useHotkeySequences.md | 70 +++++ docs/framework/vue/reference/index.md | 2 + .../interfaces/UseHotkeySequenceDefinition.md | 46 +++ .../injectHotkeySequences/angular.json | 63 ++++ .../injectHotkeySequences/package.json | 38 +++ .../src/app/app.component.css | 3 + .../src/app/app.component.html | 140 +++++++++ .../src/app/app.component.ts | 74 +++++ .../src/app/app.config.ts | 6 + .../injectHotkeySequences/src/index.html | 13 + .../angular/injectHotkeySequences/src/main.ts | 5 + .../injectHotkeySequences/src/styles.css | 117 ++++++++ .../injectHotkeySequences/tsconfig.json | 29 ++ .../useHotkeySequences/eslint.config.js | 11 + examples/preact/useHotkeySequences/index.html | 14 + .../preact/useHotkeySequences/package.json | 24 ++ .../preact/useHotkeySequences/src/index.css | 135 +++++++++ .../preact/useHotkeySequences/src/index.tsx | 257 ++++++++++++++++ .../preact/useHotkeySequences/tsconfig.json | 20 ++ .../preact/useHotkeySequences/vite.config.ts | 6 + .../react/useHotkeySequences/eslint.config.js | 11 + examples/react/useHotkeySequences/index.html | 14 + .../react/useHotkeySequences/package.json | 27 ++ .../react/useHotkeySequences/src/index.css | 159 ++++++++++ .../react/useHotkeySequences/src/index.tsx | 254 ++++++++++++++++ .../react/useHotkeySequences/tsconfig.json | 19 ++ .../react/useHotkeySequences/vite.config.ts | 6 + .../solid/createHotkeySequences/index.html | 11 + .../solid/createHotkeySequences/package.json | 20 ++ .../solid/createHotkeySequences/src/index.css | 141 +++++++++ .../solid/createHotkeySequences/src/index.tsx | 251 ++++++++++++++++ .../solid/createHotkeySequences/tsconfig.json | 14 + .../createHotkeySequences/vite.config.ts | 6 + .../svelte/create-hotkey-sequences/.gitignore | 23 ++ .../svelte/create-hotkey-sequences/.npmrc | 1 + .../svelte/create-hotkey-sequences/README.md | 42 +++ .../svelte/create-hotkey-sequences/index.html | 14 + .../create-hotkey-sequences/package.json | 17 ++ .../create-hotkey-sequences/src/App.svelte | 233 +++++++++++++++ .../create-hotkey-sequences/src/Root.svelte | 5 + .../create-hotkey-sequences/src/index.css | 162 +++++++++++ .../create-hotkey-sequences/src/main.ts | 5 + .../create-hotkey-sequences/static/robots.txt | 3 + .../create-hotkey-sequences/svelte.config.js | 11 + .../create-hotkey-sequences/tsconfig.json | 8 + .../create-hotkey-sequences/vite.config.ts | 6 + .../vue/useHotkeySequences/eslint.config.js | 11 + examples/vue/useHotkeySequences/index.html | 12 + examples/vue/useHotkeySequences/package.json | 24 ++ examples/vue/useHotkeySequences/src/App.vue | 217 ++++++++++++++ examples/vue/useHotkeySequences/src/index.css | 159 ++++++++++ examples/vue/useHotkeySequences/src/index.ts | 5 + examples/vue/useHotkeySequences/src/vue.d.ts | 6 + examples/vue/useHotkeySequences/tsconfig.json | 8 + .../vue/useHotkeySequences/vite.config.ts | 6 + packages/angular-hotkeys/src/index.ts | 1 + .../src/injectHotkeySequences.ts | 167 +++++++++++ packages/preact-hotkeys/src/index.ts | 1 + .../preact-hotkeys/src/useHotkeySequences.ts | 213 ++++++++++++++ .../tests/useHotkeySequences.test.tsx | 131 +++++++++ packages/react-hotkeys/src/index.ts | 1 + .../react-hotkeys/src/useHotkeySequences.ts | 213 ++++++++++++++ .../tests/useHotkeySequences.test.tsx | 189 ++++++++++++ .../src/createHotkeySequences.ts | 176 +++++++++++ packages/solid-hotkeys/src/index.ts | 1 + .../tests/createHotkeySequences.test.tsx | 132 +++++++++ .../src/createHotkeySequences.svelte.ts | 274 ++++++++++++++++++ packages/svelte-hotkeys/src/index.ts | 1 + packages/vue-hotkeys/src/index.ts | 1 + .../vue-hotkeys/src/useHotkeySequences.ts | 221 ++++++++++++++ pnpm-lock.yaml | 195 +++++++++++++ 96 files changed, 5673 insertions(+) create mode 100644 .changeset/plural-sequences.md create mode 100644 docs/framework/angular/reference/functions/injectHotkeySequences.md create mode 100644 docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md create mode 100644 docs/framework/preact/reference/functions/useHotkeySequences.md create mode 100644 docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md create mode 100644 docs/framework/react/reference/functions/useHotkeySequences.md create mode 100644 docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md create mode 100644 docs/framework/solid/reference/functions/createHotkeySequences.md create mode 100644 docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md create mode 100644 docs/framework/svelte/reference/functions/createHotkeySequences.md create mode 100644 docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md create mode 100644 docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md create mode 100644 docs/framework/vue/reference/functions/useHotkeySequences.md create mode 100644 docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md create mode 100644 examples/angular/injectHotkeySequences/angular.json create mode 100644 examples/angular/injectHotkeySequences/package.json create mode 100644 examples/angular/injectHotkeySequences/src/app/app.component.css create mode 100644 examples/angular/injectHotkeySequences/src/app/app.component.html create mode 100644 examples/angular/injectHotkeySequences/src/app/app.component.ts create mode 100644 examples/angular/injectHotkeySequences/src/app/app.config.ts create mode 100644 examples/angular/injectHotkeySequences/src/index.html create mode 100644 examples/angular/injectHotkeySequences/src/main.ts create mode 100644 examples/angular/injectHotkeySequences/src/styles.css create mode 100644 examples/angular/injectHotkeySequences/tsconfig.json create mode 100644 examples/preact/useHotkeySequences/eslint.config.js create mode 100644 examples/preact/useHotkeySequences/index.html create mode 100644 examples/preact/useHotkeySequences/package.json create mode 100644 examples/preact/useHotkeySequences/src/index.css create mode 100644 examples/preact/useHotkeySequences/src/index.tsx create mode 100644 examples/preact/useHotkeySequences/tsconfig.json create mode 100644 examples/preact/useHotkeySequences/vite.config.ts create mode 100644 examples/react/useHotkeySequences/eslint.config.js create mode 100644 examples/react/useHotkeySequences/index.html create mode 100644 examples/react/useHotkeySequences/package.json create mode 100644 examples/react/useHotkeySequences/src/index.css create mode 100644 examples/react/useHotkeySequences/src/index.tsx create mode 100644 examples/react/useHotkeySequences/tsconfig.json create mode 100644 examples/react/useHotkeySequences/vite.config.ts create mode 100644 examples/solid/createHotkeySequences/index.html create mode 100644 examples/solid/createHotkeySequences/package.json create mode 100644 examples/solid/createHotkeySequences/src/index.css create mode 100644 examples/solid/createHotkeySequences/src/index.tsx create mode 100644 examples/solid/createHotkeySequences/tsconfig.json create mode 100644 examples/solid/createHotkeySequences/vite.config.ts create mode 100644 examples/svelte/create-hotkey-sequences/.gitignore create mode 100644 examples/svelte/create-hotkey-sequences/.npmrc create mode 100644 examples/svelte/create-hotkey-sequences/README.md create mode 100644 examples/svelte/create-hotkey-sequences/index.html create mode 100644 examples/svelte/create-hotkey-sequences/package.json create mode 100644 examples/svelte/create-hotkey-sequences/src/App.svelte create mode 100644 examples/svelte/create-hotkey-sequences/src/Root.svelte create mode 100644 examples/svelte/create-hotkey-sequences/src/index.css create mode 100644 examples/svelte/create-hotkey-sequences/src/main.ts create mode 100644 examples/svelte/create-hotkey-sequences/static/robots.txt create mode 100644 examples/svelte/create-hotkey-sequences/svelte.config.js create mode 100644 examples/svelte/create-hotkey-sequences/tsconfig.json create mode 100644 examples/svelte/create-hotkey-sequences/vite.config.ts create mode 100644 examples/vue/useHotkeySequences/eslint.config.js create mode 100644 examples/vue/useHotkeySequences/index.html create mode 100644 examples/vue/useHotkeySequences/package.json create mode 100644 examples/vue/useHotkeySequences/src/App.vue create mode 100644 examples/vue/useHotkeySequences/src/index.css create mode 100644 examples/vue/useHotkeySequences/src/index.ts create mode 100644 examples/vue/useHotkeySequences/src/vue.d.ts create mode 100644 examples/vue/useHotkeySequences/tsconfig.json create mode 100644 examples/vue/useHotkeySequences/vite.config.ts create mode 100644 packages/angular-hotkeys/src/injectHotkeySequences.ts create mode 100644 packages/preact-hotkeys/src/useHotkeySequences.ts create mode 100644 packages/preact-hotkeys/tests/useHotkeySequences.test.tsx create mode 100644 packages/react-hotkeys/src/useHotkeySequences.ts create mode 100644 packages/react-hotkeys/tests/useHotkeySequences.test.tsx create mode 100644 packages/solid-hotkeys/src/createHotkeySequences.ts create mode 100644 packages/solid-hotkeys/tests/createHotkeySequences.test.tsx create mode 100644 packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts create mode 100644 packages/vue-hotkeys/src/useHotkeySequences.ts diff --git a/.changeset/plural-sequences.md b/.changeset/plural-sequences.md new file mode 100644 index 00000000..de87c91f --- /dev/null +++ b/.changeset/plural-sequences.md @@ -0,0 +1,10 @@ +--- +"@tanstack/react-hotkeys": minor +"@tanstack/preact-hotkeys": minor +"@tanstack/vue-hotkeys": minor +"@tanstack/solid-hotkeys": minor +"@tanstack/svelte-hotkeys": minor +"@tanstack/angular-hotkeys": minor +--- + +Add plural sequence registration APIs: `useHotkeySequences` (React/Preact/Vue), `createHotkeySequences` and `createHotkeySequencesAttachment` (Svelte), `createHotkeySequences` (Solid), and `injectHotkeySequences` (Angular). diff --git a/docs/config.json b/docs/config.json index 022ba669..faeb1534 100644 --- a/docs/config.json +++ b/docs/config.json @@ -652,6 +652,14 @@ { "label": "UseHotkeySequenceOptions", "to": "framework/react/reference/interfaces/UseHotkeySequenceOptions" + }, + { + "label": "useHotkeySequences", + "to": "framework/react/reference/functions/useHotkeySequences" + }, + { + "label": "UseHotkeySequenceDefinition", + "to": "framework/react/reference/interfaces/UseHotkeySequenceDefinition" } ] }, @@ -665,6 +673,14 @@ { "label": "UseHotkeySequenceOptions", "to": "framework/preact/reference/interfaces/UseHotkeySequenceOptions" + }, + { + "label": "useHotkeySequences", + "to": "framework/preact/reference/functions/useHotkeySequences" + }, + { + "label": "UseHotkeySequenceDefinition", + "to": "framework/preact/reference/interfaces/UseHotkeySequenceDefinition" } ] }, @@ -678,6 +694,14 @@ { "label": "CreateHotkeySequenceOptions", "to": "framework/solid/reference/interfaces/CreateHotkeySequenceOptions" + }, + { + "label": "createHotkeySequences", + "to": "framework/solid/reference/functions/createHotkeySequences" + }, + { + "label": "CreateHotkeySequenceDefinition", + "to": "framework/solid/reference/interfaces/CreateHotkeySequenceDefinition" } ] }, @@ -691,6 +715,14 @@ { "label": "InjectHotkeySequenceOptions", "to": "framework/angular/reference/interfaces/InjectHotkeySequenceOptions" + }, + { + "label": "injectHotkeySequences", + "to": "framework/angular/reference/functions/injectHotkeySequences" + }, + { + "label": "InjectHotkeySequenceDefinition", + "to": "framework/angular/reference/interfaces/InjectHotkeySequenceDefinition" } ] }, @@ -704,6 +736,14 @@ { "label": "UseHotkeySequenceOptions", "to": "framework/vue/reference/interfaces/UseHotkeySequenceOptions" + }, + { + "label": "useHotkeySequences", + "to": "framework/vue/reference/functions/useHotkeySequences" + }, + { + "label": "UseHotkeySequenceDefinition", + "to": "framework/vue/reference/interfaces/UseHotkeySequenceDefinition" } ] }, @@ -721,6 +761,18 @@ { "label": "CreateHotkeySequenceOptions", "to": "framework/svelte/reference/interfaces/CreateHotkeySequenceOptions" + }, + { + "label": "createHotkeySequences", + "to": "framework/svelte/reference/functions/createHotkeySequences" + }, + { + "label": "createHotkeySequencesAttachment", + "to": "framework/svelte/reference/functions/createHotkeySequencesAttachment" + }, + { + "label": "CreateHotkeySequenceDefinition", + "to": "framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition" } ] } @@ -1200,6 +1252,10 @@ "label": "useHotkeySequence", "to": "framework/react/examples/useHotkeySequence" }, + { + "label": "useHotkeySequences", + "to": "framework/react/examples/useHotkeySequences" + }, { "label": "useHotkeyRecorder", "to": "framework/react/examples/useHotkeyRecorder" @@ -1233,6 +1289,10 @@ "label": "useHotkeySequence", "to": "framework/preact/examples/useHotkeySequence" }, + { + "label": "useHotkeySequences", + "to": "framework/preact/examples/useHotkeySequences" + }, { "label": "useHotkeyRecorder", "to": "framework/preact/examples/useHotkeyRecorder" @@ -1266,6 +1326,10 @@ "label": "createHotkeySequence", "to": "framework/solid/examples/createHotkeySequence" }, + { + "label": "createHotkeySequences", + "to": "framework/solid/examples/createHotkeySequences" + }, { "label": "createHotkeyRecorder", "to": "framework/solid/examples/createHotkeyRecorder" @@ -1299,6 +1363,10 @@ "label": "injectHotkeySequence", "to": "framework/angular/examples/injectHotkeySequence" }, + { + "label": "injectHotkeySequences", + "to": "framework/angular/examples/injectHotkeySequences" + }, { "label": "injectHotkeyRecorder", "to": "framework/angular/examples/injectHotkeyRecorder" @@ -1332,6 +1400,10 @@ "label": "useHotkeySequence", "to": "framework/vue/examples/useHotkeySequence" }, + { + "label": "useHotkeySequences", + "to": "framework/vue/examples/useHotkeySequences" + }, { "label": "useHotkeyRecorder", "to": "framework/vue/examples/useHotkeyRecorder" @@ -1365,6 +1437,10 @@ "label": "createHotkeySequence", "to": "framework/svelte/examples/create-hotkey-sequence" }, + { + "label": "createHotkeySequences", + "to": "framework/svelte/examples/create-hotkey-sequences" + }, { "label": "createHotkeyRecorder", "to": "framework/svelte/examples/create-hotkey-recorder" diff --git a/docs/framework/angular/guides/sequences.md b/docs/framework/angular/guides/sequences.md index 69e15466..6ea60391 100644 --- a/docs/framework/angular/guides/sequences.md +++ b/docs/framework/angular/guides/sequences.md @@ -21,6 +21,34 @@ export class AppComponent { } ``` +## Many sequences at once + +Use `injectHotkeySequences` when you want several sequences (or a list built from data) in one injection context, instead of many `injectHotkeySequence` calls. + +```ts +import { injectHotkeySequences } from '@tanstack/angular-hotkeys' + +@Component({ standalone: true, template: `` }) +export class AppComponent { + constructor() { + injectHotkeySequences([ + { + sequence: ['G', 'G'], + callback: () => + window.scrollTo({ top: 0, behavior: 'smooth' }), + }, + { + sequence: ['D', 'D'], + callback: () => console.log('delete line'), + options: { timeout: 500 }, + }, + ]) + } +} +``` + +Options merge like `injectHotkeys`: `provideHotkeys` defaults, then `commonOptions`, then each definition’s `options`. + ## Sequence Options ```ts diff --git a/docs/framework/angular/reference/functions/injectHotkeySequences.md b/docs/framework/angular/reference/functions/injectHotkeySequences.md new file mode 100644 index 00000000..aa689927 --- /dev/null +++ b/docs/framework/angular/reference/functions/injectHotkeySequences.md @@ -0,0 +1,54 @@ +--- +id: injectHotkeySequences +title: injectHotkeySequences +--- + +# Function: injectHotkeySequences() + +```ts +function injectHotkeySequences(sequences, commonOptions): void; +``` + +Defined in: injectHotkeySequences.ts:50 + +Angular inject-based API for registering multiple keyboard shortcut sequences at once (Vim-style). + +Uses the singleton SequenceManager. Call in an injection context (e.g. constructor). +Uses `effect()` to track reactive dependencies when definitions or options are getters. + +Options are merged in this order: +provideHotkeys defaults < commonOptions < per-definition options + +Definitions with an empty `sequence` or `enabled: false` after merge are skipped. + +## Parameters + +### sequences + +Array of sequence definitions, or getter returning them + +[`InjectHotkeySequenceDefinition`](../interfaces/InjectHotkeySequenceDefinition.md)[] | () => [`InjectHotkeySequenceDefinition`](../interfaces/InjectHotkeySequenceDefinition.md)[] + +### commonOptions + +Shared options for all sequences, or getter + +[`InjectHotkeySequenceOptions`](../interfaces/InjectHotkeySequenceOptions.md) | () => [`InjectHotkeySequenceOptions`](../interfaces/InjectHotkeySequenceOptions.md) + +## Returns + +`void` + +## Example + +```ts +@Component({ ... }) +export class VimComponent { + constructor() { + injectHotkeySequences([ + { sequence: ['G', 'G'], callback: () => this.goTop() }, + { sequence: ['D', 'D'], callback: () => this.deleteLine() }, + ]) + } +} +``` diff --git a/docs/framework/angular/reference/index.md b/docs/framework/angular/reference/index.md index f3102fed..c82e4f1d 100644 --- a/docs/framework/angular/reference/index.md +++ b/docs/framework/angular/reference/index.md @@ -13,6 +13,7 @@ title: "@tanstack/angular-hotkeys" - [HotkeysProviderOptions](interfaces/HotkeysProviderOptions.md) - [InjectHotkeyDefinition](interfaces/InjectHotkeyDefinition.md) - [InjectHotkeyOptions](interfaces/InjectHotkeyOptions.md) +- [InjectHotkeySequenceDefinition](interfaces/InjectHotkeySequenceDefinition.md) - [InjectHotkeySequenceOptions](interfaces/InjectHotkeySequenceOptions.md) ## Variables @@ -30,5 +31,6 @@ title: "@tanstack/angular-hotkeys" - [injectHotkeysContext](functions/injectHotkeysContext.md) - [injectHotkeySequence](functions/injectHotkeySequence.md) - [injectHotkeySequenceRecorder](functions/injectHotkeySequenceRecorder.md) +- [injectHotkeySequences](functions/injectHotkeySequences.md) - [injectKeyHold](functions/injectKeyHold.md) - [provideHotkeys](functions/provideHotkeys.md) diff --git a/docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md b/docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md new file mode 100644 index 00000000..153345b5 --- /dev/null +++ b/docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md @@ -0,0 +1,48 @@ +--- +id: InjectHotkeySequenceDefinition +title: InjectHotkeySequenceDefinition +--- + +# Interface: InjectHotkeySequenceDefinition + +Defined in: injectHotkeySequences.ts:14 + +A single sequence definition for use with `injectHotkeySequences`. + +## Properties + +### callback + +```ts +callback: HotkeyCallback; +``` + +Defined in: injectHotkeySequences.ts:18 + +The function to call when the sequence is completed + +*** + +### options? + +```ts +optional options: + | InjectHotkeySequenceOptions + | () => InjectHotkeySequenceOptions; +``` + +Defined in: injectHotkeySequences.ts:20 + +Per-sequence options (merged on top of commonOptions) + +*** + +### sequence + +```ts +sequence: HotkeySequence | () => HotkeySequence; +``` + +Defined in: injectHotkeySequences.ts:16 + +Array of hotkey strings that form the sequence diff --git a/docs/framework/preact/guides/sequences.md b/docs/framework/preact/guides/sequences.md index 0201ee9f..9b6239e4 100644 --- a/docs/framework/preact/guides/sequences.md +++ b/docs/framework/preact/guides/sequences.md @@ -22,6 +22,21 @@ function App() { The first argument is an array of `Hotkey` strings representing each step in the sequence. The user must press them in order within the timeout window. +## Many sequences at once + +When you need several sequences—or a **dynamic** list whose length is not fixed at compile time—use `useHotkeySequences` instead of calling `useHotkeySequence` many times. One hook call keeps you within the rules of hooks while still registering every sequence. + +```tsx +import { useHotkeySequences } from '@tanstack/preact-hotkeys' + +useHotkeySequences([ + { sequence: ['G', 'G'], callback: () => scrollToTop() }, + { sequence: ['D', 'D'], callback: () => deleteLine(), options: { timeout: 500 } }, +]) +``` + +Options merge in the same order as `useHotkeys`: `HotkeysProvider` defaults, then the second-argument `commonOptions`, then each definition’s `options`. + ## Sequence Options The third argument is an options object: diff --git a/docs/framework/preact/reference/functions/useHotkeySequences.md b/docs/framework/preact/reference/functions/useHotkeySequences.md new file mode 100644 index 00000000..6868f74e --- /dev/null +++ b/docs/framework/preact/reference/functions/useHotkeySequences.md @@ -0,0 +1,67 @@ +--- +id: useHotkeySequences +title: useHotkeySequences +--- + +# Function: useHotkeySequences() + +```ts +function useHotkeySequences(definitions, commonOptions): void; +``` + +Defined in: useHotkeySequences.ts:65 + +Preact hook for registering multiple keyboard shortcut sequences at once (Vim-style). + +Uses the singleton SequenceManager. Accepts a dynamic array of definitions so you can +register variable-length lists without violating the rules of hooks. + +Options are merged in this order: +HotkeysProvider defaults < commonOptions < per-definition options + +Callbacks and options are synced on every render to avoid stale closures. + +Definitions with an empty `sequence` are skipped (no registration). + +## Parameters + +### definitions + +[`UseHotkeySequenceDefinition`](../interfaces/UseHotkeySequenceDefinition.md)[] + +Array of sequence definitions to register + +### commonOptions + +[`UseHotkeySequenceOptions`](../interfaces/UseHotkeySequenceOptions.md) = `{}` + +Shared options applied to all sequences (overridden by per-definition options) + +## Returns + +`void` + +## Examples + +```tsx +function VimPalette() { + useHotkeySequences([ + { sequence: ['G', 'G'], callback: () => scrollToTop() }, + { sequence: ['D', 'D'], callback: () => deleteLine() }, + { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord(), options: { timeout: 500 } }, + ]) +} +``` + +```tsx +function DynamicSequences({ items }) { + useHotkeySequences( + items.map((item) => ({ + sequence: item.chords, + callback: item.action, + options: { enabled: item.enabled }, + })), + { preventDefault: true }, + ) +} +``` diff --git a/docs/framework/preact/reference/index.md b/docs/framework/preact/reference/index.md index b2d72b35..b899a692 100644 --- a/docs/framework/preact/reference/index.md +++ b/docs/framework/preact/reference/index.md @@ -13,6 +13,7 @@ title: "@tanstack/preact-hotkeys" - [PreactHotkeySequenceRecorder](interfaces/PreactHotkeySequenceRecorder.md) - [UseHotkeyDefinition](interfaces/UseHotkeyDefinition.md) - [UseHotkeyOptions](interfaces/UseHotkeyOptions.md) +- [UseHotkeySequenceDefinition](interfaces/UseHotkeySequenceDefinition.md) - [UseHotkeySequenceOptions](interfaces/UseHotkeySequenceOptions.md) ## Functions @@ -27,4 +28,5 @@ title: "@tanstack/preact-hotkeys" - [useHotkeysContext](functions/useHotkeysContext.md) - [useHotkeySequence](functions/useHotkeySequence.md) - [useHotkeySequenceRecorder](functions/useHotkeySequenceRecorder.md) +- [useHotkeySequences](functions/useHotkeySequences.md) - [useKeyHold](functions/useKeyHold.md) diff --git a/docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md b/docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md new file mode 100644 index 00000000..59536414 --- /dev/null +++ b/docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md @@ -0,0 +1,46 @@ +--- +id: UseHotkeySequenceDefinition +title: UseHotkeySequenceDefinition +--- + +# Interface: UseHotkeySequenceDefinition + +Defined in: useHotkeySequences.ts:15 + +A single sequence definition for use with `useHotkeySequences`. + +## Properties + +### callback + +```ts +callback: HotkeyCallback; +``` + +Defined in: useHotkeySequences.ts:19 + +The function to call when the sequence is completed + +*** + +### options? + +```ts +optional options: UseHotkeySequenceOptions; +``` + +Defined in: useHotkeySequences.ts:21 + +Per-sequence options (merged on top of commonOptions) + +*** + +### sequence + +```ts +sequence: HotkeySequence; +``` + +Defined in: useHotkeySequences.ts:17 + +Array of hotkey strings that form the sequence diff --git a/docs/framework/react/guides/sequences.md b/docs/framework/react/guides/sequences.md index 4f27b12b..530cd65e 100644 --- a/docs/framework/react/guides/sequences.md +++ b/docs/framework/react/guides/sequences.md @@ -22,6 +22,21 @@ function App() { The first argument is an array of `Hotkey` strings representing each step in the sequence. The user must press them in order within the timeout window. +## Many sequences at once + +When you need several sequences—or a **dynamic** list whose length is not fixed at compile time—use `useHotkeySequences` instead of calling `useHotkeySequence` many times. One hook call keeps you within the rules of hooks while still registering every sequence. + +```tsx +import { useHotkeySequences } from '@tanstack/react-hotkeys' + +useHotkeySequences([ + { sequence: ['G', 'G'], callback: () => scrollToTop() }, + { sequence: ['D', 'D'], callback: () => deleteLine(), options: { timeout: 500 } }, +]) +``` + +Options merge in the same order as `useHotkeys`: `HotkeysProvider` defaults, then the second-argument `commonOptions`, then each definition’s `options`. + ## Sequence Options The third argument is an options object: diff --git a/docs/framework/react/reference/functions/useHotkeySequences.md b/docs/framework/react/reference/functions/useHotkeySequences.md new file mode 100644 index 00000000..61fa0575 --- /dev/null +++ b/docs/framework/react/reference/functions/useHotkeySequences.md @@ -0,0 +1,67 @@ +--- +id: useHotkeySequences +title: useHotkeySequences +--- + +# Function: useHotkeySequences() + +```ts +function useHotkeySequences(definitions, commonOptions): void; +``` + +Defined in: useHotkeySequences.ts:65 + +React hook for registering multiple keyboard shortcut sequences at once (Vim-style). + +Uses the singleton SequenceManager. Accepts a dynamic array of definitions so you can +register variable-length lists without violating the rules of hooks. + +Options are merged in this order: +HotkeysProvider defaults < commonOptions < per-definition options + +Callbacks and options are synced on every render to avoid stale closures. + +Definitions with an empty `sequence` are skipped (no registration). + +## Parameters + +### definitions + +[`UseHotkeySequenceDefinition`](../interfaces/UseHotkeySequenceDefinition.md)[] + +Array of sequence definitions to register + +### commonOptions + +[`UseHotkeySequenceOptions`](../interfaces/UseHotkeySequenceOptions.md) = `{}` + +Shared options applied to all sequences (overridden by per-definition options) + +## Returns + +`void` + +## Examples + +```tsx +function VimPalette() { + useHotkeySequences([ + { sequence: ['G', 'G'], callback: () => scrollToTop() }, + { sequence: ['D', 'D'], callback: () => deleteLine() }, + { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord(), options: { timeout: 500 } }, + ]) +} +``` + +```tsx +function DynamicSequences({ items }) { + useHotkeySequences( + items.map((item) => ({ + sequence: item.chords, + callback: item.action, + options: { enabled: item.enabled }, + })), + { preventDefault: true }, + ) +} +``` diff --git a/docs/framework/react/reference/index.md b/docs/framework/react/reference/index.md index f91c21e0..d694c729 100644 --- a/docs/framework/react/reference/index.md +++ b/docs/framework/react/reference/index.md @@ -13,6 +13,7 @@ title: "@tanstack/react-hotkeys" - [ReactHotkeySequenceRecorder](interfaces/ReactHotkeySequenceRecorder.md) - [UseHotkeyDefinition](interfaces/UseHotkeyDefinition.md) - [UseHotkeyOptions](interfaces/UseHotkeyOptions.md) +- [UseHotkeySequenceDefinition](interfaces/UseHotkeySequenceDefinition.md) - [UseHotkeySequenceOptions](interfaces/UseHotkeySequenceOptions.md) ## Functions @@ -27,4 +28,5 @@ title: "@tanstack/react-hotkeys" - [useHotkeysContext](functions/useHotkeysContext.md) - [useHotkeySequence](functions/useHotkeySequence.md) - [useHotkeySequenceRecorder](functions/useHotkeySequenceRecorder.md) +- [useHotkeySequences](functions/useHotkeySequences.md) - [useKeyHold](functions/useKeyHold.md) diff --git a/docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md b/docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md new file mode 100644 index 00000000..59536414 --- /dev/null +++ b/docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md @@ -0,0 +1,46 @@ +--- +id: UseHotkeySequenceDefinition +title: UseHotkeySequenceDefinition +--- + +# Interface: UseHotkeySequenceDefinition + +Defined in: useHotkeySequences.ts:15 + +A single sequence definition for use with `useHotkeySequences`. + +## Properties + +### callback + +```ts +callback: HotkeyCallback; +``` + +Defined in: useHotkeySequences.ts:19 + +The function to call when the sequence is completed + +*** + +### options? + +```ts +optional options: UseHotkeySequenceOptions; +``` + +Defined in: useHotkeySequences.ts:21 + +Per-sequence options (merged on top of commonOptions) + +*** + +### sequence + +```ts +sequence: HotkeySequence; +``` + +Defined in: useHotkeySequences.ts:17 + +Array of hotkey strings that form the sequence diff --git a/docs/framework/solid/guides/sequences.md b/docs/framework/solid/guides/sequences.md index da383da8..db693cd5 100644 --- a/docs/framework/solid/guides/sequences.md +++ b/docs/framework/solid/guides/sequences.md @@ -22,6 +22,21 @@ function App() { The first argument is an array of `Hotkey` strings representing each step in the sequence. The user must press them in order within the timeout window. +## Many sequences at once + +For several sequences or a **dynamic** list, use `createHotkeySequences` instead of many `createHotkeySequence` calls. Pass a plain array or an accessor that returns definitions. + +```tsx +import { createHotkeySequences } from '@tanstack/solid-hotkeys' + +createHotkeySequences([ + { sequence: ['G', 'G'], callback: () => scrollToTop() }, + { sequence: ['D', 'D'], callback: () => deleteLine(), options: { timeout: 500 } }, +]) +``` + +Options merge like `createHotkeys`: `HotkeysProvider` defaults, then `commonOptions`, then each definition’s `options`. For element-scoped multi-sequence registration, use `createHotkeySequencesAttachment`. + ## Reactive Options Solid's `createHotkeySequence` accepts **accessor functions** for reactive sequence and options: diff --git a/docs/framework/solid/reference/functions/createHotkeySequences.md b/docs/framework/solid/reference/functions/createHotkeySequences.md new file mode 100644 index 00000000..b69fd8e3 --- /dev/null +++ b/docs/framework/solid/reference/functions/createHotkeySequences.md @@ -0,0 +1,64 @@ +--- +id: createHotkeySequences +title: createHotkeySequences +--- + +# Function: createHotkeySequences() + +```ts +function createHotkeySequences(sequences, commonOptions): void; +``` + +Defined in: createHotkeySequences.ts:61 + +SolidJS primitive for registering multiple keyboard shortcut sequences at once (Vim-style). + +Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or accessors, +so you can react to variable-length lists. + +Options are merged in this order: +HotkeysProvider defaults < commonOptions < per-definition options + +Definitions with an empty `sequence` are skipped (no registration). + +## Parameters + +### sequences + +Array of sequence definitions, or accessor returning them + +[`CreateHotkeySequenceDefinition`](../interfaces/CreateHotkeySequenceDefinition.md)[] | () => [`CreateHotkeySequenceDefinition`](../interfaces/CreateHotkeySequenceDefinition.md)[] + +### commonOptions + +Shared options for all sequences, or accessor + +[`CreateHotkeySequenceOptions`](../interfaces/CreateHotkeySequenceOptions.md) | () => [`CreateHotkeySequenceOptions`](../interfaces/CreateHotkeySequenceOptions.md) + +## Returns + +`void` + +## Examples + +```tsx +function VimPalette() { + createHotkeySequences([ + { sequence: ['G', 'G'], callback: () => scrollToTop() }, + { sequence: ['D', 'D'], callback: () => deleteLine() }, + ]) +} +``` + +```tsx +function Dynamic(props) { + createHotkeySequences( + () => props.items.map((item) => ({ + sequence: item.chords, + callback: item.action, + options: { enabled: item.enabled }, + })), + { preventDefault: true }, + ) +} +``` diff --git a/docs/framework/solid/reference/index.md b/docs/framework/solid/reference/index.md index 866d5a1a..425d2f0a 100644 --- a/docs/framework/solid/reference/index.md +++ b/docs/framework/solid/reference/index.md @@ -9,6 +9,7 @@ title: "@tanstack/solid-hotkeys" - [CreateHotkeyDefinition](interfaces/CreateHotkeyDefinition.md) - [CreateHotkeyOptions](interfaces/CreateHotkeyOptions.md) +- [CreateHotkeySequenceDefinition](interfaces/CreateHotkeySequenceDefinition.md) - [CreateHotkeySequenceOptions](interfaces/CreateHotkeySequenceOptions.md) - [HotkeysProviderOptions](interfaces/HotkeysProviderOptions.md) - [HotkeysProviderProps](interfaces/HotkeysProviderProps.md) @@ -28,6 +29,7 @@ title: "@tanstack/solid-hotkeys" - [createHotkeys](functions/createHotkeys.md) - [createHotkeySequence](functions/createHotkeySequence.md) - [createHotkeySequenceRecorder](functions/createHotkeySequenceRecorder.md) +- [createHotkeySequences](functions/createHotkeySequences.md) - [createKeyHold](functions/createKeyHold.md) - [useDefaultHotkeysOptions](functions/useDefaultHotkeysOptions.md) - [useHotkeysContext](functions/useHotkeysContext.md) diff --git a/docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md b/docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md new file mode 100644 index 00000000..d0d2c8ab --- /dev/null +++ b/docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md @@ -0,0 +1,46 @@ +--- +id: CreateHotkeySequenceDefinition +title: CreateHotkeySequenceDefinition +--- + +# Interface: CreateHotkeySequenceDefinition + +Defined in: createHotkeySequences.ts:14 + +A single sequence definition for use with `createHotkeySequences`. + +## Properties + +### callback + +```ts +callback: HotkeyCallback; +``` + +Defined in: createHotkeySequences.ts:18 + +The function to call when the sequence is completed + +*** + +### options? + +```ts +optional options: CreateHotkeySequenceOptions; +``` + +Defined in: createHotkeySequences.ts:20 + +Per-sequence options (merged on top of commonOptions) + +*** + +### sequence + +```ts +sequence: HotkeySequence; +``` + +Defined in: createHotkeySequences.ts:16 + +Array of hotkey strings that form the sequence diff --git a/docs/framework/svelte/guides/sequences.md b/docs/framework/svelte/guides/sequences.md index 5b7b3f99..6ca89a45 100644 --- a/docs/framework/svelte/guides/sequences.md +++ b/docs/framework/svelte/guides/sequences.md @@ -17,6 +17,21 @@ TanStack Hotkeys supports multi-key sequences in Svelte, where keys are pressed ``` +## Many sequences at once + +Use `createHotkeySequences` to register several global sequences in one place (including from a reactive getter). For multiple sequences on a focused element, use `createHotkeySequencesAttachment` the same way you would use `createHotkeySequenceAttachment`. + +```svelte + +``` + ## Scoped sequences Use `createHotkeySequenceAttachment` when a sequence should only be active while a specific element owns focus. diff --git a/docs/framework/svelte/reference/functions/createHotkeySequences.md b/docs/framework/svelte/reference/functions/createHotkeySequences.md new file mode 100644 index 00000000..1b03c3d5 --- /dev/null +++ b/docs/framework/svelte/reference/functions/createHotkeySequences.md @@ -0,0 +1,41 @@ +--- +id: createHotkeySequences +title: createHotkeySequences +--- + +# Function: createHotkeySequences() + +```ts +function createHotkeySequences(definitions, commonOptions): void; +``` + +Defined in: packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:60 + +Register multiple global keyboard shortcut sequences for the current component. + +## Parameters + +### definitions + +`MaybeGetter`\<[`CreateHotkeySequenceDefinition`](../interfaces/CreateHotkeySequenceDefinition.md)[]\> + +### commonOptions + +`MaybeGetter`\<[`CreateHotkeySequenceOptions`](../interfaces/CreateHotkeySequenceOptions.md)\> = `{}` + +## Returns + +`void` + +## Example + +```svelte + +``` diff --git a/docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md b/docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md new file mode 100644 index 00000000..7954a1c7 --- /dev/null +++ b/docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md @@ -0,0 +1,43 @@ +--- +id: createHotkeySequencesAttachment +title: createHotkeySequencesAttachment +--- + +# Function: createHotkeySequencesAttachment() + +```ts +function createHotkeySequencesAttachment(definitions, commonOptions): Attachment; +``` + +Defined in: packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:184 + +Create an attachment for element-scoped multi-sequence registration. + +## Parameters + +### definitions + +`MaybeGetter`\<[`CreateHotkeySequenceDefinition`](../interfaces/CreateHotkeySequenceDefinition.md)[]\> + +### commonOptions + +`MaybeGetter`\<[`CreateHotkeySequenceOptions`](../interfaces/CreateHotkeySequenceOptions.md)\> = `{}` + +## Returns + +`Attachment`\<`HTMLElement`\> + +## Example + +```svelte + + +
Editor
+``` diff --git a/docs/framework/svelte/reference/index.md b/docs/framework/svelte/reference/index.md index 2905c01c..0d06d731 100644 --- a/docs/framework/svelte/reference/index.md +++ b/docs/framework/svelte/reference/index.md @@ -9,6 +9,7 @@ title: "@tanstack/svelte-hotkeys" - [CreateHotkeyDefinition](interfaces/CreateHotkeyDefinition.md) - [CreateHotkeyOptions](interfaces/CreateHotkeyOptions.md) +- [CreateHotkeySequenceDefinition](interfaces/CreateHotkeySequenceDefinition.md) - [CreateHotkeySequenceOptions](interfaces/CreateHotkeySequenceOptions.md) - [HotkeysProviderOptions](interfaces/HotkeysProviderOptions.md) - [HotkeysProviderProps](interfaces/HotkeysProviderProps.md) @@ -37,6 +38,8 @@ title: "@tanstack/svelte-hotkeys" - [createHotkeySequence](functions/createHotkeySequence.md) - [createHotkeySequenceAttachment](functions/createHotkeySequenceAttachment.md) - [createHotkeySequenceRecorder](functions/createHotkeySequenceRecorder.md) +- [createHotkeySequences](functions/createHotkeySequences.md) +- [createHotkeySequencesAttachment](functions/createHotkeySequencesAttachment.md) - [getDefaultHotkeysOptions](functions/getDefaultHotkeysOptions.md) - [getHeldKeyCodesMap](functions/getHeldKeyCodesMap.md) - [getHeldKeys](functions/getHeldKeys.md) diff --git a/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md b/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md new file mode 100644 index 00000000..983683e7 --- /dev/null +++ b/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md @@ -0,0 +1,46 @@ +--- +id: CreateHotkeySequenceDefinition +title: CreateHotkeySequenceDefinition +--- + +# Interface: CreateHotkeySequenceDefinition + +Defined in: packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:18 + +A single sequence definition for use with `createHotkeySequences`. + +## Properties + +### callback + +```ts +callback: HotkeyCallback; +``` + +Defined in: packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:22 + +The function to call when the sequence is completed + +*** + +### options? + +```ts +optional options: MaybeGetter; +``` + +Defined in: packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:24 + +Per-sequence options (merged on top of commonOptions) + +*** + +### sequence + +```ts +sequence: MaybeGetter; +``` + +Defined in: packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:20 + +Array of hotkey strings that form the sequence diff --git a/docs/framework/vue/guides/sequences.md b/docs/framework/vue/guides/sequences.md index 7c9c7cbc..488cbe5b 100644 --- a/docs/framework/vue/guides/sequences.md +++ b/docs/framework/vue/guides/sequences.md @@ -17,6 +17,23 @@ useHotkeySequence(['G', 'G'], () => { ``` +## Many sequences at once + +When you need several sequences—or a **reactive** list whose length changes—use `useHotkeySequences` instead of many `useHotkeySequence` calls. One composable registers every sequence safely. + +```vue + +``` + +Options merge like `useHotkeys`: `HotkeysProvider` defaults, then `commonOptions`, then each definition’s `options`. + ## Sequence Options ```ts diff --git a/docs/framework/vue/quick-start.md b/docs/framework/vue/quick-start.md index da2e9927..116c168b 100644 --- a/docs/framework/vue/quick-start.md +++ b/docs/framework/vue/quick-start.md @@ -88,6 +88,8 @@ useHotkeySequence(['G', 'Shift+G'], () => scrollToBottom()) ``` +For several sequences or a list that changes at runtime, prefer a single `useHotkeySequences([...])` call (see the [Sequences guide](./guides/sequences.md#many-sequences-at-once)). + ### Tracking Held Keys ```vue diff --git a/docs/framework/vue/reference/functions/useHotkeySequences.md b/docs/framework/vue/reference/functions/useHotkeySequences.md new file mode 100644 index 00000000..ff155891 --- /dev/null +++ b/docs/framework/vue/reference/functions/useHotkeySequences.md @@ -0,0 +1,70 @@ +--- +id: useHotkeySequences +title: useHotkeySequences +--- + +# Function: useHotkeySequences() + +```ts +function useHotkeySequences(definitions, commonOptions): void; +``` + +Defined in: packages/vue-hotkeys/src/useHotkeySequences.ts:68 + +Vue composable for registering multiple keyboard shortcut sequences at once (Vim-style). + +Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or a getter/ref +that returns one, so you can register variable-length lists safely. + +Options are merged in this order: +HotkeysProvider defaults < commonOptions < per-definition options + +Definitions with an empty `sequence` are skipped (no registration). + +## Parameters + +### definitions + +`MaybeRefOrGetter`\<[`UseHotkeySequenceDefinition`](../interfaces/UseHotkeySequenceDefinition.md)[]\> + +Array of sequence definitions, or a getter/ref + +### commonOptions + +`MaybeRefOrGetter`\<[`UseHotkeySequenceOptions`](../interfaces/UseHotkeySequenceOptions.md)\> = `{}` + +Shared options applied to all sequences (overridden by per-definition options) + +## Returns + +`void` + +## Examples + +```vue + +``` + +```vue + +``` diff --git a/docs/framework/vue/reference/index.md b/docs/framework/vue/reference/index.md index 7a4c7e9f..06fce76d 100644 --- a/docs/framework/vue/reference/index.md +++ b/docs/framework/vue/reference/index.md @@ -10,6 +10,7 @@ title: "@tanstack/vue-hotkeys" - [HotkeysProviderOptions](interfaces/HotkeysProviderOptions.md) - [UseHotkeyDefinition](interfaces/UseHotkeyDefinition.md) - [UseHotkeyOptions](interfaces/UseHotkeyOptions.md) +- [UseHotkeySequenceDefinition](interfaces/UseHotkeySequenceDefinition.md) - [UseHotkeySequenceOptions](interfaces/UseHotkeySequenceOptions.md) - [VueHotkeyRecorder](interfaces/VueHotkeyRecorder.md) - [VueHotkeySequenceRecorder](interfaces/VueHotkeySequenceRecorder.md) @@ -30,4 +31,5 @@ title: "@tanstack/vue-hotkeys" - [useHotkeysContext](functions/useHotkeysContext.md) - [useHotkeySequence](functions/useHotkeySequence.md) - [useHotkeySequenceRecorder](functions/useHotkeySequenceRecorder.md) +- [useHotkeySequences](functions/useHotkeySequences.md) - [useKeyHold](functions/useKeyHold.md) diff --git a/docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md b/docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md new file mode 100644 index 00000000..26aa302d --- /dev/null +++ b/docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md @@ -0,0 +1,46 @@ +--- +id: UseHotkeySequenceDefinition +title: UseHotkeySequenceDefinition +--- + +# Interface: UseHotkeySequenceDefinition + +Defined in: packages/vue-hotkeys/src/useHotkeySequences.ts:15 + +A single sequence definition for use with `useHotkeySequences`. + +## Properties + +### callback + +```ts +callback: HotkeyCallback; +``` + +Defined in: packages/vue-hotkeys/src/useHotkeySequences.ts:19 + +The function to call when the sequence is completed + +*** + +### options? + +```ts +optional options: MaybeRefOrGetter; +``` + +Defined in: packages/vue-hotkeys/src/useHotkeySequences.ts:21 + +Per-sequence options (merged on top of commonOptions) + +*** + +### sequence + +```ts +sequence: MaybeRefOrGetter; +``` + +Defined in: packages/vue-hotkeys/src/useHotkeySequences.ts:17 + +Array of hotkey strings that form the sequence diff --git a/examples/angular/injectHotkeySequences/angular.json b/examples/angular/injectHotkeySequences/angular.json new file mode 100644 index 00000000..441a7c9c --- /dev/null +++ b/examples/angular/injectHotkeySequences/angular.json @@ -0,0 +1,63 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "injectHotkeySequences": { + "projectType": "application", + "schematics": { + "@schematics/angular:class": { "skipTests": true }, + "@schematics/angular:component": { "skipTests": true } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/inject-hotkey-sequences", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.json", + "assets": [], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "injectHotkeySequences:build:production" + }, + "development": { + "buildTarget": "injectHotkeySequences:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + }, + "cli": { "analytics": false } +} diff --git a/examples/angular/injectHotkeySequences/package.json b/examples/angular/injectHotkeySequences/package.json new file mode 100644 index 00000000..b44eed2e --- /dev/null +++ b/examples/angular/injectHotkeySequences/package.json @@ -0,0 +1,38 @@ +{ + "name": "inject-hotkey-sequences", + "scripts": { + "ng": "ng", + "start": "ng serve --port=3069", + "dev": "ng serve --port=3069", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/common": "^21.2.5", + "@angular/compiler": "^21.2.5", + "@angular/core": "^21.2.5", + "@angular/forms": "^21.2.5", + "@angular/platform-browser": "^21.2.5", + "@angular/platform-browser-dynamic": "^21.2.5", + "@angular/router": "^21.2.5", + "@tanstack/angular-hotkeys": "^0.6.0", + "rxjs": "~7.8.2", + "tslib": "^2.8.1", + "zone.js": "~0.16.1" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^21.2.3", + "@angular/cli": "^21.2.3", + "@angular/compiler-cli": "^21.2.5", + "@types/jasmine": "~6.0.0", + "jasmine-core": "~6.1.0", + "karma": "~6.4.4", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.1", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.2.0", + "typescript": "5.9.3" + } +} diff --git a/examples/angular/injectHotkeySequences/src/app/app.component.css b/examples/angular/injectHotkeySequences/src/app/app.component.css new file mode 100644 index 00000000..5d4e87f3 --- /dev/null +++ b/examples/angular/injectHotkeySequences/src/app/app.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/examples/angular/injectHotkeySequences/src/app/app.component.html b/examples/angular/injectHotkeySequences/src/app/app.component.html new file mode 100644 index 00000000..2fc04845 --- /dev/null +++ b/examples/angular/injectHotkeySequences/src/app/app.component.html @@ -0,0 +1,140 @@ +
+
+

injectHotkeySequences

+

+ Register many multi-key sequences in one call (like Vim commands). Keys + must be pressed within the timeout window (default: 1000ms). +

+
+ +
+
+

Vim-Style Commands

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SequenceAction
g gGo to top
G (Shift+G)Go to bottom
d dDelete line
y yYank (copy) line
d wDelete word
c i wChange inner word
+
+ +
+

Fun Sequences

+
+
+

Konami Code (Partial)

+

+ Use arrow keys within 1.5 seconds +
+
+

Side to Side

+

+ Arrow keys within 1.5 seconds +
+
+

Spell It Out

+

+ h e l l o +

+ Type "hello" quickly +
+
+
+ + @if (lastSequence(); as seq) { +
Triggered: {{ seq }}
+ } + +
+

Chained Shift+letter sequences

+

+ Each step is a chord: hold Shift and press a letter. You can + press Shift alone between steps—those modifier-only presses + do not reset progress, so the next chord still counts. +

+ + + + + + + + + + + + + +
SequenceAction
+ Shift+r then Shift+t + Chained Shift+letter (2 steps)
+
+ +
+

Usage

+
+import {{ '{' }} injectHotkeySequences {{
+          '}'
+        }} from '@tanstack/angular-hotkeys'
+
+// In constructor or injection context:
+injectHotkeySequences([
+  {{ '{' }} sequence: ['G', 'G'], callback: () => scrollToTop() {{ '}' }},
+  {{ '{' }}
+    sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+    callback: () => activateCheatMode(),
+    options: {{ '{' }} timeout: 1500 {{ '}' }},
+  {{ '}' }},
+  {{ '{' }} sequence: ['C', 'I', 'W'], callback: () => changeInnerWord() {{
+          '}'
+        }},
+  {{ '{' }} sequence: ['Shift+R', 'Shift+T'], callback: () => doSomething() {{
+          '}'
+        }},
+])
+
+ + @if (history().length > 0) { +
+

History

+
    + @for (item of history(); track item) { +
  • {{ item }}
  • + } +
+ +
+ } + +

Press Escape to clear history

+
+
diff --git a/examples/angular/injectHotkeySequences/src/app/app.component.ts b/examples/angular/injectHotkeySequences/src/app/app.component.ts new file mode 100644 index 00000000..3454cc28 --- /dev/null +++ b/examples/angular/injectHotkeySequences/src/app/app.component.ts @@ -0,0 +1,74 @@ +import { Component, signal } from '@angular/core' +import { injectHotkey, injectHotkeySequences } from '@tanstack/angular-hotkeys' + +@Component({ + selector: 'app-root', + standalone: true, + templateUrl: './app.component.html', + styleUrl: './app.component.css', +}) +export class AppComponent { + lastSequence = signal(null) + history = signal([]) + + constructor() { + const addToHistory = (action: string) => { + this.lastSequence.set(action) + this.history.update((h) => [...h.slice(-9), action]) + } + + injectHotkeySequences([ + { + sequence: ['G', 'G'], + callback: () => addToHistory('gg → Go to top'), + }, + { + sequence: ['Shift+G'], + callback: () => addToHistory('G → Go to bottom'), + }, + { + sequence: ['D', 'D'], + callback: () => addToHistory('dd → Delete line'), + }, + { + sequence: ['Y', 'Y'], + callback: () => addToHistory('yy → Yank (copy) line'), + }, + { + sequence: ['D', 'W'], + callback: () => addToHistory('dw → Delete word'), + }, + { + sequence: ['C', 'I', 'W'], + callback: () => addToHistory('ciw → Change inner word'), + }, + { + sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'], + callback: () => addToHistory('↑↑↓↓ → Konami code (partial)'), + options: { timeout: 1500 }, + }, + { + sequence: ['ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight'], + callback: () => addToHistory('←→←→ → Side to side!'), + options: { timeout: 1500 }, + }, + { + sequence: ['H', 'E', 'L', 'L', 'O'], + callback: () => addToHistory('hello → Hello World!'), + }, + { + sequence: ['Shift+R', 'Shift+T'], + callback: () => addToHistory('⇧R ⇧T → Chained Shift+letter (2 steps)'), + }, + ]) + + injectHotkey('Escape', () => { + this.lastSequence.set(null) + this.history.set([]) + }) + } + + clearHistory(): void { + this.history.set([]) + } +} diff --git a/examples/angular/injectHotkeySequences/src/app/app.config.ts b/examples/angular/injectHotkeySequences/src/app/app.config.ts new file mode 100644 index 00000000..0a966a4a --- /dev/null +++ b/examples/angular/injectHotkeySequences/src/app/app.config.ts @@ -0,0 +1,6 @@ +import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core' +import { provideHotkeys } from '@tanstack/angular-hotkeys' + +export const appConfig: ApplicationConfig = { + providers: [provideZoneChangeDetection({ eventCoalescing: true })], +} diff --git a/examples/angular/injectHotkeySequences/src/index.html b/examples/angular/injectHotkeySequences/src/index.html new file mode 100644 index 00000000..9e8f41e4 --- /dev/null +++ b/examples/angular/injectHotkeySequences/src/index.html @@ -0,0 +1,13 @@ + + + + + + + injectHotkeySequences - TanStack Hotkeys Angular Example + + + + + + diff --git a/examples/angular/injectHotkeySequences/src/main.ts b/examples/angular/injectHotkeySequences/src/main.ts new file mode 100644 index 00000000..c3d8f9af --- /dev/null +++ b/examples/angular/injectHotkeySequences/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/injectHotkeySequences/src/styles.css b/examples/angular/injectHotkeySequences/src/styles.css new file mode 100644 index 00000000..3a0fd933 --- /dev/null +++ b/examples/angular/injectHotkeySequences/src/styles.css @@ -0,0 +1,117 @@ +* { + box-sizing: border-box; +} +body { + margin: 0; + font-family: + system-ui, + -apple-system, + sans-serif; + background: #f5f5f5; + color: #333; +} +.app { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} +header h1 { + margin: 0 0 10px; + color: #0066cc; +} +header p { + color: #666; + margin: 0; + max-width: 500px; + margin: 0 auto; +} +.demo-section { + background: white; + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} +.demo-section h2 { + margin: 0 0 16px; + font-size: 20px; +} +kbd { + background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%); + border: 1px solid #ccc; + border-radius: 4px; + padding: 2px 8px; + font-family: monospace; + font-size: 13px; + margin-right: 4px; +} +.sequence-table { + width: 100%; + border-collapse: collapse; +} +.sequence-table th, +.sequence-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #eee; +} +.fun-sequences { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; +} +.sequence-card { + background: #f8f9fa; + border-radius: 8px; + padding: 16px; + text-align: center; +} +.sequence-card h3 { + margin: 0 0 12px; + font-size: 16px; +} +.hint { + font-size: 12px; + color: #888; + font-style: italic; +} +.info-box { + background: #e3f2fd; + border-radius: 8px; + padding: 16px 20px; + margin-bottom: 24px; + font-size: 18px; +} +.info-box.success { + background: #e8f5e9; + color: #2e7d32; +} +.code-block { + background: #1e1e1e; + color: #d4d4d4; + padding: 16px; + border-radius: 8px; + overflow-x: auto; + font-size: 13px; +} +.history-list { + list-style: none; + padding: 0; + margin: 0 0 16px; +} +.history-list li { + padding: 10px 14px; + background: #f0f0f0; + border-radius: 6px; + margin-bottom: 6px; + font-family: monospace; + font-size: 14px; +} +button { + background: #0066cc; + color: white; + border: none; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; +} diff --git a/examples/angular/injectHotkeySequences/tsconfig.json b/examples/angular/injectHotkeySequences/tsconfig.json new file mode 100644 index 00000000..f12677ca --- /dev/null +++ b/examples/angular/injectHotkeySequences/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./out-tsc/app", + "lib": ["ES2022", "dom"], + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "types": [] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/preact/useHotkeySequences/eslint.config.js b/examples/preact/useHotkeySequences/eslint.config.js new file mode 100644 index 00000000..3fd4ac43 --- /dev/null +++ b/examples/preact/useHotkeySequences/eslint.config.js @@ -0,0 +1,11 @@ +// @ts-check + +import rootConfig from '../../../eslint.config.js' + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { + ignores: ['eslint.config.js'], + }, + ...rootConfig, +] diff --git a/examples/preact/useHotkeySequences/index.html b/examples/preact/useHotkeySequences/index.html new file mode 100644 index 00000000..aae5f349 --- /dev/null +++ b/examples/preact/useHotkeySequences/index.html @@ -0,0 +1,14 @@ + + + + + + + useHotkeySequences - TanStack Hotkeys Preact Example + + + +
+ + + diff --git a/examples/preact/useHotkeySequences/package.json b/examples/preact/useHotkeySequences/package.json new file mode 100644 index 00000000..2a5658ea --- /dev/null +++ b/examples/preact/useHotkeySequences/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/hotkeys-example-preact-use-hotkey-sequences", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3069", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/preact-hotkeys": "^0.6.0", + "preact": "^10.29.0" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.5", + "@tanstack/preact-devtools": "0.10.0", + "@tanstack/preact-hotkeys-devtools": "^0.5.0", + "typescript": "5.9.3", + "vite": "^8.0.1" + } +} diff --git a/examples/preact/useHotkeySequences/src/index.css b/examples/preact/useHotkeySequences/src/index.css new file mode 100644 index 00000000..0fe2ade8 --- /dev/null +++ b/examples/preact/useHotkeySequences/src/index.css @@ -0,0 +1,135 @@ +* { + box-sizing: border-box; +} +body { + margin: 0; + font-family: + system-ui, + -apple-system, + sans-serif; + background: #f5f5f5; + color: #333; +} +.app { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} +header { + text-align: center; + margin-bottom: 40px; +} +header h1 { + margin: 0 0 10px; + color: #0066cc; +} +header p { + color: #666; + margin: 0; + max-width: 500px; + margin: 0 auto; +} +.demo-section { + background: white; + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} +.demo-section h2 { + margin: 0 0 16px; + font-size: 20px; +} +kbd { + background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%); + border: 1px solid #ccc; + border-bottom-width: 2px; + border-radius: 4px; + padding: 2px 8px; + font-family: monospace; + font-size: 13px; + margin-right: 4px; +} +.sequence-table { + width: 100%; + border-collapse: collapse; +} +.sequence-table th, +.sequence-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #eee; +} +.sequence-table th { + font-weight: 600; + color: #666; + font-size: 14px; +} +.fun-sequences { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; +} +.sequence-card { + background: #f8f9fa; + border-radius: 8px; + padding: 16px; + text-align: center; +} +.sequence-card h3 { + margin: 0 0 12px; + font-size: 16px; +} +.sequence-card p { + margin: 0 0 8px; +} +.hint { + font-size: 12px; + color: #888; + font-style: italic; +} +.info-box { + background: #e3f2fd; + border-radius: 8px; + padding: 16px 20px; + margin-bottom: 24px; + font-size: 18px; +} +.info-box.success { + background: #e8f5e9; + color: #2e7d32; +} +.code-block { + background: #1e1e1e; + color: #d4d4d4; + padding: 16px; + border-radius: 8px; + overflow-x: auto; + font-size: 13px; + line-height: 1.5; +} +.history-list { + list-style: none; + padding: 0; + margin: 0 0 16px; +} +.history-list li { + padding: 10px 14px; + background: #f0f0f0; + border-radius: 6px; + margin-bottom: 6px; + font-family: monospace; + font-size: 14px; +} +button { + background: #0066cc; + color: white; + border: none; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; +} +button:hover { + background: #0052a3; +} diff --git a/examples/preact/useHotkeySequences/src/index.tsx b/examples/preact/useHotkeySequences/src/index.tsx new file mode 100644 index 00000000..90e53ce2 --- /dev/null +++ b/examples/preact/useHotkeySequences/src/index.tsx @@ -0,0 +1,257 @@ +import React from 'preact/compat' +import { render } from 'preact' +import { + HotkeysProvider, + useHotkey, + useHotkeySequences, +} from '@tanstack/preact-hotkeys' +import { hotkeysDevtoolsPlugin } from '@tanstack/preact-hotkeys-devtools' +import { TanStackDevtools } from '@tanstack/preact-devtools' +import './index.css' + +function App() { + const [lastSequence, setLastSequence] = React.useState(null) + const [history, setHistory] = React.useState>([]) + + const addToHistory = (action: string) => { + setLastSequence(action) + setHistory((h) => [...h.slice(-9), action]) + } + + useHotkeySequences([ + { sequence: ['G', 'G'], callback: () => addToHistory('gg → Go to top') }, + { + sequence: ['Shift+G'], + callback: () => addToHistory('G → Go to bottom'), + }, + { sequence: ['D', 'D'], callback: () => addToHistory('dd → Delete line') }, + { + sequence: ['Y', 'Y'], + callback: () => addToHistory('yy → Yank (copy) line'), + }, + { sequence: ['D', 'W'], callback: () => addToHistory('dw → Delete word') }, + { + sequence: ['C', 'I', 'W'], + callback: () => addToHistory('ciw → Change inner word'), + }, + { + sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'], + callback: () => addToHistory('↑↑↓↓ → Konami code (partial)'), + options: { timeout: 1500 }, + }, + { + sequence: ['ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight'], + callback: () => addToHistory('←→←→ → Side to side!'), + options: { timeout: 1500 }, + }, + { + sequence: ['H', 'E', 'L', 'L', 'O'], + callback: () => addToHistory('hello → Hello World!'), + }, + { + sequence: ['Shift+R', 'Shift+T'], + callback: () => addToHistory('⇧R ⇧T → Chained Shift+letter (2 steps)'), + }, + ]) + + // Clear history with Escape + useHotkey('Escape', () => { + setLastSequence(null) + setHistory([]) + }) + + return ( +
+
+

useHotkeySequences

+

+ Register many multi-key sequences in one hook (like Vim commands). + Keys must be pressed within the timeout window (default: 1000ms). +

+
+ +
+
+

Vim-Style Commands

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SequenceAction
+ g g + Go to top
+ G (Shift+G) + Go to bottom
+ d d + Delete line
+ y y + Yank (copy) line
+ d w + Delete word
+ c i w + Change inner word
+
+ +
+

Fun Sequences

+
+
+

Konami Code (Partial)

+

+ +

+ Use arrow keys within 1.5 seconds +
+
+

Side to Side

+

+ +

+ Arrow keys within 1.5 seconds +
+
+

Spell It Out

+

+ h e l l o +

+ Type "hello" quickly +
+
+
+ + {lastSequence && ( +
+ Triggered: {lastSequence} +
+ )} + +
+

Input handling

+

+ Sequences are not detected when typing in text inputs, textareas, + selects, or contenteditable elements. Button-type inputs ( + type="button", submit, reset) + still receive sequences. Focus the input below and try g{' '} + g or h + e + l + l + o — nothing will trigger. Click outside to try again. +

+ +
+ +
+

Chained Shift+letter sequences

+

+ Each step is a chord: hold Shift and press a letter. You + can press Shift alone between steps—those modifier-only + presses do not reset progress, so the next chord still counts. +

+ + + + + + + + + + + + + +
SequenceAction
+ Shift+r then Shift+ + t + Chained Shift+letter (2 steps)
+
+ +
+

Usage

+
{`import { useHotkeySequences } from '@tanstack/preact-hotkeys'
+
+function VimEditor() {
+  useHotkeySequences([
+    { sequence: ['G', 'G'], callback: () => scrollToTop() },
+    {
+      sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+      callback: () => activateCheatMode(),
+      options: { timeout: 1500 },
+    },
+    { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord() },
+    { sequence: ['Shift+R', 'Shift+T'], callback: () => doSomething() },
+  ])
+}`}
+
+ + {history.length > 0 && ( +
+

History

+
    + {history.map((item, i) => ( +
  • {item}
  • + ))} +
+ +
+ )} + +

+ Press Escape to clear history +

+
+
+ ) +} + +// TanStackDevtools as sibling of App to avoid Preact hook errors when hotkeys update state +const devtoolsPlugins = [hotkeysDevtoolsPlugin()] + +render( + // optionally, provide default options to an optional HotkeysProvider + + + + , + document.getElementById('root')!, +) diff --git a/examples/preact/useHotkeySequences/tsconfig.json b/examples/preact/useHotkeySequences/tsconfig.json new file mode 100644 index 00000000..faa3381b --- /dev/null +++ b/examples/preact/useHotkeySequences/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "jsxImportSource": "preact" + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/preact/useHotkeySequences/vite.config.ts b/examples/preact/useHotkeySequences/vite.config.ts new file mode 100644 index 00000000..bfe110c0 --- /dev/null +++ b/examples/preact/useHotkeySequences/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' + +export default defineConfig({ + plugins: [preact()], +}) diff --git a/examples/react/useHotkeySequences/eslint.config.js b/examples/react/useHotkeySequences/eslint.config.js new file mode 100644 index 00000000..3fd4ac43 --- /dev/null +++ b/examples/react/useHotkeySequences/eslint.config.js @@ -0,0 +1,11 @@ +// @ts-check + +import rootConfig from '../../../eslint.config.js' + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { + ignores: ['eslint.config.js'], + }, + ...rootConfig, +] diff --git a/examples/react/useHotkeySequences/index.html b/examples/react/useHotkeySequences/index.html new file mode 100644 index 00000000..0f57a386 --- /dev/null +++ b/examples/react/useHotkeySequences/index.html @@ -0,0 +1,14 @@ + + + + + + + useHotkeySequences - TanStack Hotkeys React Example + + + +
+ + + diff --git a/examples/react/useHotkeySequences/package.json b/examples/react/useHotkeySequences/package.json new file mode 100644 index 00000000..4a02f7f8 --- /dev/null +++ b/examples/react/useHotkeySequences/package.json @@ -0,0 +1,27 @@ +{ + "name": "@tanstack/hotkeys-example-react-use-hotkey-sequences", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3069", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/react-hotkeys": "^0.6.0", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@tanstack/react-devtools": "0.10.0", + "@tanstack/react-hotkeys-devtools": "^0.5.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "5.9.3", + "vite": "^8.0.1" + } +} diff --git a/examples/react/useHotkeySequences/src/index.css b/examples/react/useHotkeySequences/src/index.css new file mode 100644 index 00000000..69b749be --- /dev/null +++ b/examples/react/useHotkeySequences/src/index.css @@ -0,0 +1,159 @@ +* { + box-sizing: border-box; +} +body { + margin: 0; + font-family: + system-ui, + -apple-system, + sans-serif; + background: #f5f5f5; + color: #333; +} +.app { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} +header { + text-align: center; + margin-bottom: 40px; +} +header h1 { + margin: 0 0 10px; + color: #0066cc; +} +header p { + color: #666; + margin: 0; + max-width: 500px; + margin: 0 auto; +} + +.demo-section { + background: white; + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} +.demo-section h2 { + margin: 0 0 16px; + font-size: 20px; +} +kbd { + background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%); + border: 1px solid #ccc; + border-bottom-width: 2px; + border-radius: 4px; + padding: 2px 8px; + font-family: monospace; + font-size: 13px; + margin-right: 4px; +} +.sequence-table { + width: 100%; + border-collapse: collapse; +} +.sequence-table th, +.sequence-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #eee; +} +.sequence-table th { + font-weight: 600; + color: #666; + font-size: 14px; +} +.fun-sequences { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; +} +.sequence-card { + background: #f8f9fa; + border-radius: 8px; + padding: 16px; + text-align: center; +} +.sequence-card h3 { + margin: 0 0 12px; + font-size: 16px; +} +.sequence-card p { + margin: 0 0 8px; +} +.hint { + font-size: 12px; + color: #888; + font-style: italic; +} +.info-box { + background: #e3f2fd; + border-radius: 8px; + padding: 16px 20px; + margin-bottom: 24px; + font-size: 18px; +} +.info-box.success { + background: #e8f5e9; + color: #2e7d32; +} +.code-block { + background: #1e1e1e; + color: #d4d4d4; + padding: 16px; + border-radius: 8px; + overflow-x: auto; + font-size: 13px; + line-height: 1.5; +} +.history-list { + list-style: none; + padding: 0; + margin: 0 0 16px; +} +.history-list li { + padding: 10px 14px; + background: #f0f0f0; + border-radius: 6px; + margin-bottom: 6px; + font-family: monospace; + font-size: 14px; +} +button { + background: #0066cc; + color: white; + border: none; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; +} +button:hover { + background: #0052a3; +} + +.counter { + font-size: 18px; + font-weight: bold; + color: #0066cc; + margin: 12px 0; +} + +.demo-input { + width: 100%; + max-width: 400px; + padding: 12px 16px; + font-size: 14px; + border: 1px solid #ddd; + border-radius: 6px; + margin-top: 8px; +} + +.demo-input:focus { + outline: 2px solid #0066cc; + outline-offset: 2px; + border-color: #0066cc; +} diff --git a/examples/react/useHotkeySequences/src/index.tsx b/examples/react/useHotkeySequences/src/index.tsx new file mode 100644 index 00000000..f6b8d13f --- /dev/null +++ b/examples/react/useHotkeySequences/src/index.tsx @@ -0,0 +1,254 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { + HotkeysProvider, + useHotkey, + useHotkeySequences, +} from '@tanstack/react-hotkeys' +import { hotkeysDevtoolsPlugin } from '@tanstack/react-hotkeys-devtools' +import { TanStackDevtools } from '@tanstack/react-devtools' +import './index.css' + +function App() { + const [lastSequence, setLastSequence] = React.useState(null) + const [history, setHistory] = React.useState>([]) + + const addToHistory = (action: string) => { + setLastSequence(action) + setHistory((h) => [...h.slice(-9), action]) + } + + useHotkeySequences([ + { sequence: ['G', 'G'], callback: () => addToHistory('gg → Go to top') }, + { + sequence: ['Shift+G'], + callback: () => addToHistory('G → Go to bottom'), + }, + { sequence: ['D', 'D'], callback: () => addToHistory('dd → Delete line') }, + { + sequence: ['Y', 'Y'], + callback: () => addToHistory('yy → Yank (copy) line'), + }, + { sequence: ['D', 'W'], callback: () => addToHistory('dw → Delete word') }, + { + sequence: ['C', 'I', 'W'], + callback: () => addToHistory('ciw → Change inner word'), + }, + { + sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'], + callback: () => addToHistory('↑↑↓↓ → Konami code (partial)'), + options: { timeout: 1500 }, + }, + { + sequence: ['ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight'], + callback: () => addToHistory('←→←→ → Side to side!'), + options: { timeout: 1500 }, + }, + { + sequence: ['H', 'E', 'L', 'L', 'O'], + callback: () => addToHistory('hello → Hello World!'), + }, + { + sequence: ['Shift+R', 'Shift+T'], + callback: () => addToHistory('⇧R ⇧T → Chained Shift+letter (2 steps)'), + }, + ]) + + // Clear history with Escape + useHotkey('Escape', () => { + setLastSequence(null) + setHistory([]) + }) + + return ( +
+
+

useHotkeySequences

+

+ Register many multi-key sequences in one hook (like Vim commands). + Keys must be pressed within the timeout window (default: 1000ms). +

+
+ +
+
+

Vim-Style Commands

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SequenceAction
+ g g + Go to top
+ G (Shift+G) + Go to bottom
+ d d + Delete line
+ y y + Yank (copy) line
+ d w + Delete word
+ c i w + Change inner word
+
+ +
+

Fun Sequences

+
+
+

Konami Code (Partial)

+

+ +

+ Use arrow keys within 1.5 seconds +
+
+

Side to Side

+

+ +

+ Arrow keys within 1.5 seconds +
+
+

Spell It Out

+

+ h e l l o +

+ Type "hello" quickly +
+
+
+ + {lastSequence && ( +
+ Triggered: {lastSequence} +
+ )} + +
+

Input handling

+

+ Sequences are not detected when typing in text inputs, textareas, + selects, or contenteditable elements. Button-type inputs ( + type="button", submit, reset) + still receive sequences. Focus the input below and try g{' '} + g or h + e + l + l + o — nothing will trigger. Click outside to try again. +

+ +
+ +
+

Chained Shift+letter sequences

+

+ Each step is a chord: hold Shift and press a letter. You + can press Shift alone between steps—those modifier-only + presses do not reset progress, so the next chord still counts. +

+ + + + + + + + + + + + + +
SequenceAction
+ Shift+r then Shift+ + t + Chained Shift+letter (2 steps)
+
+ +
+

Usage

+
{`import { useHotkeySequences } from '@tanstack/react-hotkeys'
+
+function VimEditor() {
+  useHotkeySequences([
+    { sequence: ['G', 'G'], callback: () => scrollToTop() },
+    {
+      sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+      callback: () => activateCheatMode(),
+      options: { timeout: 1500 },
+    },
+    { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord() },
+    { sequence: ['Shift+R', 'Shift+T'], callback: () => doSomething() },
+  ])
+}`}
+
+ + {history.length > 0 && ( +
+

History

+
    + {history.map((item, i) => ( +
  • {item}
  • + ))} +
+ +
+ )} + +

+ Press Escape to clear history +

+
+ + +
+ ) +} + +ReactDOM.createRoot(document.getElementById('root')!).render( + // optionally, provide default options to an optional HotkeysProvider + + + , +) diff --git a/examples/react/useHotkeySequences/tsconfig.json b/examples/react/useHotkeySequences/tsconfig.json new file mode 100644 index 00000000..a97ff8c1 --- /dev/null +++ b/examples/react/useHotkeySequences/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/react/useHotkeySequences/vite.config.ts b/examples/react/useHotkeySequences/vite.config.ts new file mode 100644 index 00000000..9ffcc675 --- /dev/null +++ b/examples/react/useHotkeySequences/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], +}) diff --git a/examples/solid/createHotkeySequences/index.html b/examples/solid/createHotkeySequences/index.html new file mode 100644 index 00000000..96724de9 --- /dev/null +++ b/examples/solid/createHotkeySequences/index.html @@ -0,0 +1,11 @@ + + + + + createHotkeySequences - TanStack Hotkeys Solid Example + + +
+ + + diff --git a/examples/solid/createHotkeySequences/package.json b/examples/solid/createHotkeySequences/package.json new file mode 100644 index 00000000..1c5f2479 --- /dev/null +++ b/examples/solid/createHotkeySequences/package.json @@ -0,0 +1,20 @@ +{ + "name": "@tanstack/hotkeys-example-solid-create-hotkey-sequences", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3069", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/solid-devtools": "0.8.0", + "@tanstack/solid-hotkeys": "^0.6.0", + "@tanstack/solid-hotkeys-devtools": "^0.5.0", + "solid-js": "^1.9.11" + }, + "devDependencies": { + "vite": "^8.0.1", + "vite-plugin-solid": "^2.11.11" + } +} diff --git a/examples/solid/createHotkeySequences/src/index.css b/examples/solid/createHotkeySequences/src/index.css new file mode 100644 index 00000000..bfce1a5b --- /dev/null +++ b/examples/solid/createHotkeySequences/src/index.css @@ -0,0 +1,141 @@ +* { + box-sizing: border-box; +} +body { + margin: 0; + font-family: + system-ui, + -apple-system, + sans-serif; + background: #f5f5f5; + color: #333; +} +.app { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} +header h1 { + margin: 0 0 10px; + color: #0066cc; +} +header p { + color: #666; + margin: 0; + max-width: 500px; + margin: 0 auto; +} + +.demo-section { + background: white; + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} +.demo-section h2 { + margin: 0 0 16px; + font-size: 20px; +} +kbd { + background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%); + border: 1px solid #ccc; + border-radius: 4px; + padding: 2px 8px; + font-family: monospace; + font-size: 13px; + margin-right: 4px; +} +.sequence-table { + width: 100%; + border-collapse: collapse; +} +.sequence-table th, +.sequence-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #eee; +} +.fun-sequences { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; +} +.sequence-card { + background: #f8f9fa; + border-radius: 8px; + padding: 16px; + text-align: center; +} +.sequence-card h3 { + margin: 0 0 12px; + font-size: 16px; +} +.hint { + font-size: 12px; + color: #888; + font-style: italic; +} +.info-box { + background: #e3f2fd; + border-radius: 8px; + padding: 16px 20px; + margin-bottom: 24px; + font-size: 18px; +} +.info-box.success { + background: #e8f5e9; + color: #2e7d32; +} +.code-block { + background: #1e1e1e; + color: #d4d4d4; + padding: 16px; + border-radius: 8px; + overflow-x: auto; + font-size: 13px; +} +.history-list { + list-style: none; + padding: 0; + margin: 0 0 16px; +} +.history-list li { + padding: 10px 14px; + background: #f0f0f0; + border-radius: 6px; + margin-bottom: 6px; + font-family: monospace; + font-size: 14px; +} +button { + background: #0066cc; + color: white; + border: none; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; +} + +.counter { + font-size: 18px; + font-weight: bold; + color: #0066cc; + margin: 12px 0; +} + +.demo-input { + width: 100%; + max-width: 400px; + padding: 12px 16px; + font-size: 14px; + border: 1px solid #ddd; + border-radius: 6px; + margin-top: 8px; +} + +.demo-input:focus { + outline: 2px solid #0066cc; + outline-offset: 2px; + border-color: #0066cc; +} diff --git a/examples/solid/createHotkeySequences/src/index.tsx b/examples/solid/createHotkeySequences/src/index.tsx new file mode 100644 index 00000000..dc7c60cb --- /dev/null +++ b/examples/solid/createHotkeySequences/src/index.tsx @@ -0,0 +1,251 @@ +/* @refresh reload */ +import { render } from 'solid-js/web' +import { createSignal, Show } from 'solid-js' +import { + createHotkey, + createHotkeySequences, + HotkeysProvider, +} from '@tanstack/solid-hotkeys' +import { hotkeysDevtoolsPlugin } from '@tanstack/solid-hotkeys-devtools' +import { TanStackDevtools } from '@tanstack/solid-devtools' +import './index.css' + +function App() { + const [lastSequence, setLastSequence] = createSignal(null) + const [history, setHistory] = createSignal>([]) + const addToHistory = (action: string) => { + setLastSequence(action) + setHistory((h) => [...h.slice(-9), action]) + } + + createHotkeySequences([ + { sequence: ['G', 'G'], callback: () => addToHistory('gg → Go to top') }, + { + sequence: ['Shift+G'], + callback: () => addToHistory('G → Go to bottom'), + }, + { sequence: ['D', 'D'], callback: () => addToHistory('dd → Delete line') }, + { + sequence: ['Y', 'Y'], + callback: () => addToHistory('yy → Yank (copy) line'), + }, + { sequence: ['D', 'W'], callback: () => addToHistory('dw → Delete word') }, + { + sequence: ['C', 'I', 'W'], + callback: () => addToHistory('ciw → Change inner word'), + }, + { + sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'], + callback: () => addToHistory('↑↑↓↓ → Konami code (partial)'), + options: { timeout: 1500 }, + }, + { + sequence: ['ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight'], + callback: () => addToHistory('←→←→ → Side to side!'), + options: { timeout: 1500 }, + }, + { + sequence: ['H', 'E', 'L', 'L', 'O'], + callback: () => addToHistory('hello → Hello World!'), + }, + { + sequence: ['Shift+R', 'Shift+T'], + callback: () => addToHistory('⇧R ⇧T → Chained Shift+letter (2 steps)'), + }, + ]) + + createHotkey('Escape', () => { + setLastSequence(null) + setHistory([]) + }) + + return ( +
+
+

createHotkeySequences

+

+ Register many multi-key sequences in one primitive (like Vim + commands). Keys must be pressed within the timeout window (default: + 1000ms). +

+
+ +
+
+

Vim-Style Commands

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SequenceAction
+ g g + Go to top
+ G (Shift+G) + Go to bottom
+ d d + Delete line
+ y y + Yank (copy) line
+ d w + Delete word
+ c i w + Change inner word
+
+ +
+

Fun Sequences

+
+
+

Konami Code (Partial)

+

+ +

+ Use arrow keys within 1.5 seconds +
+
+

Side to Side

+

+ +

+ Arrow keys within 1.5 seconds +
+
+

Spell It Out

+

+ h e l l o +

+ Type "hello" quickly +
+
+
+ + +
+ Triggered: {lastSequence()} +
+
+ +
+

Input handling

+

+ Sequences are not detected when typing in text inputs, textareas, + selects, or contenteditable elements. Button-type inputs ( + type="button", submit, reset) + still receive sequences. Focus the input below and try g{' '} + g or h + e + l + l + o — nothing will trigger. Click outside to try again. +

+ +
+ +
+

Chained Shift+letter sequences

+

+ Each step is a chord: hold Shift and press a letter. You + can press Shift alone between steps—those modifier-only + presses do not reset progress, so the next chord still counts. +

+ + + + + + + + + + + + + +
SequenceAction
+ Shift+r then Shift+ + t + Chained Shift+letter (2 steps)
+
+ +
+

Usage

+
{`import { createHotkeySequences } from '@tanstack/solid-hotkeys'
+
+function VimEditor() {
+  createHotkeySequences([
+    { sequence: ['G', 'G'], callback: () => scrollToTop() },
+    {
+      sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+      callback: () => activateCheatMode(),
+      options: { timeout: 1500 },
+    },
+    { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord() },
+    { sequence: ['Shift+R', 'Shift+T'], callback: () => doSomething() },
+  ])
+}`}
+
+ + 0}> +
+

History

+
    + {history().map((item, i) => ( +
  • {item}
  • + ))} +
+ +
+
+ +

+ Press Escape to clear history +

+
+ + +
+ ) +} + +const root = document.getElementById('root')! +render( + () => ( + + + + ), + root, +) diff --git a/examples/solid/createHotkeySequences/tsconfig.json b/examples/solid/createHotkeySequences/tsconfig.json new file mode 100644 index 00000000..cf560b81 --- /dev/null +++ b/examples/solid/createHotkeySequences/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "jsx": "preserve", + "jsxImportSource": "solid-js", + "strict": true, + "types": ["vite/client", "solid-js"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/solid/createHotkeySequences/vite.config.ts b/examples/solid/createHotkeySequences/vite.config.ts new file mode 100644 index 00000000..4095d9be --- /dev/null +++ b/examples/solid/createHotkeySequences/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [solid()], +}) diff --git a/examples/svelte/create-hotkey-sequences/.gitignore b/examples/svelte/create-hotkey-sequences/.gitignore new file mode 100644 index 00000000..3b462cb0 --- /dev/null +++ b/examples/svelte/create-hotkey-sequences/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/examples/svelte/create-hotkey-sequences/.npmrc b/examples/svelte/create-hotkey-sequences/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/examples/svelte/create-hotkey-sequences/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/examples/svelte/create-hotkey-sequences/README.md b/examples/svelte/create-hotkey-sequences/README.md new file mode 100644 index 00000000..57b77134 --- /dev/null +++ b/examples/svelte/create-hotkey-sequences/README.md @@ -0,0 +1,42 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project +npx sv create my-app +``` + +To recreate this project with the same configuration: + +```sh +# recreate this project +pnpm dlx sv create --template minimal --types ts --install pnpm create-hotkey +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/examples/svelte/create-hotkey-sequences/index.html b/examples/svelte/create-hotkey-sequences/index.html new file mode 100644 index 00000000..5ca121cc --- /dev/null +++ b/examples/svelte/create-hotkey-sequences/index.html @@ -0,0 +1,14 @@ + + + + + + + createHotkeySequences - TanStack Hotkeys Svelte Example + + + +
+ + + diff --git a/examples/svelte/create-hotkey-sequences/package.json b/examples/svelte/create-hotkey-sequences/package.json new file mode 100644 index 00000000..e9641353 --- /dev/null +++ b/examples/svelte/create-hotkey-sequences/package.json @@ -0,0 +1,17 @@ +{ + "name": "@tanstack/hotkeys-example-svelte-create-hotkey-sequences", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3069" + }, + "dependencies": { + "@tanstack/svelte-hotkeys": "0.6.0", + "svelte": "^5.54.1" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "typescript": "5.9.3", + "vite": "^8.0.1" + } +} diff --git a/examples/svelte/create-hotkey-sequences/src/App.svelte b/examples/svelte/create-hotkey-sequences/src/App.svelte new file mode 100644 index 00000000..29d6461e --- /dev/null +++ b/examples/svelte/create-hotkey-sequences/src/App.svelte @@ -0,0 +1,233 @@ + + +
+
+

createHotkeySequences

+

+ Register many multi-key sequences in one call (like Vim commands). Keys + must be pressed within the timeout window (default: 1000ms). +

+
+ +
+
+

Vim-Style Commands

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SequenceAction
+ g g + Go to top
+ G (Shift+G) + Go to bottom
+ d d + Delete line
+ y y + Yank (copy) line
+ d w + Delete word
+ c i w + Change inner word
+
+ +
+

Fun Sequences

+
+
+

Konami Code (Partial)

+

+ +

+ Use arrow keys within 1.5 seconds +
+
+

Side to Side

+

+ +

+ Arrow keys within 1.5 seconds +
+
+

Spell It Out

+

+ h e l l o +

+ Type "hello" quickly +
+
+
+ + {#if lastSequence} +
+ Triggered: + {lastSequence} +
+ {/if} + +
+

Input handling

+

+ Sequences are not detected when typing in text inputs, textareas, + selects, or contenteditable elements. Button-type inputs ( + type="button", submit, reset) + still receive sequences. Focus the input below and try g + g or h + e + l + l + o — nothing will trigger. Click outside to try again. +

+ +
+ +
+

Chained Shift+letter sequences

+

+ Each step is a chord: hold Shift and press a letter. You can + press Shift alone between steps—those modifier-only presses do + not reset progress, so the next chord still counts. +

+ + + + + + + + + + + + + +
SequenceAction
+ Shift+r then Shift+t + Chained Shift+letter (2 steps)
+
+ +
+

Usage

+
{`import { createHotkeySequences } from '@tanstack/svelte-hotkeys'
+
+`}
+
+ + {#if history.length > 0} +
+

History

+
    + {#each history as item} +
  • {item}
  • + {/each} +
+ +
+ {/if} + +

+ Press Escape to clear history +

+
+
diff --git a/examples/svelte/create-hotkey-sequences/src/Root.svelte b/examples/svelte/create-hotkey-sequences/src/Root.svelte new file mode 100644 index 00000000..12e94e98 --- /dev/null +++ b/examples/svelte/create-hotkey-sequences/src/Root.svelte @@ -0,0 +1,5 @@ + + + diff --git a/examples/svelte/create-hotkey-sequences/src/index.css b/examples/svelte/create-hotkey-sequences/src/index.css new file mode 100644 index 00000000..8ee43876 --- /dev/null +++ b/examples/svelte/create-hotkey-sequences/src/index.css @@ -0,0 +1,162 @@ +* { + box-sizing: border-box; +} +body { + margin: 0; + font-family: + system-ui, + -apple-system, + sans-serif; + background: #f5f5f5; + color: #333; +} +.app { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} +header { + text-align: center; + margin-bottom: 40px; +} +header h1 { + margin: 0 0 10px; + color: #0066cc; +} +header p { + color: #666; + margin: 0; + max-width: 500px; + margin: 0 auto; +} + +.demo-section { + background: white; + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} +.demo-section h2 { + margin: 0 0 16px; + font-size: 20px; +} +.demo-section p { + margin: 0 0 12px; +} +kbd { + background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%); + border: 1px solid #ccc; + border-bottom-width: 2px; + border-radius: 4px; + padding: 2px 8px; + font-family: monospace; + font-size: 13px; + margin-right: 4px; +} +.sequence-table { + width: 100%; + border-collapse: collapse; +} +.sequence-table th, +.sequence-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #eee; +} +.sequence-table th { + font-weight: 600; + color: #666; + font-size: 14px; +} +.fun-sequences { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; +} +.sequence-card { + background: #f8f9fa; + border-radius: 8px; + padding: 16px; + text-align: center; +} +.sequence-card h3 { + margin: 0 0 12px; + font-size: 16px; +} +.sequence-card p { + margin: 0 0 8px; +} +.hint { + font-size: 12px; + color: #888; + font-style: italic; +} +.info-box { + background: #e3f2fd; + border-radius: 8px; + padding: 16px 20px; + margin-bottom: 24px; + font-size: 18px; +} +.info-box.success { + background: #e8f5e9; + color: #2e7d32; +} +.code-block { + background: #1e1e1e; + color: #d4d4d4; + padding: 16px; + border-radius: 8px; + overflow-x: auto; + font-size: 13px; + line-height: 1.5; +} +.history-list { + list-style: none; + padding: 0; + margin: 0 0 16px; +} +.history-list li { + padding: 10px 14px; + background: #f0f0f0; + border-radius: 6px; + margin-bottom: 6px; + font-family: monospace; + font-size: 14px; +} +button { + background: #0066cc; + color: white; + border: none; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; +} +button:hover { + background: #0052a3; +} + +.counter { + font-size: 18px; + font-weight: bold; + color: #0066cc; + margin: 12px 0; +} + +.demo-input { + width: 100%; + max-width: 400px; + padding: 12px 16px; + font-size: 14px; + border: 1px solid #ddd; + border-radius: 6px; + margin-top: 8px; +} + +.demo-input:focus { + outline: 2px solid #0066cc; + outline-offset: 2px; + border-color: #0066cc; +} diff --git a/examples/svelte/create-hotkey-sequences/src/main.ts b/examples/svelte/create-hotkey-sequences/src/main.ts new file mode 100644 index 00000000..93579001 --- /dev/null +++ b/examples/svelte/create-hotkey-sequences/src/main.ts @@ -0,0 +1,5 @@ +import { mount } from 'svelte' +import Root from './Root.svelte' +import './index.css' + +mount(Root, { target: document.getElementById('app')! }) diff --git a/examples/svelte/create-hotkey-sequences/static/robots.txt b/examples/svelte/create-hotkey-sequences/static/robots.txt new file mode 100644 index 00000000..b6dd6670 --- /dev/null +++ b/examples/svelte/create-hotkey-sequences/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/examples/svelte/create-hotkey-sequences/svelte.config.js b/examples/svelte/create-hotkey-sequences/svelte.config.js new file mode 100644 index 00000000..b30b6572 --- /dev/null +++ b/examples/svelte/create-hotkey-sequences/svelte.config.js @@ -0,0 +1,11 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +/** @type {import('svelte').Config} */ +const config = { + preprocess: vitePreprocess(), + compilerOptions: { + runes: true, + }, +} + +export default config diff --git a/examples/svelte/create-hotkey-sequences/tsconfig.json b/examples/svelte/create-hotkey-sequences/tsconfig.json new file mode 100644 index 00000000..912a0303 --- /dev/null +++ b/examples/svelte/create-hotkey-sequences/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "moduleResolution": "bundler" + }, + "include": ["src"], + "exclude": ["eslint.config.js"] +} diff --git a/examples/svelte/create-hotkey-sequences/vite.config.ts b/examples/svelte/create-hotkey-sequences/vite.config.ts new file mode 100644 index 00000000..951a9ba4 --- /dev/null +++ b/examples/svelte/create-hotkey-sequences/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +export default defineConfig({ + plugins: [svelte()], +}) diff --git a/examples/vue/useHotkeySequences/eslint.config.js b/examples/vue/useHotkeySequences/eslint.config.js new file mode 100644 index 00000000..a47f4a62 --- /dev/null +++ b/examples/vue/useHotkeySequences/eslint.config.js @@ -0,0 +1,11 @@ +// @ts-check + +import rootConfig from '../../../eslint.config.js' + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { + ignores: ['eslint.config.js', 'vite.config.ts'], + }, + ...rootConfig, +] diff --git a/examples/vue/useHotkeySequences/index.html b/examples/vue/useHotkeySequences/index.html new file mode 100644 index 00000000..8d533559 --- /dev/null +++ b/examples/vue/useHotkeySequences/index.html @@ -0,0 +1,12 @@ + + + + + + useHotkeySequences - TanStack Hotkeys Vue Example + + +
+ + + diff --git a/examples/vue/useHotkeySequences/package.json b/examples/vue/useHotkeySequences/package.json new file mode 100644 index 00000000..95ab61fd --- /dev/null +++ b/examples/vue/useHotkeySequences/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/hotkeys-example-vue-use-hotkey-sequences", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3069", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/vue-hotkeys": "^0.6.0", + "vue": "^3.5.30" + }, + "devDependencies": { + "@tanstack/vue-devtools": "^0.2.14", + "@tanstack/vue-hotkeys-devtools": "^0.5.0", + "@vitejs/plugin-vue": "^6.0.5", + "typescript": "5.9.3", + "vite": "^8.0.1" + } +} diff --git a/examples/vue/useHotkeySequences/src/App.vue b/examples/vue/useHotkeySequences/src/App.vue new file mode 100644 index 00000000..7f71c747 --- /dev/null +++ b/examples/vue/useHotkeySequences/src/App.vue @@ -0,0 +1,217 @@ + + + diff --git a/examples/vue/useHotkeySequences/src/index.css b/examples/vue/useHotkeySequences/src/index.css new file mode 100644 index 00000000..69b749be --- /dev/null +++ b/examples/vue/useHotkeySequences/src/index.css @@ -0,0 +1,159 @@ +* { + box-sizing: border-box; +} +body { + margin: 0; + font-family: + system-ui, + -apple-system, + sans-serif; + background: #f5f5f5; + color: #333; +} +.app { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} +header { + text-align: center; + margin-bottom: 40px; +} +header h1 { + margin: 0 0 10px; + color: #0066cc; +} +header p { + color: #666; + margin: 0; + max-width: 500px; + margin: 0 auto; +} + +.demo-section { + background: white; + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} +.demo-section h2 { + margin: 0 0 16px; + font-size: 20px; +} +kbd { + background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%); + border: 1px solid #ccc; + border-bottom-width: 2px; + border-radius: 4px; + padding: 2px 8px; + font-family: monospace; + font-size: 13px; + margin-right: 4px; +} +.sequence-table { + width: 100%; + border-collapse: collapse; +} +.sequence-table th, +.sequence-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #eee; +} +.sequence-table th { + font-weight: 600; + color: #666; + font-size: 14px; +} +.fun-sequences { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; +} +.sequence-card { + background: #f8f9fa; + border-radius: 8px; + padding: 16px; + text-align: center; +} +.sequence-card h3 { + margin: 0 0 12px; + font-size: 16px; +} +.sequence-card p { + margin: 0 0 8px; +} +.hint { + font-size: 12px; + color: #888; + font-style: italic; +} +.info-box { + background: #e3f2fd; + border-radius: 8px; + padding: 16px 20px; + margin-bottom: 24px; + font-size: 18px; +} +.info-box.success { + background: #e8f5e9; + color: #2e7d32; +} +.code-block { + background: #1e1e1e; + color: #d4d4d4; + padding: 16px; + border-radius: 8px; + overflow-x: auto; + font-size: 13px; + line-height: 1.5; +} +.history-list { + list-style: none; + padding: 0; + margin: 0 0 16px; +} +.history-list li { + padding: 10px 14px; + background: #f0f0f0; + border-radius: 6px; + margin-bottom: 6px; + font-family: monospace; + font-size: 14px; +} +button { + background: #0066cc; + color: white; + border: none; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; +} +button:hover { + background: #0052a3; +} + +.counter { + font-size: 18px; + font-weight: bold; + color: #0066cc; + margin: 12px 0; +} + +.demo-input { + width: 100%; + max-width: 400px; + padding: 12px 16px; + font-size: 14px; + border: 1px solid #ddd; + border-radius: 6px; + margin-top: 8px; +} + +.demo-input:focus { + outline: 2px solid #0066cc; + outline-offset: 2px; + border-color: #0066cc; +} diff --git a/examples/vue/useHotkeySequences/src/index.ts b/examples/vue/useHotkeySequences/src/index.ts new file mode 100644 index 00000000..50a4dab0 --- /dev/null +++ b/examples/vue/useHotkeySequences/src/index.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import App from './App.vue' +import './index.css' + +createApp(App).mount('#app') diff --git a/examples/vue/useHotkeySequences/src/vue.d.ts b/examples/vue/useHotkeySequences/src/vue.d.ts new file mode 100644 index 00000000..b07a0596 --- /dev/null +++ b/examples/vue/useHotkeySequences/src/vue.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/examples/vue/useHotkeySequences/tsconfig.json b/examples/vue/useHotkeySequences/tsconfig.json new file mode 100644 index 00000000..b1d72611 --- /dev/null +++ b/examples/vue/useHotkeySequences/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve" + }, + "include": ["src"], + "exclude": ["eslint.config.js"] +} diff --git a/examples/vue/useHotkeySequences/vite.config.ts b/examples/vue/useHotkeySequences/vite.config.ts new file mode 100644 index 00000000..c40aa3c3 --- /dev/null +++ b/examples/vue/useHotkeySequences/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], +}) diff --git a/packages/angular-hotkeys/src/index.ts b/packages/angular-hotkeys/src/index.ts index 0101fd77..d19de42e 100644 --- a/packages/angular-hotkeys/src/index.ts +++ b/packages/angular-hotkeys/src/index.ts @@ -8,6 +8,7 @@ export * from './hotkeys-provider' export * from './injectHotkey' export * from './injectHotkeys' export * from './injectHotkeySequence' +export * from './injectHotkeySequences' export * from './injectHeldKeys' export * from './injectHeldKeyCodes' export * from './injectKeyHold' diff --git a/packages/angular-hotkeys/src/injectHotkeySequences.ts b/packages/angular-hotkeys/src/injectHotkeySequences.ts new file mode 100644 index 00000000..7f02ac2a --- /dev/null +++ b/packages/angular-hotkeys/src/injectHotkeySequences.ts @@ -0,0 +1,167 @@ +import { DestroyRef, effect, inject } from '@angular/core' +import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys' +import { injectDefaultHotkeysOptions } from './hotkeys-provider' +import type { InjectHotkeySequenceOptions } from './injectHotkeySequence' +import type { + HotkeyCallback, + HotkeySequence, + SequenceRegistrationHandle, +} from '@tanstack/hotkeys' + +/** + * A single sequence definition for use with `injectHotkeySequences`. + */ +export interface InjectHotkeySequenceDefinition { + /** Array of hotkey strings that form the sequence */ + sequence: HotkeySequence | (() => HotkeySequence) + /** The function to call when the sequence is completed */ + callback: HotkeyCallback + /** Per-sequence options (merged on top of commonOptions) */ + options?: InjectHotkeySequenceOptions | (() => InjectHotkeySequenceOptions) +} + +/** + * Angular inject-based API for registering multiple keyboard shortcut sequences at once (Vim-style). + * + * Uses the singleton SequenceManager. Call in an injection context (e.g. constructor). + * Uses `effect()` to track reactive dependencies when definitions or options are getters. + * + * Options are merged in this order: + * provideHotkeys defaults < commonOptions < per-definition options + * + * Definitions with an empty `sequence` or `enabled: false` after merge are skipped. + * + * @param sequences - Array of sequence definitions, or getter returning them + * @param commonOptions - Shared options for all sequences, or getter + * + * @example + * ```ts + * @Component({ ... }) + * export class VimComponent { + * constructor() { + * injectHotkeySequences([ + * { sequence: ['G', 'G'], callback: () => this.goTop() }, + * { sequence: ['D', 'D'], callback: () => this.deleteLine() }, + * ]) + * } + * } + * ``` + */ +export function injectHotkeySequences( + sequences: + | Array + | (() => Array), + commonOptions: + | InjectHotkeySequenceOptions + | (() => InjectHotkeySequenceOptions) = {}, +): void { + type RegistrationRecord = { + handle: SequenceRegistrationHandle + target: Document | HTMLElement | Window + } + + const defaultOptions = injectDefaultHotkeysOptions() + const manager = getSequenceManager() + const destroyRef = inject(DestroyRef) + + const registrations = new Map() + + destroyRef.onDestroy(() => { + for (const { handle } of registrations.values()) { + if (handle.isActive) { + handle.unregister() + } + } + registrations.clear() + }) + + effect(() => { + const resolvedSequences = + typeof sequences === 'function' ? sequences() : sequences + const resolvedCommonOptions = + typeof commonOptions === 'function' ? commonOptions() : commonOptions + + const nextKeys = new Set() + const prepared: Array<{ + registrationKey: string + def: InjectHotkeySequenceDefinition + resolvedSequence: HotkeySequence + sequenceOpts: Omit + resolvedTarget: Document | HTMLElement | Window + }> = [] + + for (let i = 0; i < resolvedSequences.length; i++) { + const def = resolvedSequences[i]! + const resolvedSequence = + typeof def.sequence === 'function' ? def.sequence() : def.sequence + const resolvedDefOptions = + typeof def.options === 'function' ? def.options() : (def.options ?? {}) + + const mergedOptions = { + ...defaultOptions.hotkeySequence, + ...resolvedCommonOptions, + ...resolvedDefOptions, + } as InjectHotkeySequenceOptions + + const { enabled = true, ...sequenceOpts } = mergedOptions + + if (!enabled || resolvedSequence.length === 0) { + continue + } + + const resolvedTarget = + sequenceOpts.target ?? + (typeof document !== 'undefined' ? document : null) + + if (!resolvedTarget) { + continue + } + + const registrationKey = `${i}:${formatHotkeySequence(resolvedSequence)}` + nextKeys.add(registrationKey) + prepared.push({ + registrationKey, + def, + resolvedSequence, + sequenceOpts, + resolvedTarget, + }) + } + + for (const [key, record] of [...registrations.entries()]) { + if (!nextKeys.has(key)) { + if (record.handle.isActive) { + record.handle.unregister() + } + registrations.delete(key) + } + } + + for (const p of prepared) { + const existing = registrations.get(p.registrationKey) + if (existing?.handle.isActive && existing.target === p.resolvedTarget) { + existing.handle.callback = p.def.callback + const { target: _target, ...optionsWithoutTarget } = p.sequenceOpts + existing.handle.setOptions(optionsWithoutTarget) + continue + } + + if (existing) { + if (existing.handle.isActive) { + existing.handle.unregister() + } + registrations.delete(p.registrationKey) + } + + const handle = manager.register(p.resolvedSequence, p.def.callback, { + ...p.sequenceOpts, + enabled: true, + target: p.resolvedTarget, + }) + registrations.set(p.registrationKey, { + handle, + target: p.resolvedTarget, + }) + } + }) +} diff --git a/packages/preact-hotkeys/src/index.ts b/packages/preact-hotkeys/src/index.ts index c4463a2a..f144170e 100644 --- a/packages/preact-hotkeys/src/index.ts +++ b/packages/preact-hotkeys/src/index.ts @@ -11,5 +11,6 @@ export * from './useHeldKeys' export * from './useHeldKeyCodes' export * from './useKeyHold' export * from './useHotkeySequence' +export * from './useHotkeySequences' export * from './useHotkeyRecorder' export * from './useHotkeySequenceRecorder' diff --git a/packages/preact-hotkeys/src/useHotkeySequences.ts b/packages/preact-hotkeys/src/useHotkeySequences.ts new file mode 100644 index 00000000..f6ce18d6 --- /dev/null +++ b/packages/preact-hotkeys/src/useHotkeySequences.ts @@ -0,0 +1,213 @@ +import { useEffect, useRef } from 'preact/hooks' +import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys' +import { useDefaultHotkeysOptions } from './HotkeysProvider' +import { isRef } from './utils' +import type { UseHotkeySequenceOptions } from './useHotkeySequence' +import type { + HotkeyCallback, + HotkeySequence, + SequenceRegistrationHandle, +} from '@tanstack/hotkeys' + +/** + * A single sequence definition for use with `useHotkeySequences`. + */ +export interface UseHotkeySequenceDefinition { + /** Array of hotkey strings that form the sequence */ + sequence: HotkeySequence + /** The function to call when the sequence is completed */ + callback: HotkeyCallback + /** Per-sequence options (merged on top of commonOptions) */ + options?: UseHotkeySequenceOptions +} + +/** + * Preact hook for registering multiple keyboard shortcut sequences at once (Vim-style). + * + * Uses the singleton SequenceManager. Accepts a dynamic array of definitions so you can + * register variable-length lists without violating the rules of hooks. + * + * Options are merged in this order: + * HotkeysProvider defaults < commonOptions < per-definition options + * + * Callbacks and options are synced on every render to avoid stale closures. + * + * Definitions with an empty `sequence` are skipped (no registration). + * + * @param definitions - Array of sequence definitions to register + * @param commonOptions - Shared options applied to all sequences (overridden by per-definition options) + * + * @example + * ```tsx + * function VimPalette() { + * useHotkeySequences([ + * { sequence: ['G', 'G'], callback: () => scrollToTop() }, + * { sequence: ['D', 'D'], callback: () => deleteLine() }, + * { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord(), options: { timeout: 500 } }, + * ]) + * } + * ``` + * + * @example + * ```tsx + * function DynamicSequences({ items }) { + * useHotkeySequences( + * items.map((item) => ({ + * sequence: item.chords, + * callback: item.action, + * options: { enabled: item.enabled }, + * })), + * { preventDefault: true }, + * ) + * } + * ``` + */ +export function useHotkeySequences( + definitions: Array, + commonOptions: UseHotkeySequenceOptions = {}, +): void { + type RegistrationRecord = { + handle: SequenceRegistrationHandle + target: Document | HTMLElement | Window + } + + const defaultOptions = useDefaultHotkeysOptions().hotkeySequence + const manager = getSequenceManager() + + const registrationsRef = useRef>(new Map()) + const definitionsRef = useRef(definitions) + const sequenceStringsRef = useRef>([]) + const commonOptionsRef = useRef(commonOptions) + const defaultOptionsRef = useRef(defaultOptions) + const managerRef = useRef(manager) + + const sequenceStrings = definitions.map((def) => + formatHotkeySequence(def.sequence), + ) + + definitionsRef.current = definitions + sequenceStringsRef.current = sequenceStrings + commonOptionsRef.current = commonOptions + defaultOptionsRef.current = defaultOptions + managerRef.current = manager + + const sequenceKey = sequenceStrings.join('\0') + const enabledKey = definitions + .map((def) => { + const merged = { + ...defaultOptions, + ...commonOptions, + ...def.options, + } + return merged.enabled ?? true + }) + .join('\0') + + useEffect(() => { + const prevRegistrations = registrationsRef.current + const nextRegistrations = new Map() + + const rows: Array<{ + registrationKey: string + def: (typeof definitionsRef.current)[number] + seq: HotkeySequence + seqStr: string + mergedOptions: UseHotkeySequenceOptions + resolvedTarget: Document | HTMLElement | Window + }> = [] + + for (let i = 0; i < definitionsRef.current.length; i++) { + const def = definitionsRef.current[i]! + const seqStr = sequenceStringsRef.current[i]! + const seq = def.sequence + if (seq.length === 0) { + continue + } + + const mergedOptions = { + ...defaultOptionsRef.current, + ...commonOptionsRef.current, + ...def.options, + } as UseHotkeySequenceOptions + + const resolvedTarget = isRef(mergedOptions.target) + ? mergedOptions.target.current + : (mergedOptions.target ?? + (typeof document !== 'undefined' ? document : null)) + + if (!resolvedTarget) { + continue + } + + const registrationKey = `${i}:${seqStr}` + rows.push({ + registrationKey, + def, + seq, + seqStr, + mergedOptions, + resolvedTarget, + }) + } + + const nextKeys = new Set(rows.map((r) => r.registrationKey)) + + for (const [key, record] of prevRegistrations) { + if (!nextKeys.has(key) && record.handle.isActive) { + record.handle.unregister() + } + } + + for (const row of rows) { + const { registrationKey, def, seq, mergedOptions, resolvedTarget } = row + + const existing = prevRegistrations.get(registrationKey) + if (existing?.handle.isActive && existing.target === resolvedTarget) { + nextRegistrations.set(registrationKey, existing) + continue + } + + if (existing?.handle.isActive) { + existing.handle.unregister() + } + + const handle = managerRef.current.register(seq, def.callback, { + ...mergedOptions, + target: resolvedTarget, + }) + nextRegistrations.set(registrationKey, { + handle, + target: resolvedTarget, + }) + } + + registrationsRef.current = nextRegistrations + + return () => { + for (const { handle } of registrationsRef.current.values()) { + if (handle.isActive) { + handle.unregister() + } + } + registrationsRef.current = new Map() + } + }, [sequenceKey, enabledKey]) + + for (let i = 0; i < definitions.length; i++) { + const def = definitions[i]! + const seqStr = sequenceStrings[i]! + const registrationKey = `${i}:${seqStr}` + const handle = registrationsRef.current.get(registrationKey)?.handle + + if (handle?.isActive && def.sequence.length > 0) { + handle.callback = def.callback + const mergedOptions = { + ...defaultOptions, + ...commonOptions, + ...def.options, + } as UseHotkeySequenceOptions + const { target: _target, ...optionsWithoutTarget } = mergedOptions + handle.setOptions(optionsWithoutTarget) + } + } +} diff --git a/packages/preact-hotkeys/tests/useHotkeySequences.test.tsx b/packages/preact-hotkeys/tests/useHotkeySequences.test.tsx new file mode 100644 index 00000000..baab1b00 --- /dev/null +++ b/packages/preact-hotkeys/tests/useHotkeySequences.test.tsx @@ -0,0 +1,131 @@ +// @vitest-environment happy-dom +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { cleanup, render } from '@testing-library/preact' +import { SequenceManager } from '@tanstack/hotkeys' +import { useHotkeySequences } from '../src/useHotkeySequences' +import type { UseHotkeySequenceDefinition } from '../src/useHotkeySequences' + +function dispatchKey(key: string) { + document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true })) +} + +function SequencesComponent({ + definitions, +}: { + definitions: Array +}) { + useHotkeySequences(definitions) + return null +} + +describe('useHotkeySequences', () => { + beforeEach(() => { + SequenceManager.resetInstance() + }) + + afterEach(() => { + SequenceManager.resetInstance() + cleanup() + }) + + it('should register multiple sequence handlers', () => { + const a = vi.fn() + const b = vi.fn() + + render( + , + ) + + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(2) + }) + + it('should call the correct callback for each sequence', () => { + const gg = vi.fn() + const dd = vi.fn() + + render( + , + ) + + dispatchKey('g') + dispatchKey('g') + expect(gg).toHaveBeenCalledTimes(1) + expect(dd).not.toHaveBeenCalled() + + dispatchKey('d') + dispatchKey('d') + expect(gg).toHaveBeenCalledTimes(1) + expect(dd).toHaveBeenCalledTimes(1) + }) + + it('should unregister all sequences on unmount', () => { + const { unmount } = render( + , + ) + + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(2) + unmount() + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(0) + }) + + it('should handle an empty array as a no-op', () => { + render() + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(0) + }) + + it('should skip definitions with an empty sequence', () => { + render( + , + ) + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(1) + }) + + describe('stale closure prevention', () => { + it('should sync enabled option on every render', () => { + const callback = vi.fn() + + function EnabledSequences({ enabled }: { enabled: boolean }) { + useHotkeySequences([ + { sequence: ['G', 'G'], callback, options: { enabled } }, + ]) + return null + } + + const { rerender } = render() + + dispatchKey('g') + dispatchKey('g') + expect(callback).toHaveBeenCalledTimes(1) + + rerender() + dispatchKey('g') + dispatchKey('g') + expect(callback).toHaveBeenCalledTimes(1) + + rerender() + dispatchKey('g') + dispatchKey('g') + expect(callback).toHaveBeenCalledTimes(2) + }) + }) +}) diff --git a/packages/react-hotkeys/src/index.ts b/packages/react-hotkeys/src/index.ts index d33cdc82..a89024f7 100644 --- a/packages/react-hotkeys/src/index.ts +++ b/packages/react-hotkeys/src/index.ts @@ -11,5 +11,6 @@ export * from './useHeldKeys' export * from './useHeldKeyCodes' export * from './useKeyHold' export * from './useHotkeySequence' +export * from './useHotkeySequences' export * from './useHotkeyRecorder' export * from './useHotkeySequenceRecorder' diff --git a/packages/react-hotkeys/src/useHotkeySequences.ts b/packages/react-hotkeys/src/useHotkeySequences.ts new file mode 100644 index 00000000..3b58da56 --- /dev/null +++ b/packages/react-hotkeys/src/useHotkeySequences.ts @@ -0,0 +1,213 @@ +import { useEffect, useRef } from 'react' +import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys' +import { useDefaultHotkeysOptions } from './HotkeysProvider' +import { isRef } from './utils' +import type { UseHotkeySequenceOptions } from './useHotkeySequence' +import type { + HotkeyCallback, + HotkeySequence, + SequenceRegistrationHandle, +} from '@tanstack/hotkeys' + +/** + * A single sequence definition for use with `useHotkeySequences`. + */ +export interface UseHotkeySequenceDefinition { + /** Array of hotkey strings that form the sequence */ + sequence: HotkeySequence + /** The function to call when the sequence is completed */ + callback: HotkeyCallback + /** Per-sequence options (merged on top of commonOptions) */ + options?: UseHotkeySequenceOptions +} + +/** + * React hook for registering multiple keyboard shortcut sequences at once (Vim-style). + * + * Uses the singleton SequenceManager. Accepts a dynamic array of definitions so you can + * register variable-length lists without violating the rules of hooks. + * + * Options are merged in this order: + * HotkeysProvider defaults < commonOptions < per-definition options + * + * Callbacks and options are synced on every render to avoid stale closures. + * + * Definitions with an empty `sequence` are skipped (no registration). + * + * @param definitions - Array of sequence definitions to register + * @param commonOptions - Shared options applied to all sequences (overridden by per-definition options) + * + * @example + * ```tsx + * function VimPalette() { + * useHotkeySequences([ + * { sequence: ['G', 'G'], callback: () => scrollToTop() }, + * { sequence: ['D', 'D'], callback: () => deleteLine() }, + * { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord(), options: { timeout: 500 } }, + * ]) + * } + * ``` + * + * @example + * ```tsx + * function DynamicSequences({ items }) { + * useHotkeySequences( + * items.map((item) => ({ + * sequence: item.chords, + * callback: item.action, + * options: { enabled: item.enabled }, + * })), + * { preventDefault: true }, + * ) + * } + * ``` + */ +export function useHotkeySequences( + definitions: Array, + commonOptions: UseHotkeySequenceOptions = {}, +): void { + type RegistrationRecord = { + handle: SequenceRegistrationHandle + target: Document | HTMLElement | Window + } + + const defaultOptions = useDefaultHotkeysOptions().hotkeySequence + const manager = getSequenceManager() + + const registrationsRef = useRef>(new Map()) + const definitionsRef = useRef(definitions) + const sequenceStringsRef = useRef>([]) + const commonOptionsRef = useRef(commonOptions) + const defaultOptionsRef = useRef(defaultOptions) + const managerRef = useRef(manager) + + const sequenceStrings = definitions.map((def) => + formatHotkeySequence(def.sequence), + ) + + definitionsRef.current = definitions + sequenceStringsRef.current = sequenceStrings + commonOptionsRef.current = commonOptions + defaultOptionsRef.current = defaultOptions + managerRef.current = manager + + const sequenceKey = sequenceStrings.join('\0') + const enabledKey = definitions + .map((def) => { + const merged = { + ...defaultOptions, + ...commonOptions, + ...def.options, + } + return merged.enabled ?? true + }) + .join('\0') + + useEffect(() => { + const prevRegistrations = registrationsRef.current + const nextRegistrations = new Map() + + const rows: Array<{ + registrationKey: string + def: (typeof definitionsRef.current)[number] + seq: HotkeySequence + seqStr: string + mergedOptions: UseHotkeySequenceOptions + resolvedTarget: Document | HTMLElement | Window + }> = [] + + for (let i = 0; i < definitionsRef.current.length; i++) { + const def = definitionsRef.current[i]! + const seqStr = sequenceStringsRef.current[i]! + const seq = def.sequence + if (seq.length === 0) { + continue + } + + const mergedOptions = { + ...defaultOptionsRef.current, + ...commonOptionsRef.current, + ...def.options, + } as UseHotkeySequenceOptions + + const resolvedTarget = isRef(mergedOptions.target) + ? mergedOptions.target.current + : (mergedOptions.target ?? + (typeof document !== 'undefined' ? document : null)) + + if (!resolvedTarget) { + continue + } + + const registrationKey = `${i}:${seqStr}` + rows.push({ + registrationKey, + def, + seq, + seqStr, + mergedOptions, + resolvedTarget, + }) + } + + const nextKeys = new Set(rows.map((r) => r.registrationKey)) + + for (const [key, record] of prevRegistrations) { + if (!nextKeys.has(key) && record.handle.isActive) { + record.handle.unregister() + } + } + + for (const row of rows) { + const { registrationKey, def, seq, mergedOptions, resolvedTarget } = row + + const existing = prevRegistrations.get(registrationKey) + if (existing?.handle.isActive && existing.target === resolvedTarget) { + nextRegistrations.set(registrationKey, existing) + continue + } + + if (existing?.handle.isActive) { + existing.handle.unregister() + } + + const handle = managerRef.current.register(seq, def.callback, { + ...mergedOptions, + target: resolvedTarget, + }) + nextRegistrations.set(registrationKey, { + handle, + target: resolvedTarget, + }) + } + + registrationsRef.current = nextRegistrations + + return () => { + for (const { handle } of registrationsRef.current.values()) { + if (handle.isActive) { + handle.unregister() + } + } + registrationsRef.current = new Map() + } + }, [sequenceKey, enabledKey]) + + for (let i = 0; i < definitions.length; i++) { + const def = definitions[i]! + const seqStr = sequenceStrings[i]! + const registrationKey = `${i}:${seqStr}` + const handle = registrationsRef.current.get(registrationKey)?.handle + + if (handle?.isActive && def.sequence.length > 0) { + handle.callback = def.callback + const mergedOptions = { + ...defaultOptions, + ...commonOptions, + ...def.options, + } as UseHotkeySequenceOptions + const { target: _target, ...optionsWithoutTarget } = mergedOptions + handle.setOptions(optionsWithoutTarget) + } + } +} diff --git a/packages/react-hotkeys/tests/useHotkeySequences.test.tsx b/packages/react-hotkeys/tests/useHotkeySequences.test.tsx new file mode 100644 index 00000000..fcd25b51 --- /dev/null +++ b/packages/react-hotkeys/tests/useHotkeySequences.test.tsx @@ -0,0 +1,189 @@ +// @vitest-environment happy-dom +import { act, renderHook } from '@testing-library/react' +import { SequenceManager } from '@tanstack/hotkeys' +import { useState } from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { useHotkeySequences } from '../src/useHotkeySequences' +import type { UseHotkeySequenceDefinition } from '../src/useHotkeySequences' + +function dispatchKey(key: string) { + document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true })) +} + +describe('useHotkeySequences', () => { + beforeEach(() => { + SequenceManager.resetInstance() + }) + + afterEach(() => { + SequenceManager.resetInstance() + }) + + it('should register multiple sequence handlers', () => { + const a = vi.fn() + const b = vi.fn() + + renderHook(() => + useHotkeySequences([ + { sequence: ['G', 'G'], callback: a }, + { sequence: ['D', 'D'], callback: b }, + ]), + ) + + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(2) + }) + + it('should call the correct callback for each sequence', () => { + const gg = vi.fn() + const dd = vi.fn() + + renderHook(() => + useHotkeySequences([ + { sequence: ['G', 'G'], callback: gg }, + { sequence: ['D', 'D'], callback: dd }, + ]), + ) + + dispatchKey('g') + dispatchKey('g') + expect(gg).toHaveBeenCalledTimes(1) + expect(dd).not.toHaveBeenCalled() + + dispatchKey('d') + dispatchKey('d') + expect(gg).toHaveBeenCalledTimes(1) + expect(dd).toHaveBeenCalledTimes(1) + }) + + it('should unregister all sequences on unmount', () => { + const { unmount } = renderHook(() => + useHotkeySequences([ + { sequence: ['G', 'G'], callback: vi.fn() }, + { sequence: ['D', 'D'], callback: vi.fn() }, + ]), + ) + + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(2) + unmount() + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(0) + }) + + it('should handle an empty array as a no-op', () => { + renderHook(() => useHotkeySequences([])) + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(0) + }) + + it('should skip definitions with an empty sequence', () => { + renderHook(() => + useHotkeySequences([ + { sequence: [], callback: vi.fn() }, + { sequence: ['G', 'G'], callback: vi.fn() }, + ]), + ) + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(1) + }) + + it('should handle dynamic array changes (add sequence)', () => { + const gg = vi.fn() + const dd = vi.fn() + const yy = vi.fn() + + const { rerender } = renderHook( + ({ defs }: { defs: Array }) => + useHotkeySequences(defs), + { + initialProps: { + defs: [ + { sequence: ['G', 'G'], callback: gg }, + { sequence: ['D', 'D'], callback: dd }, + ], + }, + }, + ) + + dispatchKey('y') + dispatchKey('y') + expect(yy).not.toHaveBeenCalled() + + rerender({ + defs: [ + { sequence: ['G', 'G'], callback: gg }, + { sequence: ['D', 'D'], callback: dd }, + { sequence: ['Y', 'Y'], callback: yy }, + ], + }) + + dispatchKey('y') + dispatchKey('y') + expect(yy).toHaveBeenCalledTimes(1) + }) + + describe('stale closure prevention', () => { + it('should have access to latest state values in callbacks', () => { + const capturedValues: Array = [] + + const { result, rerender } = renderHook(() => { + const [count, setCount] = useState(0) + useHotkeySequences([ + { + sequence: ['G', 'G'], + callback: () => { + capturedValues.push(count) + }, + }, + ]) + return { setCount } + }) + + dispatchKey('g') + dispatchKey('g') + expect(capturedValues).toEqual([0]) + + act(() => { + result.current.setCount(5) + }) + rerender() + + dispatchKey('g') + dispatchKey('g') + expect(capturedValues).toEqual([0, 5]) + + act(() => { + result.current.setCount(10) + }) + rerender() + + dispatchKey('g') + dispatchKey('g') + expect(capturedValues).toEqual([0, 5, 10]) + }) + + it('should sync enabled option on every render', () => { + const callback = vi.fn() + + const { rerender } = renderHook( + ({ enabled }: { enabled: boolean }) => + useHotkeySequences([ + { sequence: ['G', 'G'], callback, options: { enabled } }, + ]), + { initialProps: { enabled: true } }, + ) + + dispatchKey('g') + dispatchKey('g') + expect(callback).toHaveBeenCalledTimes(1) + + rerender({ enabled: false }) + + dispatchKey('g') + dispatchKey('g') + expect(callback).toHaveBeenCalledTimes(1) + + rerender({ enabled: true }) + + dispatchKey('g') + dispatchKey('g') + expect(callback).toHaveBeenCalledTimes(2) + }) + }) +}) diff --git a/packages/solid-hotkeys/src/createHotkeySequences.ts b/packages/solid-hotkeys/src/createHotkeySequences.ts new file mode 100644 index 00000000..6b259117 --- /dev/null +++ b/packages/solid-hotkeys/src/createHotkeySequences.ts @@ -0,0 +1,176 @@ +import { createEffect, onCleanup } from 'solid-js' +import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys' +import { useDefaultHotkeysOptions } from './HotkeysProvider' +import type { CreateHotkeySequenceOptions } from './createHotkeySequence' +import type { + HotkeyCallback, + HotkeySequence, + SequenceRegistrationHandle, +} from '@tanstack/hotkeys' + +/** + * A single sequence definition for use with `createHotkeySequences`. + */ +export interface CreateHotkeySequenceDefinition { + /** Array of hotkey strings that form the sequence */ + sequence: HotkeySequence + /** The function to call when the sequence is completed */ + callback: HotkeyCallback + /** Per-sequence options (merged on top of commonOptions) */ + options?: CreateHotkeySequenceOptions +} + +/** + * SolidJS primitive for registering multiple keyboard shortcut sequences at once (Vim-style). + * + * Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or accessors, + * so you can react to variable-length lists. + * + * Options are merged in this order: + * HotkeysProvider defaults < commonOptions < per-definition options + * + * Definitions with an empty `sequence` are skipped (no registration). + * + * @param sequences - Array of sequence definitions, or accessor returning them + * @param commonOptions - Shared options for all sequences, or accessor + * + * @example + * ```tsx + * function VimPalette() { + * createHotkeySequences([ + * { sequence: ['G', 'G'], callback: () => scrollToTop() }, + * { sequence: ['D', 'D'], callback: () => deleteLine() }, + * ]) + * } + * ``` + * + * @example + * ```tsx + * function Dynamic(props) { + * createHotkeySequences( + * () => props.items.map((item) => ({ + * sequence: item.chords, + * callback: item.action, + * options: { enabled: item.enabled }, + * })), + * { preventDefault: true }, + * ) + * } + * ``` + */ +export function createHotkeySequences( + sequences: + | Array + | (() => Array), + commonOptions: + | CreateHotkeySequenceOptions + | (() => CreateHotkeySequenceOptions) = {}, +): void { + type RegistrationRecord = { + handle: SequenceRegistrationHandle + target: Document | HTMLElement | Window + } + + const defaultOptions = useDefaultHotkeysOptions() + const manager = getSequenceManager() + + const registrations = new Map() + + onCleanup(() => { + for (const { handle } of registrations.values()) { + if (handle.isActive) { + handle.unregister() + } + } + registrations.clear() + }) + + createEffect(() => { + const resolved = typeof sequences === 'function' ? sequences() : sequences + const resolvedCommonOptions = + typeof commonOptions === 'function' ? commonOptions() : commonOptions + + const nextKeys = new Set() + const prepared: Array<{ + registrationKey: string + def: CreateHotkeySequenceDefinition + mergedOptions: CreateHotkeySequenceOptions + sequenceString: string + resolvedTarget: Document | HTMLElement | Window + }> = [] + + for (let i = 0; i < resolved.length; i++) { + const def = resolved[i]! + const resolvedDefOptions = def.options ?? {} + + const mergedOptions = { + ...defaultOptions.hotkeySequence, + ...resolvedCommonOptions, + ...resolvedDefOptions, + } as CreateHotkeySequenceOptions + + if (def.sequence.length === 0) { + continue + } + + const sequenceString = formatHotkeySequence(def.sequence) + + const resolvedTarget = + 'target' in mergedOptions + ? (mergedOptions.target ?? null) + : typeof document !== 'undefined' + ? document + : null + + if (!resolvedTarget) { + continue + } + + const registrationKey = `${i}:${sequenceString}` + nextKeys.add(registrationKey) + prepared.push({ + registrationKey, + def, + mergedOptions, + sequenceString, + resolvedTarget, + }) + } + + for (const [key, record] of [...registrations.entries()]) { + if (!nextKeys.has(key)) { + if (record.handle.isActive) { + record.handle.unregister() + } + registrations.delete(key) + } + } + + for (const p of prepared) { + const existing = registrations.get(p.registrationKey) + if (existing?.handle.isActive && existing.target === p.resolvedTarget) { + existing.handle.callback = p.def.callback + const { target: _target, ...optionsWithoutTarget } = p.mergedOptions + existing.handle.setOptions(optionsWithoutTarget) + continue + } + + if (existing) { + if (existing.handle.isActive) { + existing.handle.unregister() + } + registrations.delete(p.registrationKey) + } + + const { target: _target, ...optionsWithoutTarget } = p.mergedOptions + const handle = manager.register(p.def.sequence, p.def.callback, { + ...optionsWithoutTarget, + target: p.resolvedTarget, + }) + registrations.set(p.registrationKey, { + handle, + target: p.resolvedTarget, + }) + } + }) +} diff --git a/packages/solid-hotkeys/src/index.ts b/packages/solid-hotkeys/src/index.ts index 5c079982..9aa45f70 100644 --- a/packages/solid-hotkeys/src/index.ts +++ b/packages/solid-hotkeys/src/index.ts @@ -11,5 +11,6 @@ export * from './createHeldKeys' export * from './createHeldKeyCodes' export * from './createKeyHold' export * from './createHotkeySequence' +export * from './createHotkeySequences' export * from './createHotkeyRecorder' export * from './createHotkeySequenceRecorder' diff --git a/packages/solid-hotkeys/tests/createHotkeySequences.test.tsx b/packages/solid-hotkeys/tests/createHotkeySequences.test.tsx new file mode 100644 index 00000000..9d5fea5b --- /dev/null +++ b/packages/solid-hotkeys/tests/createHotkeySequences.test.tsx @@ -0,0 +1,132 @@ +// @vitest-environment happy-dom +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' +import { render } from '@solidjs/testing-library' +import { SequenceManager } from '@tanstack/hotkeys' +import { + createHotkeySequences, + type CreateHotkeySequenceDefinition, +} from '../src/createHotkeySequences' +import { createSignal, type Component } from 'solid-js' + +function dispatchKey(key: string) { + document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true })) +} + +describe('createHotkeySequences', () => { + beforeEach(() => { + SequenceManager.resetInstance() + }) + + afterEach(() => { + SequenceManager.resetInstance() + }) + + it('should register multiple sequence handlers', () => { + const a = vi.fn() + const b = vi.fn() + + const TestComponent: Component = () => { + createHotkeySequences([ + { sequence: ['G', 'G'], callback: a }, + { sequence: ['D', 'D'], callback: b }, + ]) + return null + } + + render(() => ) + + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(2) + }) + + it('should call the correct callback for each sequence', () => { + const gg = vi.fn() + const dd = vi.fn() + + const TestComponent: Component = () => { + createHotkeySequences([ + { sequence: ['G', 'G'], callback: gg }, + { sequence: ['D', 'D'], callback: dd }, + ]) + return null + } + + render(() => ) + + dispatchKey('g') + dispatchKey('g') + expect(gg).toHaveBeenCalledTimes(1) + expect(dd).not.toHaveBeenCalled() + + dispatchKey('d') + dispatchKey('d') + expect(gg).toHaveBeenCalledTimes(1) + expect(dd).toHaveBeenCalledTimes(1) + }) + + it('should unregister all sequences on unmount', () => { + const TestComponent: Component = () => { + createHotkeySequences([ + { sequence: ['G', 'G'], callback: vi.fn() }, + { sequence: ['D', 'D'], callback: vi.fn() }, + ]) + return null + } + + const { unmount } = render(() => ) + + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(2) + unmount() + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(0) + }) + + it('should handle an empty array as a no-op', () => { + const TestComponent: Component = () => { + createHotkeySequences([]) + return null + } + + render(() => ) + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(0) + }) + + it('should skip definitions with an empty sequence', () => { + const TestComponent: Component = () => { + createHotkeySequences([ + { sequence: [], callback: vi.fn() }, + { sequence: ['G', 'G'], callback: vi.fn() }, + ]) + return null + } + + render(() => ) + expect(SequenceManager.getInstance().getRegistrationCount()).toBe(1) + }) + + it('should handle dynamic accessor for definitions', () => { + const gg = vi.fn() + const yy = vi.fn() + const [defs, setDefs] = createSignal>( + [{ sequence: ['G', 'G'], callback: gg }], + ) + + const TestComponent: Component = () => { + createHotkeySequences(() => defs()) + return null + } + + render(() => ) + + dispatchKey('y') + dispatchKey('y') + expect(yy).not.toHaveBeenCalled() + + setDefs([ + { sequence: ['G', 'G'], callback: gg }, + { sequence: ['Y', 'Y'], callback: yy }, + ]) + + dispatchKey('y') + dispatchKey('y') + expect(yy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts b/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts new file mode 100644 index 00000000..2b2b849c --- /dev/null +++ b/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts @@ -0,0 +1,274 @@ +import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys' +import { onDestroy } from 'svelte' +import { SvelteMap, SvelteSet } from 'svelte/reactivity' +import { getDefaultHotkeysOptions } from './HotkeysCtx' +import { resolveMaybeGetter } from './internal.svelte' +import type { + HotkeyCallback, + HotkeySequence, + SequenceRegistrationHandle, +} from '@tanstack/hotkeys' +import type { CreateHotkeySequenceOptions } from './createHotkeySequence.svelte' +import type { MaybeGetter } from './internal.svelte' +import type { Attachment } from 'svelte/attachments' + +/** + * A single sequence definition for use with `createHotkeySequences`. + */ +export interface CreateHotkeySequenceDefinition { + /** Array of hotkey strings that form the sequence */ + sequence: MaybeGetter + /** The function to call when the sequence is completed */ + callback: HotkeyCallback + /** Per-sequence options (merged on top of commonOptions) */ + options?: MaybeGetter +} + +type RegistrationRecord = { + handle: SequenceRegistrationHandle + target: Document | HTMLElement | Window +} + +function cleanupRegistrations( + registrations: + | Map + | SvelteMap, +) { + for (const { handle } of registrations.values()) { + if (handle.isActive) { + handle.unregister() + } + } + registrations.clear() +} + +/** + * Register multiple global keyboard shortcut sequences for the current component. + * + * @example + * ```svelte + * + * ``` + */ +export function createHotkeySequences( + definitions: MaybeGetter>, + commonOptions: MaybeGetter = {}, +): void { + const registrations = new SvelteMap() + + onDestroy(() => { + cleanupRegistrations(registrations) + }) + + $effect(() => { + if (typeof document === 'undefined') { + return + } + + const resolvedDefinitions = resolveMaybeGetter(definitions) + const resolvedCommonOptions = resolveMaybeGetter(commonOptions) + const nextKeys = new SvelteSet() + const prepared: Array<{ + registrationKey: string + def: CreateHotkeySequenceDefinition + mergedOptions: CreateHotkeySequenceOptions + sequenceString: string + resolvedSequence: HotkeySequence + resolvedTarget: Document | HTMLElement | Window + }> = [] + + for (let i = 0; i < resolvedDefinitions.length; i++) { + const def = resolvedDefinitions[i]! + const resolvedSequence = resolveMaybeGetter(def.sequence) + if (resolvedSequence.length === 0) { + continue + } + + const resolvedDefOptions = def.options + ? resolveMaybeGetter(def.options) + : {} + + const mergedOptions = { + ...getDefaultHotkeysOptions().hotkeySequence, + ...resolvedCommonOptions, + ...resolvedDefOptions, + } as CreateHotkeySequenceOptions + + const resolvedTarget = + mergedOptions.target ?? + (typeof document !== 'undefined' ? document : null) + + if (!resolvedTarget) { + continue + } + + const sequenceString = formatHotkeySequence(resolvedSequence) + const registrationKey = `${i}:${sequenceString}` + nextKeys.add(registrationKey) + prepared.push({ + registrationKey, + def, + mergedOptions, + sequenceString, + resolvedSequence, + resolvedTarget, + }) + } + + for (const [key, record] of [...registrations.entries()]) { + if (!nextKeys.has(key)) { + if (record.handle.isActive) { + record.handle.unregister() + } + registrations.delete(key) + } + } + + for (const p of prepared) { + const existing = registrations.get(p.registrationKey) + const { target: _target, ...optionsWithoutTarget } = p.mergedOptions + + if (existing?.handle.isActive && existing.target === p.resolvedTarget) { + existing.handle.callback = p.def.callback + existing.handle.setOptions(optionsWithoutTarget) + continue + } + + if (existing?.handle.isActive) { + existing.handle.unregister() + } + if (existing) { + registrations.delete(p.registrationKey) + } + + const handle = getSequenceManager().register( + p.resolvedSequence, + p.def.callback, + { + ...optionsWithoutTarget, + target: p.resolvedTarget, + }, + ) + registrations.set(p.registrationKey, { + handle, + target: p.resolvedTarget, + }) + } + }) +} + +/** + * Create an attachment for element-scoped multi-sequence registration. + * + * @example + * ```svelte + * + * + *
Editor
+ * ``` + */ +export function createHotkeySequencesAttachment( + definitions: MaybeGetter>, + commonOptions: MaybeGetter = {}, +): Attachment { + return (element) => { + const registrations = new SvelteMap() + + $effect(() => { + const resolvedDefinitions = resolveMaybeGetter(definitions) + const resolvedCommonOptions = resolveMaybeGetter(commonOptions) + const nextKeys = new SvelteSet() + const prepared: Array<{ + registrationKey: string + def: CreateHotkeySequenceDefinition + mergedOptions: CreateHotkeySequenceOptions + sequenceString: string + resolvedSequence: HotkeySequence + }> = [] + + for (let i = 0; i < resolvedDefinitions.length; i++) { + const def = resolvedDefinitions[i]! + const resolvedSequence = resolveMaybeGetter(def.sequence) + if (resolvedSequence.length === 0) { + continue + } + + const resolvedDefOptions = def.options + ? resolveMaybeGetter(def.options) + : {} + + const mergedOptions = { + ...getDefaultHotkeysOptions().hotkeySequence, + ...resolvedCommonOptions, + ...resolvedDefOptions, + } as CreateHotkeySequenceOptions + + const sequenceString = formatHotkeySequence(resolvedSequence) + const registrationKey = `${i}:${sequenceString}` + nextKeys.add(registrationKey) + prepared.push({ + registrationKey, + def, + mergedOptions, + sequenceString, + resolvedSequence, + }) + } + + for (const [key, record] of [...registrations.entries()]) { + if (!nextKeys.has(key)) { + if (record.handle.isActive) { + record.handle.unregister() + } + registrations.delete(key) + } + } + + for (const p of prepared) { + const existing = registrations.get(p.registrationKey) + const { target: _target, ...optionsWithoutTarget } = p.mergedOptions + + if (existing?.handle.isActive && existing.target === element) { + existing.handle.callback = p.def.callback + existing.handle.setOptions(optionsWithoutTarget) + continue + } + + if (existing?.handle.isActive) { + existing.handle.unregister() + } + if (existing) { + registrations.delete(p.registrationKey) + } + + const handle = getSequenceManager().register( + p.resolvedSequence, + p.def.callback, + { + ...optionsWithoutTarget, + target: element, + }, + ) + registrations.set(p.registrationKey, { handle, target: element }) + } + }) + + return () => { + cleanupRegistrations(registrations) + } + } +} diff --git a/packages/svelte-hotkeys/src/index.ts b/packages/svelte-hotkeys/src/index.ts index b5ce4329..93b44be4 100644 --- a/packages/svelte-hotkeys/src/index.ts +++ b/packages/svelte-hotkeys/src/index.ts @@ -4,6 +4,7 @@ export * from '@tanstack/hotkeys' export * from './createHotkey.svelte' export * from './createHotkeys.svelte' export * from './createHotkeySequence.svelte' +export * from './createHotkeySequences.svelte' export * from './createHotkeyRecorder.svelte' export * from './createHotkeySequenceRecorder.svelte' export * from './getHeldKeys.svelte' diff --git a/packages/vue-hotkeys/src/index.ts b/packages/vue-hotkeys/src/index.ts index faf548e8..37a24c14 100644 --- a/packages/vue-hotkeys/src/index.ts +++ b/packages/vue-hotkeys/src/index.ts @@ -12,5 +12,6 @@ export * from './useHeldKeys' export * from './useHeldKeyCodes' export * from './useKeyHold' export * from './useHotkeySequence' +export * from './useHotkeySequences' export * from './useHotkeyRecorder' export * from './useHotkeySequenceRecorder' diff --git a/packages/vue-hotkeys/src/useHotkeySequences.ts b/packages/vue-hotkeys/src/useHotkeySequences.ts new file mode 100644 index 00000000..4c746f50 --- /dev/null +++ b/packages/vue-hotkeys/src/useHotkeySequences.ts @@ -0,0 +1,221 @@ +import { onUnmounted, unref, watch } from 'vue' +import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys' +import { useDefaultHotkeysOptions } from './HotkeysProviderContext' +import type { UseHotkeySequenceOptions } from './useHotkeySequence' +import type { + HotkeyCallback, + HotkeySequence, + SequenceRegistrationHandle, +} from '@tanstack/hotkeys' +import type { MaybeRefOrGetter } from 'vue' + +/** + * A single sequence definition for use with `useHotkeySequences`. + */ +export interface UseHotkeySequenceDefinition { + /** Array of hotkey strings that form the sequence */ + sequence: MaybeRefOrGetter + /** The function to call when the sequence is completed */ + callback: HotkeyCallback + /** Per-sequence options (merged on top of commonOptions) */ + options?: MaybeRefOrGetter +} + +/** + * Vue composable for registering multiple keyboard shortcut sequences at once (Vim-style). + * + * Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or a getter/ref + * that returns one, so you can register variable-length lists safely. + * + * Options are merged in this order: + * HotkeysProvider defaults < commonOptions < per-definition options + * + * Definitions with an empty `sequence` are skipped (no registration). + * + * @param definitions - Array of sequence definitions, or a getter/ref + * @param commonOptions - Shared options applied to all sequences (overridden by per-definition options) + * + * @example + * ```vue + * + * ``` + * + * @example + * ```vue + * + * ``` + */ +export function useHotkeySequences( + definitions: MaybeRefOrGetter>, + commonOptions: MaybeRefOrGetter = {}, +): void { + type RegistrationRecord = { + handle: SequenceRegistrationHandle + target: Document | HTMLElement | Window + } + + const defaultOptions = useDefaultHotkeysOptions() + const manager = getSequenceManager() + + const registrations = new Map() + + const stopWatcher = watch( + () => { + const resolvedDefinitions = resolveMaybeRefOrGetter(definitions) + const resolvedCommonOptions = resolveMaybeRefOrGetter(commonOptions) + + return resolvedDefinitions.map((def, i) => { + const resolvedSequence = resolveMaybeRefOrGetter(def.sequence) + const resolvedDefOptions = def.options + ? resolveMaybeRefOrGetter(def.options) + : {} + + const mergedOptions = { + ...defaultOptions.hotkeySequence, + ...resolvedCommonOptions, + ...resolvedDefOptions, + } as UseHotkeySequenceOptions + + const sequenceString = formatHotkeySequence(resolvedSequence) + + const resolvedEnabled = + mergedOptions.enabled === undefined + ? undefined + : resolveMaybeRefOrGetter(mergedOptions.enabled) + const resolvedTarget = + mergedOptions.target === undefined + ? undefined + : resolveMaybeRefOrGetter(mergedOptions.target as any) + + return { + index: i, + resolvedSequence, + sequenceString, + callback: def.callback, + mergedOptions, + resolvedEnabled, + resolvedTarget, + } + }) + }, + (resolved) => { + const nextKeys = new Set() + const prepared: Array<{ + registrationKey: string + entry: (typeof resolved)[number] + finalTarget: Document | HTMLElement | Window + }> = [] + + for (const entry of resolved) { + if (entry.resolvedSequence.length === 0) { + continue + } + + const finalTarget = + entry.resolvedTarget ?? + (typeof document !== 'undefined' ? document : null) + + if (!finalTarget) { + continue + } + + const registrationKey = `${entry.index}:${entry.sequenceString}` + nextKeys.add(registrationKey) + prepared.push({ registrationKey, entry, finalTarget }) + } + + for (const [key, record] of [...registrations.entries()]) { + if (!nextKeys.has(key)) { + if (record.handle.isActive) { + record.handle.unregister() + } + registrations.delete(key) + } + } + + for (const { registrationKey, entry, finalTarget } of prepared) { + const existing = registrations.get(registrationKey) + if (existing?.handle.isActive && existing.target === finalTarget) { + existing.handle.callback = entry.callback + const { + target: _target, + enabled: _enabled, + ...restOptions + } = entry.mergedOptions + const optionsWithoutTarget = { + ...restOptions, + ...(entry.resolvedEnabled === undefined + ? {} + : { enabled: entry.resolvedEnabled }), + } + existing.handle.setOptions(optionsWithoutTarget) + continue + } + + if (existing) { + if (existing.handle.isActive) { + existing.handle.unregister() + } + registrations.delete(registrationKey) + } + + const { + target: _target, + enabled: _enabled, + ...restOptions + } = entry.mergedOptions + const optionsWithoutTarget = { + ...restOptions, + ...(entry.resolvedEnabled === undefined + ? {} + : { enabled: entry.resolvedEnabled }), + } + + const handle = manager.register( + entry.resolvedSequence, + entry.callback, + { + ...optionsWithoutTarget, + target: finalTarget, + }, + ) + registrations.set(registrationKey, { handle, target: finalTarget }) + } + }, + { immediate: true }, + ) + + onUnmounted(() => { + stopWatcher() + for (const { handle } of registrations.values()) { + if (handle.isActive) { + handle.unregister() + } + } + registrations.clear() + }) +} + +function resolveMaybeRefOrGetter(value: MaybeRefOrGetter): T { + return typeof value === 'function' ? (value as () => T)() : unref(value) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fc159a9..d5db5ac8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -431,6 +431,76 @@ importers: specifier: 5.9.3 version: 5.9.3 + examples/angular/injectHotkeySequences: + dependencies: + '@angular/common': + specifier: ^21.2.5 + version: 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/compiler': + specifier: ^21.2.5 + version: 21.2.5 + '@angular/core': + specifier: ^21.2.5 + version: 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/forms': + specifier: ^21.2.5 + version: 21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/platform-browser': + specifier: ^21.2.5 + version: 21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-browser-dynamic': + specifier: ^21.2.5 + version: 21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.5)(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))) + '@angular/router': + specifier: ^21.2.5 + version: 21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@tanstack/angular-hotkeys': + specifier: ^0.6.0 + version: link:../../../packages/angular-hotkeys + rxjs: + specifier: ~7.8.2 + version: 7.8.2 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + zone.js: + specifier: ~0.16.1 + version: 0.16.1 + devDependencies: + '@angular-devkit/build-angular': + specifier: ^21.2.3 + version: 21.2.3(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3))(@angular/compiler@21.2.5)(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.5.0)(chokidar@5.0.0)(jiti@2.6.1)(karma@6.4.4)(lightningcss@1.32.0)(typescript@5.9.3)(vitest@4.1.0(@types/node@25.5.0)(happy-dom@20.8.4)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)))(yaml@2.8.2) + '@angular/cli': + specifier: ^21.2.3 + version: 21.2.3(@types/node@25.5.0)(chokidar@5.0.0) + '@angular/compiler-cli': + specifier: ^21.2.5 + version: 21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3) + '@types/jasmine': + specifier: ~6.0.0 + version: 6.0.0 + jasmine-core: + specifier: ~6.1.0 + version: 6.1.0 + karma: + specifier: ~6.4.4 + version: 6.4.4 + karma-chrome-launcher: + specifier: ~3.2.0 + version: 3.2.0 + karma-coverage: + specifier: ~2.2.1 + version: 2.2.1 + karma-jasmine: + specifier: ~5.1.0 + version: 5.1.0(karma@6.4.4) + karma-jasmine-html-reporter: + specifier: ~2.2.0 + version: 2.2.0(jasmine-core@6.1.0)(karma-jasmine@5.1.0(karma@6.4.4))(karma@6.4.4) + typescript: + specifier: 5.9.3 + version: 5.9.3 + examples/angular/injectHotkeys: dependencies: '@angular/common': @@ -696,6 +766,31 @@ importers: specifier: ^8.0.1 version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) + examples/preact/useHotkeySequences: + dependencies: + '@tanstack/preact-hotkeys': + specifier: ^0.6.0 + version: link:../../../packages/preact-hotkeys + preact: + specifier: ^10.29.0 + version: 10.29.0 + devDependencies: + '@preact/preset-vite': + specifier: ^2.10.5 + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.57.1)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) + '@tanstack/preact-devtools': + specifier: 0.10.0 + version: 0.10.0(csstype@3.2.3)(preact@10.29.0)(solid-js@1.9.11) + '@tanstack/preact-hotkeys-devtools': + specifier: ^0.5.0 + version: link:../../../packages/preact-hotkeys-devtools + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: ^8.0.1 + version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) + examples/preact/useHotkeys: dependencies: '@tanstack/preact-hotkeys': @@ -916,6 +1011,40 @@ importers: specifier: ^8.0.1 version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) + examples/react/useHotkeySequences: + dependencies: + '@tanstack/react-hotkeys': + specifier: ^0.6.0 + version: link:../../../packages/react-hotkeys + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + devDependencies: + '@tanstack/react-devtools': + specifier: 0.10.0 + version: 0.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11) + '@tanstack/react-hotkeys-devtools': + specifier: ^0.5.0 + version: link:../../../packages/react-hotkeys-devtools + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: ^8.0.1 + version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) + examples/react/useHotkeys: dependencies: '@tanstack/react-hotkeys': @@ -1100,6 +1229,28 @@ importers: specifier: ^2.11.11 version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) + examples/solid/createHotkeySequences: + dependencies: + '@tanstack/solid-devtools': + specifier: 0.8.0 + version: 0.8.0(csstype@3.2.3)(solid-js@1.9.11) + '@tanstack/solid-hotkeys': + specifier: ^0.6.0 + version: link:../../../packages/solid-hotkeys + '@tanstack/solid-hotkeys-devtools': + specifier: ^0.5.0 + version: link:../../../packages/solid-hotkeys-devtools + solid-js: + specifier: ^1.9.11 + version: 1.9.11 + devDependencies: + vite: + specifier: ^8.0.1 + version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) + vite-plugin-solid: + specifier: ^2.11.11 + version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) + examples/solid/createHotkeys: dependencies: '@tanstack/solid-devtools': @@ -1226,6 +1377,25 @@ importers: specifier: ^8.0.1 version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) + examples/svelte/create-hotkey-sequences: + dependencies: + '@tanstack/svelte-hotkeys': + specifier: 0.6.0 + version: link:../../../packages/svelte-hotkeys + svelte: + specifier: ^5.54.1 + version: 5.54.1 + devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: ^7.0.0 + version: 7.0.0(svelte@5.54.1)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: ^8.0.1 + version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) + examples/svelte/create-hotkeys: dependencies: '@tanstack/svelte-hotkeys': @@ -1408,6 +1578,31 @@ importers: specifier: ^8.0.1 version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) + examples/vue/useHotkeySequences: + dependencies: + '@tanstack/vue-hotkeys': + specifier: ^0.6.0 + version: link:../../../packages/vue-hotkeys + vue: + specifier: ^3.5.30 + version: 3.5.30(typescript@5.9.3) + devDependencies: + '@tanstack/vue-devtools': + specifier: ^0.2.14 + version: 0.2.14(csstype@3.2.3)(solid-js@1.9.11) + '@tanstack/vue-hotkeys-devtools': + specifier: ^0.5.0 + version: link:../../../packages/vue-hotkeys-devtools + '@vitejs/plugin-vue': + specifier: ^6.0.5 + version: 6.0.5(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)) + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: ^8.0.1 + version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) + examples/vue/useHotkeys: dependencies: '@tanstack/vue-hotkeys': From 4767bbb2cf6d3160b1a2a72df51b92a468d54414 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Wed, 25 Mar 2026 08:13:14 -0500 Subject: [PATCH 2/4] address pr feedback --- .changeset/plural-sequences.md | 12 ++++++------ docs/framework/angular/guides/sequences.md | 1 + .../reference/functions/injectHotkeySequences.md | 2 +- .../interfaces/InjectHotkeySequenceDefinition.md | 8 ++++---- .../preact/reference/functions/useHotkeySequences.md | 2 +- .../interfaces/UseHotkeySequenceDefinition.md | 8 ++++---- .../react/reference/functions/useHotkeySequences.md | 2 +- .../interfaces/UseHotkeySequenceDefinition.md | 8 ++++---- .../reference/functions/createHotkeySequences.md | 2 +- .../interfaces/CreateHotkeySequenceDefinition.md | 8 ++++---- .../reference/functions/createHotkeySequences.md | 2 +- .../functions/createHotkeySequencesAttachment.md | 2 +- .../interfaces/CreateHotkeySequenceDefinition.md | 8 ++++---- .../vue/reference/functions/useHotkeySequences.md | 2 +- .../interfaces/UseHotkeySequenceDefinition.md | 8 ++++---- .../injectHotkeySequences/src/app/app.component.html | 4 +++- .../injectHotkeySequences/src/app/app.component.ts | 2 +- .../injectHotkeySequences/src/app/app.config.ts | 8 ++++++-- .../angular/injectHotkeySequences/src/styles.css | 1 - examples/preact/useHotkeySequences/src/index.css | 1 - examples/react/useHotkeySequences/src/index.css | 1 - examples/solid/createHotkeySequences/src/index.css | 1 - examples/solid/createHotkeySequences/src/index.tsx | 4 ++-- examples/svelte/create-hotkey-sequences/README.md | 2 +- .../svelte/create-hotkey-sequences/src/index.css | 1 - examples/vue/useHotkeySequences/src/index.css | 1 - .../tests/createHotkeySequences.test.tsx | 11 +++++------ 27 files changed, 56 insertions(+), 56 deletions(-) diff --git a/.changeset/plural-sequences.md b/.changeset/plural-sequences.md index de87c91f..3bfa61e6 100644 --- a/.changeset/plural-sequences.md +++ b/.changeset/plural-sequences.md @@ -1,10 +1,10 @@ --- -"@tanstack/react-hotkeys": minor -"@tanstack/preact-hotkeys": minor -"@tanstack/vue-hotkeys": minor -"@tanstack/solid-hotkeys": minor -"@tanstack/svelte-hotkeys": minor -"@tanstack/angular-hotkeys": minor +'@tanstack/react-hotkeys': minor +'@tanstack/preact-hotkeys': minor +'@tanstack/vue-hotkeys': minor +'@tanstack/solid-hotkeys': minor +'@tanstack/svelte-hotkeys': minor +'@tanstack/angular-hotkeys': minor --- Add plural sequence registration APIs: `useHotkeySequences` (React/Preact/Vue), `createHotkeySequences` and `createHotkeySequencesAttachment` (Svelte), `createHotkeySequences` (Solid), and `injectHotkeySequences` (Angular). diff --git a/docs/framework/angular/guides/sequences.md b/docs/framework/angular/guides/sequences.md index 6ea60391..1e1702b5 100644 --- a/docs/framework/angular/guides/sequences.md +++ b/docs/framework/angular/guides/sequences.md @@ -26,6 +26,7 @@ export class AppComponent { Use `injectHotkeySequences` when you want several sequences (or a list built from data) in one injection context, instead of many `injectHotkeySequence` calls. ```ts +import { Component } from '@angular/core' import { injectHotkeySequences } from '@tanstack/angular-hotkeys' @Component({ standalone: true, template: `` }) diff --git a/docs/framework/angular/reference/functions/injectHotkeySequences.md b/docs/framework/angular/reference/functions/injectHotkeySequences.md index aa689927..29b768ed 100644 --- a/docs/framework/angular/reference/functions/injectHotkeySequences.md +++ b/docs/framework/angular/reference/functions/injectHotkeySequences.md @@ -9,7 +9,7 @@ title: injectHotkeySequences function injectHotkeySequences(sequences, commonOptions): void; ``` -Defined in: injectHotkeySequences.ts:50 +Defined in: [injectHotkeySequences.ts:50](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequences.ts#L50) Angular inject-based API for registering multiple keyboard shortcut sequences at once (Vim-style). diff --git a/docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md b/docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md index 153345b5..77e35da7 100644 --- a/docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md +++ b/docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md @@ -5,7 +5,7 @@ title: InjectHotkeySequenceDefinition # Interface: InjectHotkeySequenceDefinition -Defined in: injectHotkeySequences.ts:14 +Defined in: [injectHotkeySequences.ts:14](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequences.ts#L14) A single sequence definition for use with `injectHotkeySequences`. @@ -17,7 +17,7 @@ A single sequence definition for use with `injectHotkeySequences`. callback: HotkeyCallback; ``` -Defined in: injectHotkeySequences.ts:18 +Defined in: [injectHotkeySequences.ts:18](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequences.ts#L18) The function to call when the sequence is completed @@ -31,7 +31,7 @@ optional options: | () => InjectHotkeySequenceOptions; ``` -Defined in: injectHotkeySequences.ts:20 +Defined in: [injectHotkeySequences.ts:20](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequences.ts#L20) Per-sequence options (merged on top of commonOptions) @@ -43,6 +43,6 @@ Per-sequence options (merged on top of commonOptions) sequence: HotkeySequence | () => HotkeySequence; ``` -Defined in: injectHotkeySequences.ts:16 +Defined in: [injectHotkeySequences.ts:16](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequences.ts#L16) Array of hotkey strings that form the sequence diff --git a/docs/framework/preact/reference/functions/useHotkeySequences.md b/docs/framework/preact/reference/functions/useHotkeySequences.md index 6868f74e..6610956e 100644 --- a/docs/framework/preact/reference/functions/useHotkeySequences.md +++ b/docs/framework/preact/reference/functions/useHotkeySequences.md @@ -9,7 +9,7 @@ title: useHotkeySequences function useHotkeySequences(definitions, commonOptions): void; ``` -Defined in: useHotkeySequences.ts:65 +Defined in: [useHotkeySequences.ts:65](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequences.ts#L65) Preact hook for registering multiple keyboard shortcut sequences at once (Vim-style). diff --git a/docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md b/docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md index 59536414..9534c315 100644 --- a/docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md +++ b/docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md @@ -5,7 +5,7 @@ title: UseHotkeySequenceDefinition # Interface: UseHotkeySequenceDefinition -Defined in: useHotkeySequences.ts:15 +Defined in: [useHotkeySequences.ts:15](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequences.ts#L15) A single sequence definition for use with `useHotkeySequences`. @@ -17,7 +17,7 @@ A single sequence definition for use with `useHotkeySequences`. callback: HotkeyCallback; ``` -Defined in: useHotkeySequences.ts:19 +Defined in: [useHotkeySequences.ts:19](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequences.ts#L19) The function to call when the sequence is completed @@ -29,7 +29,7 @@ The function to call when the sequence is completed optional options: UseHotkeySequenceOptions; ``` -Defined in: useHotkeySequences.ts:21 +Defined in: [useHotkeySequences.ts:21](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequences.ts#L21) Per-sequence options (merged on top of commonOptions) @@ -41,6 +41,6 @@ Per-sequence options (merged on top of commonOptions) sequence: HotkeySequence; ``` -Defined in: useHotkeySequences.ts:17 +Defined in: [useHotkeySequences.ts:17](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequences.ts#L17) Array of hotkey strings that form the sequence diff --git a/docs/framework/react/reference/functions/useHotkeySequences.md b/docs/framework/react/reference/functions/useHotkeySequences.md index 61fa0575..2f0ef16b 100644 --- a/docs/framework/react/reference/functions/useHotkeySequences.md +++ b/docs/framework/react/reference/functions/useHotkeySequences.md @@ -9,7 +9,7 @@ title: useHotkeySequences function useHotkeySequences(definitions, commonOptions): void; ``` -Defined in: useHotkeySequences.ts:65 +Defined in: [useHotkeySequences.ts:65](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequences.ts#L65) React hook for registering multiple keyboard shortcut sequences at once (Vim-style). diff --git a/docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md b/docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md index 59536414..7965ef8d 100644 --- a/docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md +++ b/docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md @@ -5,7 +5,7 @@ title: UseHotkeySequenceDefinition # Interface: UseHotkeySequenceDefinition -Defined in: useHotkeySequences.ts:15 +Defined in: [useHotkeySequences.ts:15](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequences.ts#L15) A single sequence definition for use with `useHotkeySequences`. @@ -17,7 +17,7 @@ A single sequence definition for use with `useHotkeySequences`. callback: HotkeyCallback; ``` -Defined in: useHotkeySequences.ts:19 +Defined in: [useHotkeySequences.ts:19](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequences.ts#L19) The function to call when the sequence is completed @@ -29,7 +29,7 @@ The function to call when the sequence is completed optional options: UseHotkeySequenceOptions; ``` -Defined in: useHotkeySequences.ts:21 +Defined in: [useHotkeySequences.ts:21](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequences.ts#L21) Per-sequence options (merged on top of commonOptions) @@ -41,6 +41,6 @@ Per-sequence options (merged on top of commonOptions) sequence: HotkeySequence; ``` -Defined in: useHotkeySequences.ts:17 +Defined in: [useHotkeySequences.ts:17](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequences.ts#L17) Array of hotkey strings that form the sequence diff --git a/docs/framework/solid/reference/functions/createHotkeySequences.md b/docs/framework/solid/reference/functions/createHotkeySequences.md index b69fd8e3..e92fd09a 100644 --- a/docs/framework/solid/reference/functions/createHotkeySequences.md +++ b/docs/framework/solid/reference/functions/createHotkeySequences.md @@ -9,7 +9,7 @@ title: createHotkeySequences function createHotkeySequences(sequences, commonOptions): void; ``` -Defined in: createHotkeySequences.ts:61 +Defined in: [createHotkeySequences.ts:61](https://github.com/TanStack/hotkeys/blob/main/packages/solid-hotkeys/src/createHotkeySequences.ts#L61) SolidJS primitive for registering multiple keyboard shortcut sequences at once (Vim-style). diff --git a/docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md b/docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md index d0d2c8ab..d5d4ab66 100644 --- a/docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md +++ b/docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md @@ -5,7 +5,7 @@ title: CreateHotkeySequenceDefinition # Interface: CreateHotkeySequenceDefinition -Defined in: createHotkeySequences.ts:14 +Defined in: [createHotkeySequences.ts:14](https://github.com/TanStack/hotkeys/blob/main/packages/solid-hotkeys/src/createHotkeySequences.ts#L14) A single sequence definition for use with `createHotkeySequences`. @@ -17,7 +17,7 @@ A single sequence definition for use with `createHotkeySequences`. callback: HotkeyCallback; ``` -Defined in: createHotkeySequences.ts:18 +Defined in: [createHotkeySequences.ts:18](https://github.com/TanStack/hotkeys/blob/main/packages/solid-hotkeys/src/createHotkeySequences.ts#L18) The function to call when the sequence is completed @@ -29,7 +29,7 @@ The function to call when the sequence is completed optional options: CreateHotkeySequenceOptions; ``` -Defined in: createHotkeySequences.ts:20 +Defined in: [createHotkeySequences.ts:20](https://github.com/TanStack/hotkeys/blob/main/packages/solid-hotkeys/src/createHotkeySequences.ts#L20) Per-sequence options (merged on top of commonOptions) @@ -41,6 +41,6 @@ Per-sequence options (merged on top of commonOptions) sequence: HotkeySequence; ``` -Defined in: createHotkeySequences.ts:16 +Defined in: [createHotkeySequences.ts:16](https://github.com/TanStack/hotkeys/blob/main/packages/solid-hotkeys/src/createHotkeySequences.ts#L16) Array of hotkey strings that form the sequence diff --git a/docs/framework/svelte/reference/functions/createHotkeySequences.md b/docs/framework/svelte/reference/functions/createHotkeySequences.md index 1b03c3d5..503f12c8 100644 --- a/docs/framework/svelte/reference/functions/createHotkeySequences.md +++ b/docs/framework/svelte/reference/functions/createHotkeySequences.md @@ -9,7 +9,7 @@ title: createHotkeySequences function createHotkeySequences(definitions, commonOptions): void; ``` -Defined in: packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:60 +Defined in: [packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:60](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts#L60) Register multiple global keyboard shortcut sequences for the current component. diff --git a/docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md b/docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md index 7954a1c7..d843bb45 100644 --- a/docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md +++ b/docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md @@ -9,7 +9,7 @@ title: createHotkeySequencesAttachment function createHotkeySequencesAttachment(definitions, commonOptions): Attachment; ``` -Defined in: packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:184 +Defined in: [packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:184](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts#L184) Create an attachment for element-scoped multi-sequence registration. diff --git a/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md b/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md index 983683e7..7347a8b9 100644 --- a/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md +++ b/docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md @@ -5,7 +5,7 @@ title: CreateHotkeySequenceDefinition # Interface: CreateHotkeySequenceDefinition -Defined in: packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:18 +Defined in: [packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:18](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts#L18) A single sequence definition for use with `createHotkeySequences`. @@ -17,7 +17,7 @@ A single sequence definition for use with `createHotkeySequences`. callback: HotkeyCallback; ``` -Defined in: packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:22 +Defined in: [packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:22](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts#L22) The function to call when the sequence is completed @@ -29,7 +29,7 @@ The function to call when the sequence is completed optional options: MaybeGetter; ``` -Defined in: packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:24 +Defined in: [packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:24](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts#L24) Per-sequence options (merged on top of commonOptions) @@ -41,6 +41,6 @@ Per-sequence options (merged on top of commonOptions) sequence: MaybeGetter; ``` -Defined in: packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:20 +Defined in: [packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts:20](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts#L20) Array of hotkey strings that form the sequence diff --git a/docs/framework/vue/reference/functions/useHotkeySequences.md b/docs/framework/vue/reference/functions/useHotkeySequences.md index ff155891..81d75a4f 100644 --- a/docs/framework/vue/reference/functions/useHotkeySequences.md +++ b/docs/framework/vue/reference/functions/useHotkeySequences.md @@ -9,7 +9,7 @@ title: useHotkeySequences function useHotkeySequences(definitions, commonOptions): void; ``` -Defined in: packages/vue-hotkeys/src/useHotkeySequences.ts:68 +Defined in: [packages/vue-hotkeys/src/useHotkeySequences.ts:68](https://github.com/TanStack/hotkeys/blob/main/packages/vue-hotkeys/src/useHotkeySequences.ts#L68) Vue composable for registering multiple keyboard shortcut sequences at once (Vim-style). diff --git a/docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md b/docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md index 26aa302d..669d1ead 100644 --- a/docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md +++ b/docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md @@ -5,7 +5,7 @@ title: UseHotkeySequenceDefinition # Interface: UseHotkeySequenceDefinition -Defined in: packages/vue-hotkeys/src/useHotkeySequences.ts:15 +Defined in: [packages/vue-hotkeys/src/useHotkeySequences.ts:15](https://github.com/TanStack/hotkeys/blob/main/packages/vue-hotkeys/src/useHotkeySequences.ts#L15) A single sequence definition for use with `useHotkeySequences`. @@ -17,7 +17,7 @@ A single sequence definition for use with `useHotkeySequences`. callback: HotkeyCallback; ``` -Defined in: packages/vue-hotkeys/src/useHotkeySequences.ts:19 +Defined in: [packages/vue-hotkeys/src/useHotkeySequences.ts:19](https://github.com/TanStack/hotkeys/blob/main/packages/vue-hotkeys/src/useHotkeySequences.ts#L19) The function to call when the sequence is completed @@ -29,7 +29,7 @@ The function to call when the sequence is completed optional options: MaybeRefOrGetter; ``` -Defined in: packages/vue-hotkeys/src/useHotkeySequences.ts:21 +Defined in: [packages/vue-hotkeys/src/useHotkeySequences.ts:21](https://github.com/TanStack/hotkeys/blob/main/packages/vue-hotkeys/src/useHotkeySequences.ts#L21) Per-sequence options (merged on top of commonOptions) @@ -41,6 +41,6 @@ Per-sequence options (merged on top of commonOptions) sequence: MaybeRefOrGetter; ``` -Defined in: packages/vue-hotkeys/src/useHotkeySequences.ts:17 +Defined in: [packages/vue-hotkeys/src/useHotkeySequences.ts:17](https://github.com/TanStack/hotkeys/blob/main/packages/vue-hotkeys/src/useHotkeySequences.ts#L17) Array of hotkey strings that form the sequence diff --git a/examples/angular/injectHotkeySequences/src/app/app.component.html b/examples/angular/injectHotkeySequences/src/app/app.component.html index 2fc04845..1138c614 100644 --- a/examples/angular/injectHotkeySequences/src/app/app.component.html +++ b/examples/angular/injectHotkeySequences/src/app/app.component.html @@ -70,7 +70,9 @@

Spell It Out

@if (lastSequence(); as seq) { -
Triggered: {{ seq }}
+
+ Triggered: {{ seq }} +
}
diff --git a/examples/angular/injectHotkeySequences/src/app/app.component.ts b/examples/angular/injectHotkeySequences/src/app/app.component.ts index 3454cc28..4a27c81b 100644 --- a/examples/angular/injectHotkeySequences/src/app/app.component.ts +++ b/examples/angular/injectHotkeySequences/src/app/app.component.ts @@ -9,7 +9,7 @@ import { injectHotkey, injectHotkeySequences } from '@tanstack/angular-hotkeys' }) export class AppComponent { lastSequence = signal(null) - history = signal([]) + history = signal>([]) constructor() { const addToHistory = (action: string) => { diff --git a/examples/angular/injectHotkeySequences/src/app/app.config.ts b/examples/angular/injectHotkeySequences/src/app/app.config.ts index 0a966a4a..9a7d7e90 100644 --- a/examples/angular/injectHotkeySequences/src/app/app.config.ts +++ b/examples/angular/injectHotkeySequences/src/app/app.config.ts @@ -1,6 +1,10 @@ -import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core' +import type { ApplicationConfig } from '@angular/core' +import { provideZoneChangeDetection } from '@angular/core' import { provideHotkeys } from '@tanstack/angular-hotkeys' export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true })], + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideHotkeys({}), + ], } diff --git a/examples/angular/injectHotkeySequences/src/styles.css b/examples/angular/injectHotkeySequences/src/styles.css index 3a0fd933..b784c2b0 100644 --- a/examples/angular/injectHotkeySequences/src/styles.css +++ b/examples/angular/injectHotkeySequences/src/styles.css @@ -21,7 +21,6 @@ header h1 { } header p { color: #666; - margin: 0; max-width: 500px; margin: 0 auto; } diff --git a/examples/preact/useHotkeySequences/src/index.css b/examples/preact/useHotkeySequences/src/index.css index 0fe2ade8..3f8c07d7 100644 --- a/examples/preact/useHotkeySequences/src/index.css +++ b/examples/preact/useHotkeySequences/src/index.css @@ -25,7 +25,6 @@ header h1 { } header p { color: #666; - margin: 0; max-width: 500px; margin: 0 auto; } diff --git a/examples/react/useHotkeySequences/src/index.css b/examples/react/useHotkeySequences/src/index.css index 69b749be..c7ed3747 100644 --- a/examples/react/useHotkeySequences/src/index.css +++ b/examples/react/useHotkeySequences/src/index.css @@ -25,7 +25,6 @@ header h1 { } header p { color: #666; - margin: 0; max-width: 500px; margin: 0 auto; } diff --git a/examples/solid/createHotkeySequences/src/index.css b/examples/solid/createHotkeySequences/src/index.css index bfce1a5b..68d1d06e 100644 --- a/examples/solid/createHotkeySequences/src/index.css +++ b/examples/solid/createHotkeySequences/src/index.css @@ -21,7 +21,6 @@ header h1 { } header p { color: #666; - margin: 0; max-width: 500px; margin: 0 auto; } diff --git a/examples/solid/createHotkeySequences/src/index.tsx b/examples/solid/createHotkeySequences/src/index.tsx index dc7c60cb..4d055e5d 100644 --- a/examples/solid/createHotkeySequences/src/index.tsx +++ b/examples/solid/createHotkeySequences/src/index.tsx @@ -1,10 +1,10 @@ /* @refresh reload */ import { render } from 'solid-js/web' -import { createSignal, Show } from 'solid-js' +import { Show, createSignal } from 'solid-js' import { + HotkeysProvider, createHotkey, createHotkeySequences, - HotkeysProvider, } from '@tanstack/solid-hotkeys' import { hotkeysDevtoolsPlugin } from '@tanstack/solid-hotkeys-devtools' import { TanStackDevtools } from '@tanstack/solid-devtools' diff --git a/examples/svelte/create-hotkey-sequences/README.md b/examples/svelte/create-hotkey-sequences/README.md index 57b77134..661b3b80 100644 --- a/examples/svelte/create-hotkey-sequences/README.md +++ b/examples/svelte/create-hotkey-sequences/README.md @@ -15,7 +15,7 @@ To recreate this project with the same configuration: ```sh # recreate this project -pnpm dlx sv create --template minimal --types ts --install pnpm create-hotkey +pnpm dlx sv create --template minimal --types ts --install pnpm create-hotkey-sequences ``` ## Developing diff --git a/examples/svelte/create-hotkey-sequences/src/index.css b/examples/svelte/create-hotkey-sequences/src/index.css index 8ee43876..a8c099f4 100644 --- a/examples/svelte/create-hotkey-sequences/src/index.css +++ b/examples/svelte/create-hotkey-sequences/src/index.css @@ -25,7 +25,6 @@ header h1 { } header p { color: #666; - margin: 0; max-width: 500px; margin: 0 auto; } diff --git a/examples/vue/useHotkeySequences/src/index.css b/examples/vue/useHotkeySequences/src/index.css index 69b749be..c7ed3747 100644 --- a/examples/vue/useHotkeySequences/src/index.css +++ b/examples/vue/useHotkeySequences/src/index.css @@ -25,7 +25,6 @@ header h1 { } header p { color: #666; - margin: 0; max-width: 500px; margin: 0 auto; } diff --git a/packages/solid-hotkeys/tests/createHotkeySequences.test.tsx b/packages/solid-hotkeys/tests/createHotkeySequences.test.tsx index 9d5fea5b..317889c8 100644 --- a/packages/solid-hotkeys/tests/createHotkeySequences.test.tsx +++ b/packages/solid-hotkeys/tests/createHotkeySequences.test.tsx @@ -1,12 +1,11 @@ // @vitest-environment happy-dom -import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { render } from '@solidjs/testing-library' import { SequenceManager } from '@tanstack/hotkeys' -import { - createHotkeySequences, - type CreateHotkeySequenceDefinition, -} from '../src/createHotkeySequences' -import { createSignal, type Component } from 'solid-js' +import { createSignal } from 'solid-js' +import type { Component } from 'solid-js' +import { createHotkeySequences } from '../src/createHotkeySequences' +import type { CreateHotkeySequenceDefinition } from '../src/createHotkeySequences' function dispatchKey(key: string) { document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true })) From afec5a4032e05fe88dbc0f76e4e53d7da2978652 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Wed, 25 Mar 2026 08:59:39 -0500 Subject: [PATCH 3/4] rewrite sequence enabled stuff --- .changeset/plural-sequences.md | 2 +- docs/framework/angular/guides/hotkeys.md | 2 + docs/framework/angular/guides/sequences.md | 2 + .../reference/functions/injectHotkey.md | 4 +- .../functions/injectHotkeySequence.md | 5 +- .../functions/injectHotkeySequences.md | 5 +- .../interfaces/InjectHotkeyOptions.md | 4 +- .../interfaces/InjectHotkeySequenceOptions.md | 4 +- docs/framework/preact/guides/hotkeys.md | 2 + docs/framework/preact/guides/sequences.md | 2 + .../preact/reference/functions/useHotkey.md | 5 +- .../reference/functions/useHotkeySequence.md | 5 +- .../reference/functions/useHotkeySequences.md | 7 +- .../preact/reference/functions/useHotkeys.md | 7 +- docs/framework/react/guides/hotkeys.md | 2 + docs/framework/react/guides/sequences.md | 2 + .../react/reference/functions/useHotkey.md | 5 +- .../reference/functions/useHotkeySequence.md | 5 +- .../reference/functions/useHotkeySequences.md | 7 +- .../react/reference/functions/useHotkeys.md | 7 +- docs/framework/solid/guides/hotkeys.md | 2 + docs/framework/solid/guides/sequences.md | 2 + docs/framework/svelte/guides/hotkeys.md | 2 + docs/framework/svelte/guides/sequences.md | 2 + .../reference/functions/createHotkey.md | 2 +- .../functions/createHotkeyAttachment.md | 2 +- .../functions/createHotkeySequence.md | 2 +- .../createHotkeySequenceAttachment.md | 2 +- .../interfaces/CreateHotkeyOptions.md | 4 +- .../interfaces/CreateHotkeySequenceOptions.md | 4 +- docs/framework/vue/guides/hotkeys.md | 2 + docs/framework/vue/guides/sequences.md | 2 + docs/reference/classes/HotkeyManager.md | 18 ++-- docs/reference/functions/getHotkeyManager.md | 2 +- docs/reference/interfaces/HotkeyOptions.md | 20 +++-- .../interfaces/HotkeyRegistration.md | 18 ++-- .../interfaces/HotkeyRegistrationHandle.md | 12 +-- docs/reference/interfaces/SequenceOptions.md | 18 ++-- .../src/app/app.component.html | 10 +++ .../src/app/app.component.ts | 11 ++- .../injectHotkeySequence/src/styles.css | 7 ++ .../src/app/app.component.html | 10 +++ .../src/app/app.component.ts | 6 ++ .../injectHotkeySequences/src/styles.css | 7 ++ .../preact/useHotkeySequence/src/index.css | 7 ++ .../preact/useHotkeySequence/src/index.tsx | 18 +++- .../preact/useHotkeySequences/src/index.css | 7 ++ .../preact/useHotkeySequences/src/index.tsx | 13 +++ .../react/useHotkeySequence/src/index.css | 7 ++ .../react/useHotkeySequence/src/index.tsx | 18 +++- .../react/useHotkeySequences/src/index.css | 7 ++ .../react/useHotkeySequences/src/index.tsx | 13 +++ .../solid/createHotkeySequence/src/index.css | 7 ++ .../solid/createHotkeySequence/src/index.tsx | 20 ++++- .../solid/createHotkeySequences/src/index.css | 7 ++ .../solid/createHotkeySequences/src/index.tsx | 19 ++++ .../create-hotkey-sequence/src/App.svelte | 17 +++- .../create-hotkey-sequence/src/index.css | 7 ++ .../create-hotkey-sequences/src/App.svelte | 12 +++ .../create-hotkey-sequences/src/index.css | 7 ++ examples/vue/useHotkeySequence/src/App.vue | 20 ++++- examples/vue/useHotkeySequence/src/index.css | 7 ++ examples/vue/useHotkeySequences/src/App.vue | 15 ++++ examples/vue/useHotkeySequences/src/index.css | 7 ++ packages/angular-hotkeys/src/injectHotkey.ts | 54 ++++++++--- .../src/injectHotkeySequence.ts | 74 +++++++++++---- .../src/injectHotkeySequences.ts | 14 ++- packages/hotkeys/src/hotkey-manager.ts | 6 +- .../hotkeys/tests/sequence-manager.test.ts | 21 +++++ packages/preact-hotkeys/src/useHotkey.ts | 11 ++- .../preact-hotkeys/src/useHotkeySequence.ts | 23 ++++- .../preact-hotkeys/src/useHotkeySequences.ts | 17 ++-- packages/preact-hotkeys/src/useHotkeys.ts | 17 ++-- .../preact-hotkeys/tests/useHotkey.test.tsx | 26 ++++++ .../tests/useHotkeySequences.test.tsx | 56 ++++++++++++ .../preact-hotkeys/tests/useHotkeys.test.tsx | 31 +++++++ packages/react-hotkeys/src/useHotkey.ts | 11 ++- .../react-hotkeys/src/useHotkeySequence.ts | 17 +++- .../react-hotkeys/src/useHotkeySequences.ts | 17 ++-- packages/react-hotkeys/src/useHotkeys.ts | 17 ++-- .../react-hotkeys/tests/useHotkey.test.tsx | 28 ++++++ .../tests/useHotkeySequences.test.tsx | 53 +++++++++++ .../react-hotkeys/tests/useHotkeys.test.tsx | 30 +++++++ packages/solid-hotkeys/src/createHotkey.ts | 45 +++++++--- .../solid-hotkeys/src/createHotkeySequence.ts | 49 ++++++---- .../tests/createHotkeySequences.test.tsx | 57 ++++++++++++ .../tests/createHotkeys.test.tsx | 35 ++++++++ .../svelte-hotkeys/src/createHotkey.svelte.ts | 80 ++++++++++++++--- .../src/createHotkeySequence.svelte.ts | 90 ++++++++++++++----- packages/vue-hotkeys/src/useHotkey.ts | 34 +++++-- packages/vue-hotkeys/src/useHotkeySequence.ts | 43 ++++++--- 91 files changed, 1177 insertions(+), 265 deletions(-) diff --git a/.changeset/plural-sequences.md b/.changeset/plural-sequences.md index 3bfa61e6..97685d35 100644 --- a/.changeset/plural-sequences.md +++ b/.changeset/plural-sequences.md @@ -7,4 +7,4 @@ '@tanstack/angular-hotkeys': minor --- -Add plural sequence registration APIs: `useHotkeySequences` (React/Preact/Vue), `createHotkeySequences` and `createHotkeySequencesAttachment` (Svelte), `createHotkeySequences` (Solid), and `injectHotkeySequences` (Angular). +Add plural sequence APIs (`useHotkeySequences`, `createHotkeySequences`, `createHotkeySequencesAttachment`, `injectHotkeySequences`) and align `enabled` across adapters: disabled registrations stay in the manager for devtools, only core dispatch is skipped, and toggling `enabled` updates handles via `setOptions` instead of churning unregister/register. diff --git a/docs/framework/angular/guides/hotkeys.md b/docs/framework/angular/guides/hotkeys.md index bafca47b..e772c94f 100644 --- a/docs/framework/angular/guides/hotkeys.md +++ b/docs/framework/angular/guides/hotkeys.md @@ -54,6 +54,8 @@ For reactive state, pass an accessor function as the third argument. ### `enabled` +When `enabled` is false, the hotkey **stays registered** (visible in devtools); only the callback is suppressed. + ```ts import { Component, signal } from '@angular/core' import { injectHotkey } from '@tanstack/angular-hotkeys' diff --git a/docs/framework/angular/guides/sequences.md b/docs/framework/angular/guides/sequences.md index 1e1702b5..80759a75 100644 --- a/docs/framework/angular/guides/sequences.md +++ b/docs/framework/angular/guides/sequences.md @@ -61,6 +61,8 @@ injectHotkeySequence(['G', 'G'], callback, { ### Reactive `enabled` +When disabled, the sequence **stays registered** (visible in devtools); only execution is suppressed. + ```ts import { Component, signal } from '@angular/core' import { injectHotkeySequence } from '@tanstack/angular-hotkeys' diff --git a/docs/framework/angular/reference/functions/injectHotkey.md b/docs/framework/angular/reference/functions/injectHotkey.md index b6f9ad80..c981ed63 100644 --- a/docs/framework/angular/reference/functions/injectHotkey.md +++ b/docs/framework/angular/reference/functions/injectHotkey.md @@ -12,7 +12,7 @@ function injectHotkey( options): void; ``` -Defined in: [injectHotkey.ts:83](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkey.ts#L83) +Defined in: [injectHotkey.ts:86](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkey.ts#L86) Angular inject-based API for registering a keyboard hotkey. @@ -23,6 +23,8 @@ containing the hotkey string and parsed hotkey. Call in an injection context (e.g. constructor or field initializer). Uses effect() to track reactive dependencies and update registration when options or the callback change. +`enabled: false` keeps the registration (visible in devtools) and only suppresses firing; the same +handle is updated instead of unregistering and re-registering when identity is unchanged. ## Parameters diff --git a/docs/framework/angular/reference/functions/injectHotkeySequence.md b/docs/framework/angular/reference/functions/injectHotkeySequence.md index 31c97310..5b12527c 100644 --- a/docs/framework/angular/reference/functions/injectHotkeySequence.md +++ b/docs/framework/angular/reference/functions/injectHotkeySequence.md @@ -12,7 +12,7 @@ function injectHotkeySequence( options): void; ``` -Defined in: [injectHotkeySequence.ts:48](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequence.ts#L48) +Defined in: [injectHotkeySequence.ts:52](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequence.ts#L52) Angular inject-based API for registering a keyboard shortcut sequence (Vim-style). @@ -40,7 +40,8 @@ Function to call when the sequence is completed ### options -Options for the sequence behavior (or getter function) +Options for the sequence behavior (or getter function). `enabled: false` still registers + the sequence (visible in devtools); only execution is suppressed. [`InjectHotkeySequenceOptions`](../interfaces/InjectHotkeySequenceOptions.md) | () => [`InjectHotkeySequenceOptions`](../interfaces/InjectHotkeySequenceOptions.md) diff --git a/docs/framework/angular/reference/functions/injectHotkeySequences.md b/docs/framework/angular/reference/functions/injectHotkeySequences.md index 29b768ed..c4f43988 100644 --- a/docs/framework/angular/reference/functions/injectHotkeySequences.md +++ b/docs/framework/angular/reference/functions/injectHotkeySequences.md @@ -9,7 +9,7 @@ title: injectHotkeySequences function injectHotkeySequences(sequences, commonOptions): void; ``` -Defined in: [injectHotkeySequences.ts:50](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequences.ts#L50) +Defined in: [injectHotkeySequences.ts:51](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequences.ts#L51) Angular inject-based API for registering multiple keyboard shortcut sequences at once (Vim-style). @@ -19,7 +19,8 @@ Uses `effect()` to track reactive dependencies when definitions or options are g Options are merged in this order: provideHotkeys defaults < commonOptions < per-definition options -Definitions with an empty `sequence` or `enabled: false` after merge are skipped. +Definitions with an empty `sequence` are skipped. Disabled sequences (`enabled: false`) +remain registered so they stay visible in devtools; the core manager suppresses execution. ## Parameters diff --git a/docs/framework/angular/reference/interfaces/InjectHotkeyOptions.md b/docs/framework/angular/reference/interfaces/InjectHotkeyOptions.md index 548a7d43..a840c0b2 100644 --- a/docs/framework/angular/reference/interfaces/InjectHotkeyOptions.md +++ b/docs/framework/angular/reference/interfaces/InjectHotkeyOptions.md @@ -5,7 +5,7 @@ title: InjectHotkeyOptions # Interface: InjectHotkeyOptions -Defined in: [injectHotkey.ts:16](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkey.ts#L16) +Defined in: [injectHotkey.ts:17](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkey.ts#L17) ## Extends @@ -19,7 +19,7 @@ Defined in: [injectHotkey.ts:16](https://github.com/TanStack/hotkeys/blob/main/p optional target: HTMLElement | Document | Window | null; ``` -Defined in: [injectHotkey.ts:24](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkey.ts#L24) +Defined in: [injectHotkey.ts:25](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkey.ts#L25) The DOM element to attach the event listener to. Can be a direct DOM element, an accessor (for reactive targets that become diff --git a/docs/framework/angular/reference/interfaces/InjectHotkeySequenceOptions.md b/docs/framework/angular/reference/interfaces/InjectHotkeySequenceOptions.md index 61922e7b..ee000ed7 100644 --- a/docs/framework/angular/reference/interfaces/InjectHotkeySequenceOptions.md +++ b/docs/framework/angular/reference/interfaces/InjectHotkeySequenceOptions.md @@ -5,7 +5,7 @@ title: InjectHotkeySequenceOptions # Interface: InjectHotkeySequenceOptions -Defined in: [injectHotkeySequence.ts:10](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequence.ts#L10) +Defined in: [injectHotkeySequence.ts:13](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequence.ts#L13) ## Extends @@ -19,6 +19,6 @@ Defined in: [injectHotkeySequence.ts:10](https://github.com/TanStack/hotkeys/blo optional enabled: boolean; ``` -Defined in: [injectHotkeySequence.ts:15](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequence.ts#L15) +Defined in: [injectHotkeySequence.ts:18](https://github.com/TanStack/hotkeys/blob/main/packages/angular-hotkeys/src/injectHotkeySequence.ts#L18) Whether the sequence is enabled. Defaults to true. diff --git a/docs/framework/preact/guides/hotkeys.md b/docs/framework/preact/guides/hotkeys.md index 8859e640..a7ab75b7 100644 --- a/docs/framework/preact/guides/hotkeys.md +++ b/docs/framework/preact/guides/hotkeys.md @@ -91,6 +91,8 @@ import { HotkeysProvider } from '@tanstack/preact-hotkeys' Controls whether the hotkey is active. Defaults to `true`. +Disabled hotkeys **remain registered** in the manager and stay visible in devtools; only execution is suppressed. Hooks update `enabled` on the existing registration instead of unregistering and re-registering. + ```tsx const [isEditing, setIsEditing] = useState(false) diff --git a/docs/framework/preact/guides/sequences.md b/docs/framework/preact/guides/sequences.md index 9b6239e4..695e5080 100644 --- a/docs/framework/preact/guides/sequences.md +++ b/docs/framework/preact/guides/sequences.md @@ -65,6 +65,8 @@ useHotkeySequence(['Shift+Z', 'Shift+Z'], () => forceQuit(), { timeout: 2000 }) Controls whether the sequence is active. Defaults to `true`. +Disabled sequences **remain registered** and stay visible in devtools; only execution is suppressed. + ```tsx const [isVimMode, setIsVimMode] = useState(true) diff --git a/docs/framework/preact/reference/functions/useHotkey.md b/docs/framework/preact/reference/functions/useHotkey.md index fb699e07..f24260b9 100644 --- a/docs/framework/preact/reference/functions/useHotkey.md +++ b/docs/framework/preact/reference/functions/useHotkey.md @@ -12,7 +12,7 @@ function useHotkey( options): void; ``` -Defined in: [useHotkey.ts:91](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkey.ts#L91) +Defined in: [useHotkey.ts:92](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkey.ts#L92) Preact hook for registering a keyboard hotkey. @@ -43,7 +43,8 @@ The function to call when the hotkey is pressed [`UseHotkeyOptions`](../interfaces/UseHotkeyOptions.md) = `{}` -Options for the hotkey behavior +Options for the hotkey behavior. `enabled: false` keeps the registration (visible in devtools) + and only suppresses firing; the hook updates the existing handle instead of unregistering. ## Returns diff --git a/docs/framework/preact/reference/functions/useHotkeySequence.md b/docs/framework/preact/reference/functions/useHotkeySequence.md index 014c0474..348b9f5b 100644 --- a/docs/framework/preact/reference/functions/useHotkeySequence.md +++ b/docs/framework/preact/reference/functions/useHotkeySequence.md @@ -12,7 +12,7 @@ function useHotkeySequence( options): void; ``` -Defined in: [useHotkeySequence.ts:73](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequence.ts#L73) +Defined in: [useHotkeySequence.ts:74](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequence.ts#L74) Preact hook for registering a keyboard shortcut sequence (Vim-style). @@ -42,7 +42,8 @@ Function to call when the sequence is completed [`UseHotkeySequenceOptions`](../interfaces/UseHotkeySequenceOptions.md) = `{}` -Options for the sequence behavior +Options for the sequence behavior. `enabled: false` keeps the registration (visible in devtools) + and only suppresses firing; the hook updates the existing handle instead of unregistering. ## Returns diff --git a/docs/framework/preact/reference/functions/useHotkeySequences.md b/docs/framework/preact/reference/functions/useHotkeySequences.md index 6610956e..0820ffd9 100644 --- a/docs/framework/preact/reference/functions/useHotkeySequences.md +++ b/docs/framework/preact/reference/functions/useHotkeySequences.md @@ -9,7 +9,7 @@ title: useHotkeySequences function useHotkeySequences(definitions, commonOptions): void; ``` -Defined in: [useHotkeySequences.ts:65](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequences.ts#L65) +Defined in: [useHotkeySequences.ts:68](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequences.ts#L68) Preact hook for registering multiple keyboard shortcut sequences at once (Vim-style). @@ -35,7 +35,10 @@ Array of sequence definitions to register [`UseHotkeySequenceOptions`](../interfaces/UseHotkeySequenceOptions.md) = `{}` -Shared options applied to all sequences (overridden by per-definition options) +Shared options applied to all sequences (overridden by per-definition options). + Per-row `enabled: false` still registers that sequence: `SequenceManager` suppresses execution only (the row + stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle + via `setOptions` (no unregister/re-register churn). ## Returns diff --git a/docs/framework/preact/reference/functions/useHotkeys.md b/docs/framework/preact/reference/functions/useHotkeys.md index 7264e159..02d03d1d 100644 --- a/docs/framework/preact/reference/functions/useHotkeys.md +++ b/docs/framework/preact/reference/functions/useHotkeys.md @@ -9,7 +9,7 @@ title: useHotkeys function useHotkeys(hotkeys, commonOptions): void; ``` -Defined in: [useHotkeys.ts:71](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeys.ts#L71) +Defined in: [useHotkeys.ts:74](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeys.ts#L74) Preact hook for registering multiple keyboard hotkeys at once. @@ -34,7 +34,10 @@ Array of hotkey definitions to register [`UseHotkeyOptions`](../interfaces/UseHotkeyOptions.md) = `{}` -Shared options applied to all hotkeys (overridden by per-definition options) +Shared options applied to all hotkeys (overridden by per-definition options). + Per-row `enabled: false` still registers that hotkey: `HotkeyManager` suppresses execution only (the row + stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle + via `setOptions` (no unregister/re-register churn). ## Returns diff --git a/docs/framework/react/guides/hotkeys.md b/docs/framework/react/guides/hotkeys.md index 71fe5b75..3a0f5c88 100644 --- a/docs/framework/react/guides/hotkeys.md +++ b/docs/framework/react/guides/hotkeys.md @@ -91,6 +91,8 @@ import { HotkeysProvider } from '@tanstack/react-hotkeys' Controls whether the hotkey is active. Defaults to `true`. +Disabled hotkeys **remain registered** in the manager and stay visible in devtools; only execution is suppressed. Framework hooks update `enabled` on the existing registration instead of unregistering and re-registering. + ```tsx const [isEditing, setIsEditing] = useState(false) diff --git a/docs/framework/react/guides/sequences.md b/docs/framework/react/guides/sequences.md index 530cd65e..3ebd8cec 100644 --- a/docs/framework/react/guides/sequences.md +++ b/docs/framework/react/guides/sequences.md @@ -64,6 +64,8 @@ useHotkeySequence(['Shift+Z', 'Shift+Z'], () => forceQuit(), { timeout: 2000 }) Controls whether the sequence is active. Defaults to `true`. +Disabled sequences **remain registered** and stay visible in devtools; only execution is suppressed. + ```tsx const [isVimMode, setIsVimMode] = useState(true) diff --git a/docs/framework/react/reference/functions/useHotkey.md b/docs/framework/react/reference/functions/useHotkey.md index 9da8d452..17a96595 100644 --- a/docs/framework/react/reference/functions/useHotkey.md +++ b/docs/framework/react/reference/functions/useHotkey.md @@ -12,7 +12,7 @@ function useHotkey( options): void; ``` -Defined in: [useHotkey.ts:90](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkey.ts#L90) +Defined in: [useHotkey.ts:91](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkey.ts#L91) React hook for registering a keyboard hotkey. @@ -43,7 +43,8 @@ The function to call when the hotkey is pressed [`UseHotkeyOptions`](../interfaces/UseHotkeyOptions.md) = `{}` -Options for the hotkey behavior +Options for the hotkey behavior. `enabled: false` keeps the registration (visible in devtools) + and only suppresses firing; the hook updates the existing handle instead of unregistering. ## Returns diff --git a/docs/framework/react/reference/functions/useHotkeySequence.md b/docs/framework/react/reference/functions/useHotkeySequence.md index 3436b036..de36b75c 100644 --- a/docs/framework/react/reference/functions/useHotkeySequence.md +++ b/docs/framework/react/reference/functions/useHotkeySequence.md @@ -12,7 +12,7 @@ function useHotkeySequence( options): void; ``` -Defined in: [useHotkeySequence.ts:72](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequence.ts#L72) +Defined in: [useHotkeySequence.ts:73](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequence.ts#L73) React hook for registering a keyboard shortcut sequence (Vim-style). @@ -42,7 +42,8 @@ Function to call when the sequence is completed [`UseHotkeySequenceOptions`](../interfaces/UseHotkeySequenceOptions.md) = `{}` -Options for the sequence behavior +Options for the sequence behavior. `enabled: false` keeps the registration (visible in devtools) + and only suppresses firing; the hook updates the existing handle instead of unregistering. ## Returns diff --git a/docs/framework/react/reference/functions/useHotkeySequences.md b/docs/framework/react/reference/functions/useHotkeySequences.md index 2f0ef16b..057d7015 100644 --- a/docs/framework/react/reference/functions/useHotkeySequences.md +++ b/docs/framework/react/reference/functions/useHotkeySequences.md @@ -9,7 +9,7 @@ title: useHotkeySequences function useHotkeySequences(definitions, commonOptions): void; ``` -Defined in: [useHotkeySequences.ts:65](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequences.ts#L65) +Defined in: [useHotkeySequences.ts:68](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeySequences.ts#L68) React hook for registering multiple keyboard shortcut sequences at once (Vim-style). @@ -35,7 +35,10 @@ Array of sequence definitions to register [`UseHotkeySequenceOptions`](../interfaces/UseHotkeySequenceOptions.md) = `{}` -Shared options applied to all sequences (overridden by per-definition options) +Shared options applied to all sequences (overridden by per-definition options). + Per-row `enabled: false` still registers that sequence: `SequenceManager` suppresses execution only (the row + stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle + via `setOptions` (no unregister/re-register churn). ## Returns diff --git a/docs/framework/react/reference/functions/useHotkeys.md b/docs/framework/react/reference/functions/useHotkeys.md index 20d618d2..e83a6b05 100644 --- a/docs/framework/react/reference/functions/useHotkeys.md +++ b/docs/framework/react/reference/functions/useHotkeys.md @@ -9,7 +9,7 @@ title: useHotkeys function useHotkeys(hotkeys, commonOptions): void; ``` -Defined in: [useHotkeys.ts:71](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeys.ts#L71) +Defined in: [useHotkeys.ts:74](https://github.com/TanStack/hotkeys/blob/main/packages/react-hotkeys/src/useHotkeys.ts#L74) React hook for registering multiple keyboard hotkeys at once. @@ -34,7 +34,10 @@ Array of hotkey definitions to register [`UseHotkeyOptions`](../interfaces/UseHotkeyOptions.md) = `{}` -Shared options applied to all hotkeys (overridden by per-definition options) +Shared options applied to all hotkeys (overridden by per-definition options). + Per-row `enabled: false` still registers that hotkey: `HotkeyManager` suppresses execution only (the row + stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle + via `setOptions` (no unregister/re-register churn). ## Returns diff --git a/docs/framework/solid/guides/hotkeys.md b/docs/framework/solid/guides/hotkeys.md index f5c85d3b..93fdb99b 100644 --- a/docs/framework/solid/guides/hotkeys.md +++ b/docs/framework/solid/guides/hotkeys.md @@ -120,6 +120,8 @@ import { HotkeysProvider } from '@tanstack/solid-hotkeys' Controls whether the hotkey is active. Defaults to `true`. Use an accessor for reactive control. +Disabled hotkeys **remain registered** in the manager and stay visible in devtools; only execution is suppressed. + ```tsx const [isEditing, setIsEditing] = createSignal(false) diff --git a/docs/framework/solid/guides/sequences.md b/docs/framework/solid/guides/sequences.md index db693cd5..25edda67 100644 --- a/docs/framework/solid/guides/sequences.md +++ b/docs/framework/solid/guides/sequences.md @@ -77,6 +77,8 @@ createHotkeySequence(['Shift+Z', 'Shift+Z'], () => forceQuit(), { timeout: 2000 Controls whether the sequence is active. Defaults to `true`. Use an accessor for reactive control. +Disabled sequences **remain registered** and stay visible in devtools; only execution is suppressed. + ```tsx const [isVimMode, setIsVimMode] = createSignal(true) diff --git a/docs/framework/svelte/guides/hotkeys.md b/docs/framework/svelte/guides/hotkeys.md index 78eaed3d..bf23333f 100644 --- a/docs/framework/svelte/guides/hotkeys.md +++ b/docs/framework/svelte/guides/hotkeys.md @@ -48,6 +48,8 @@ Hotkeys can take plain values for static registrations or getter functions when ### Reactive `enabled` +When `enabled` is false, the hotkey **stays registered** (visible in devtools); only the callback is suppressed. + ```svelte