@tanstack/hotkeys 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,11 @@
1
- import { ConflictBehavior, Hotkey, HotkeyCallback, HotkeyMeta, ParsedHotkey, RegisterableHotkey } from "./hotkey.types.js";
1
+ import { ConflictBehavior, HotkeyCallback, HotkeyChord, HotkeyMeta, HotkeySequence, ParsedHotkey, RegisterableHotkey, RegisterableHotkeyChord } from "./hotkey.types.js";
2
+ import { AppHotkeysStore } from "./app-hotkeys.js";
2
3
  import { Store } from "@tanstack/store";
3
4
  //#region src/hotkey-manager.d.ts
5
+ /** An element, document, or window that hotkeys can listen on. */
6
+ export type Target = HTMLElement | Document | Window;
4
7
  /**
5
- * Options for registering a hotkey.
8
+ * Options shared by chords and sequences.
6
9
  */
7
10
  export interface HotkeyOptions {
8
11
  /** Behavior when this hotkey conflicts with an existing registration on the same target. Defaults to 'warn' */
@@ -15,71 +18,103 @@ export interface HotkeyOptions {
15
18
  enabled?: boolean;
16
19
  /** The event type to listen for. Defaults to 'keydown' */
17
20
  eventType?: 'keydown' | 'keyup';
18
- /** Whether to ignore hotkeys when keyboard events originate from input-like elements (text inputs, textarea, select, contenteditable — button-type inputs like type=button/submit/reset are not ignored). Defaults based on hotkey: true for single keys and Shift/Alt combos; false for Ctrl/Meta shortcuts and Escape */
21
+ /** Whether to ignore hotkeys when keyboard events originate from input-like elements (text inputs, textarea, select, contenteditable — button-type inputs like type=button/submit/reset are not ignored). Defaults based on the hotkey (the first step of a sequence): true for single keys and Shift/Alt combos; false for Ctrl/Meta shortcuts and Escape */
19
22
  ignoreInputs?: boolean;
20
23
  /** The target platform for resolving 'Mod' */
21
24
  platform?: 'mac' | 'windows' | 'linux';
22
25
  /** Prevent the default browser action when the hotkey matches. Defaults to true */
23
26
  preventDefault?: boolean;
24
- /** If true, only trigger once until all keys are released. Default: false */
25
- requireReset?: boolean;
26
27
  /** Stop event propagation when the hotkey matches. Defaults to true */
27
28
  stopPropagation?: boolean;
28
29
  /** The DOM element to attach the event listener to. Defaults to document. */
29
- target?: HTMLElement | Document | Window | null;
30
+ target?: Target | null;
30
31
  /** Optional metadata (name, description, custom fields via declaration merging) */
31
32
  meta?: HotkeyMeta;
33
+ /** Id of the app hotkey this registration implements. Adapters set this when registering an app hotkey from `defineAppHotkeys`. */
34
+ appHotkeyId?: string;
35
+ }
36
+ /** Options for a single chord such as `'Mod+S'`. */
37
+ export interface HotkeyChordOptions extends HotkeyOptions {
38
+ /** If true, only trigger once until all keys are released. Default: false */
39
+ requireReset?: boolean;
40
+ }
41
+ /** Options for a sequence such as `['G', 'G']`. */
42
+ export interface HotkeySequenceOptions extends HotkeyOptions {
43
+ /** Maximum time between steps in milliseconds. Default: 1000 */
44
+ timeout?: number;
32
45
  }
33
46
  /**
34
- * A registered hotkey handler in the HotkeyManager.
47
+ * The options type for a hotkey: chord options for chords (including one-step
48
+ * tuples), sequence options for sequences.
35
49
  */
36
- export interface HotkeyRegistration {
50
+ export type HotkeyOptionsFor<T extends RegisterableHotkey> = [T] extends [never] ? HotkeyChordOptions & HotkeySequenceOptions : T extends readonly [RegisterableHotkeyChord] ? HotkeyChordOptions : T extends HotkeySequence ? HotkeySequenceOptions : HotkeyChordOptions;
51
+ interface RegistrationBase<TOptions extends HotkeyOptions> {
52
+ /** Unique identifier for this registration */
53
+ id: string;
37
54
  /** The callback to invoke */
38
55
  callback: HotkeyCallback;
39
- /** Whether this registration has fired and needs reset (for requireReset) */
56
+ /** Resolved options for this registration */
57
+ options: TOptions;
58
+ /** The resolved target element for this registration */
59
+ target: Target;
60
+ /** How many times this registration's callback has been triggered */
61
+ triggerCount: number;
62
+ /** Whether this registration has fired (and, for requireReset chords, needs a reset) */
40
63
  hasFired: boolean;
64
+ }
65
+ /** A registered chord in the HotkeyManager. */
66
+ export interface HotkeyChordRegistration extends RegistrationBase<HotkeyChordOptions> {
67
+ kind: 'chord';
68
+ hotkey: HotkeyChord;
69
+ parsedHotkey: ParsedHotkey;
41
70
  /** The concrete key/code that activated a requireReset registration. */
42
71
  activeMatch?: {
43
72
  key: string;
44
73
  code: string;
45
74
  };
46
- /** The original hotkey string */
47
- hotkey: Hotkey;
48
- /** Unique identifier for this registration */
49
- id: string;
50
- /** Options for this registration */
51
- options: HotkeyOptions;
52
- /** The parsed hotkey */
53
- parsedHotkey: ParsedHotkey;
54
- /** The resolved target element for this registration */
55
- target: HTMLElement | Document | Window;
56
- /** How many times this registration's callback has been triggered */
57
- triggerCount: number;
58
75
  }
59
- /**
60
- * Public view of a hotkey registration for display and introspection.
61
- * Omits the callback function which is an internal implementation detail.
62
- */
63
- export interface HotkeyRegistrationView {
64
- /** The original hotkey string */
65
- hotkey: Hotkey;
66
- /** Unique identifier for this registration */
76
+ /** A registered sequence in the HotkeyManager. */
77
+ export interface HotkeySequenceRegistration extends RegistrationBase<HotkeySequenceOptions> {
78
+ kind: 'sequence';
79
+ hotkey: HotkeySequence;
80
+ parsedSequence: Array<ParsedHotkey>;
81
+ /** Steps matched in the current attempt */
82
+ currentIndex: number;
83
+ /** `Date.now()` when the last step in the current attempt matched */
84
+ lastKeyTime: number;
85
+ }
86
+ /** A registered hotkey in the HotkeyManager. */
87
+ export type HotkeyRegistration = HotkeyChordRegistration | HotkeySequenceRegistration;
88
+ interface RegistrationViewBase {
67
89
  id: string;
68
- /** Options for this registration */
69
- options: HotkeyOptions;
70
- /** The parsed hotkey */
71
- parsedHotkey: ParsedHotkey;
72
- /** The resolved target element for this registration */
73
- target: HTMLElement | Document | Window;
74
- /** How many times this registration's callback has been triggered */
90
+ target: Target;
75
91
  triggerCount: number;
76
- /** Whether this registration has fired and needs reset (for requireReset) */
77
92
  hasFired: boolean;
78
93
  }
94
+ /** Public view of a chord registration, without its callback. */
95
+ export interface HotkeyChordRegistrationView extends RegistrationViewBase {
96
+ kind: 'chord';
97
+ hotkey: HotkeyChord;
98
+ parsedHotkey: ParsedHotkey;
99
+ options: HotkeyChordOptions;
100
+ }
101
+ /** Public view of a sequence registration, without its callback. */
102
+ export interface HotkeySequenceRegistrationView extends RegistrationViewBase {
103
+ kind: 'sequence';
104
+ hotkey: HotkeySequence;
105
+ parsedSequence: Array<ParsedHotkey>;
106
+ options: HotkeySequenceOptions;
107
+ /** Steps matched in the current attempt (0 when idle or just completed). */
108
+ matchedStepCount: number;
109
+ /** `Date.now()` when the last step in the current attempt matched; 0 if none. */
110
+ partialMatchLastKeyTime: number;
111
+ }
79
112
  /**
80
- * Creates a public view from an internal registration,
81
- * stripping the callback function.
113
+ * Public view of a hotkey registration for display and introspection.
114
+ * Narrow with `kind` to read chord- or sequence-specific fields.
82
115
  */
116
+ export type HotkeyRegistrationView = HotkeyChordRegistrationView | HotkeySequenceRegistrationView;
117
+ /** Creates a public view from an internal registration, stripping the callback. */
83
118
  export declare function toHotkeyRegistrationView(reg: HotkeyRegistration): HotkeyRegistrationView;
84
119
  /**
85
120
  * A handle returned from HotkeyManager.register() that allows updating
@@ -95,16 +130,11 @@ export declare function toHotkeyRegistrationView(reg: HotkeyRegistration): Hotke
95
130
  * // Update options without re-registering
96
131
  * handle.setOptions({ enabled: false })
97
132
  *
98
- * // Check if still active
99
- * if (handle.isActive) {
100
- * // ...
101
- * }
102
- *
103
133
  * // Unregister when done
104
134
  * handle.unregister()
105
135
  * ```
106
136
  */
107
- export interface HotkeyRegistrationHandle {
137
+ export interface HotkeyRegistrationHandle<TOptions extends HotkeyOptions = HotkeyChordOptions | HotkeySequenceOptions> {
108
138
  /**
109
139
  * The callback function. Can be set directly to update without re-registering.
110
140
  * This avoids stale closures when the callback references React state.
@@ -118,33 +148,40 @@ export interface HotkeyRegistrationHandle {
118
148
  * Update options (merged with existing options).
119
149
  * Useful for updating `enabled`, `preventDefault`, etc. without re-registering.
120
150
  */
121
- setOptions: (options: Partial<HotkeyOptions>) => void;
151
+ setOptions: (options: Partial<TOptions>) => void;
122
152
  /** Unregister this hotkey */
123
153
  unregister: () => void;
124
154
  }
125
155
  /**
126
- * Singleton manager for hotkey registrations.
156
+ * Singleton manager for hotkey registrations, including sequences.
127
157
  *
128
- * This class provides a centralized way to register and manage keyboard hotkeys.
129
- * It uses a single event listener for efficiency, regardless of how many hotkeys
130
- * are registered.
158
+ * This class provides a centralized way to register and manage keyboard
159
+ * hotkeys. It attaches one keydown and one keyup listener per target,
160
+ * regardless of how many hotkeys are registered.
161
+ *
162
+ * Sequences (Vim-style hotkeys such as `['G', 'G']`) match when every step is
163
+ * pressed within the timeout. Modifier-only events (Shift, Control, Alt, or
164
+ * Meta alone) neither advance nor reset a sequence, so chained modifier chords
165
+ * such as `['Shift+R', 'Shift+T']` work.
131
166
  *
132
167
  * @example
133
168
  * ```ts
134
169
  * const manager = HotkeyManager.getInstance()
135
170
  *
136
- * const unregister = manager.register('Mod+S', (event, context) => {
171
+ * const save = manager.register('Mod+S', (event, context) => {
137
172
  * console.log('Save triggered!')
138
173
  * })
174
+ * const top = manager.register(['G', 'G'], () => scrollToTop(), { timeout: 500 })
139
175
  *
140
176
  * // Later, to unregister:
141
- * unregister()
177
+ * save.unregister()
178
+ * top.unregister()
142
179
  * ```
143
180
  */
144
181
  export declare class HotkeyManager {
145
182
  #private;
146
183
  /**
147
- * The TanStack Store containing all hotkey registrations.
184
+ * The TanStack Store containing all hotkey registrations, chords and sequences.
148
185
  * Use this to subscribe to registration changes or access current registrations.
149
186
  *
150
187
  * @example
@@ -163,6 +200,11 @@ export declare class HotkeyManager {
163
200
  * ```
164
201
  */
165
202
  readonly registrations: Store<Map<string, HotkeyRegistration>>;
203
+ /**
204
+ * The app hotkeys store published by a framework provider, so tools such as
205
+ * devtools can list declared hotkeys that are not registered.
206
+ */
207
+ readonly appHotkeys: Store<AppHotkeysStore | null>;
166
208
  private constructor();
167
209
  /**
168
210
  * Gets the singleton instance of HotkeyManager.
@@ -175,10 +217,14 @@ export declare class HotkeyManager {
175
217
  /**
176
218
  * Registers a hotkey handler and returns a handle for updating the registration.
177
219
  *
220
+ * Pass a chord (`'Mod+S'` or a RawHotkey object) or a sequence (`['G', 'G']`).
221
+ * A one-step sequence registers as its chord. Chords accept `requireReset`;
222
+ * sequences accept `timeout`.
223
+ *
178
224
  * The returned handle allows updating the callback and options without
179
225
  * re-registering, which is useful for avoiding stale closures in React.
180
226
  *
181
- * @param hotkey - The hotkey string (e.g., 'Mod+S') or RawHotkey object
227
+ * @param hotkey - A chord string, RawHotkey object, or sequence
182
228
  * @param callback - The function to call when the hotkey is pressed
183
229
  * @param options - Options for the hotkey behavior
184
230
  * @returns A handle for managing the registration
@@ -197,27 +243,32 @@ export declare class HotkeyManager {
197
243
  * handle.unregister()
198
244
  * ```
199
245
  */
200
- register(hotkey: RegisterableHotkey, callback: HotkeyCallback, options?: HotkeyOptions): HotkeyRegistrationHandle;
246
+ register<T extends RegisterableHotkey>(hotkey: T, callback: HotkeyCallback, options?: HotkeyOptionsFor<T>): HotkeyRegistrationHandle<HotkeyOptionsFor<T>>;
247
+ /**
248
+ * Resets the progress of every in-progress sequence.
249
+ */
250
+ resetSequenceProgress(): void;
201
251
  /**
202
252
  * Triggers a registration's callback programmatically from devtools.
203
- * Creates a synthetic KeyboardEvent and invokes the callback.
253
+ * Creates a synthetic KeyboardEvent from the chord (or a sequence's last step)
254
+ * and invokes the callback.
204
255
  *
205
256
  * @param id - The registration ID to trigger
206
257
  * @returns True if the registration was found and triggered
207
258
  */
208
259
  triggerRegistration(id: string): boolean;
209
260
  /**
210
- * Gets the number of registered hotkeys.
261
+ * Gets the number of registered hotkeys, chords and sequences.
211
262
  */
212
263
  getRegistrationCount(): number;
213
264
  /**
214
265
  * Checks if a specific hotkey is registered.
215
266
  *
216
- * @param hotkey - The hotkey string to check
267
+ * @param hotkey - The chord or sequence to check
217
268
  * @param target - Optional target element to match (if provided, both hotkey and target must match)
218
269
  * @returns True if a matching registration exists
219
270
  */
220
- isRegistered(hotkey: Hotkey, target?: HTMLElement | Document | Window): boolean;
271
+ isRegistered(hotkey: RegisterableHotkey, target?: Target): boolean;
221
272
  /**
222
273
  * Destroys the manager and removes all listeners.
223
274
  */