@tanstack/hotkeys 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/format.d.ts CHANGED
@@ -1,20 +1,24 @@
1
- import { ParsedHotkey, RegisterableHotkeyChord } from "./hotkey.types.js";
1
+ import { ParsedHotkeyChord, RegisterableHotkeyChord } from "./hotkey.types.js";
2
2
  //#region src/format.d.ts
3
3
  /**
4
- * Converts a ParsedHotkey back to a hotkey string.
4
+ * Converts a parsed hotkey back to a string, with each platform modifier
5
+ * spelled out, such as `Control+Shift+S`. A parsed chord returns a string, and
6
+ * a parsed sequence returns one string per step. To get `Mod` back where the
7
+ * platform allows it, use `normalizeHotkey` instead.
5
8
  *
6
- * @param parsed - The parsed hotkey object
7
- * @returns A hotkey string in canonical form
9
+ * @param parsed - A parsed chord, or the parsed steps of a sequence
8
10
  *
9
11
  * @example
10
12
  * ```ts
11
- * formatHotkey({ key: 'S', ctrl: true, shift: true, alt: false, meta: false, modifiers: ['Control', 'Shift'] })
12
- * // Returns: 'Control+Shift+S'
13
+ * formatHotkey(parseHotkey('Mod+Shift+S', 'windows')) // 'Control+Shift+S'
14
+ * formatHotkey(parseHotkey(['G', 'Mod+K'], 'mac')) // ['G', 'Meta+K']
13
15
  * ```
14
16
  */
15
- export declare function formatHotkey(parsed: ParsedHotkey): string;
17
+ export declare function formatHotkey(parsed: ParsedHotkeyChord): string;
18
+ export declare function formatHotkey(parsed: ReadonlyArray<ParsedHotkeyChord>): Array<string>;
19
+ export declare function formatHotkey(parsed: ParsedHotkeyChord | ReadonlyArray<ParsedHotkeyChord>): string | Array<string>;
16
20
  /** A single chord to display: a string, raw object, or parsed hotkey. */
17
- export type DisplayHotkeyChord = RegisterableHotkeyChord | ParsedHotkey | (string & {});
21
+ export type DisplayHotkeyChord = RegisterableHotkeyChord | ParsedHotkeyChord | (string & {});
18
22
  /** A hotkey to display: a chord or a sequence of chords. */
19
23
  export type DisplayHotkey = DisplayHotkeyChord | ReadonlyArray<DisplayHotkeyChord>;
20
24
  /**
@@ -31,7 +35,7 @@ export type DisplayHotkey = DisplayHotkeyChord | ReadonlyArray<DisplayHotkeyChor
31
35
  * Sequence steps are formatted individually and joined with `sequenceSeparator`
32
36
  * (a space by default).
33
37
  *
34
- * @param hotkey - A logical or physical string, raw object, ParsedHotkey, or sequence
38
+ * @param hotkey - A logical or physical string, raw object, ParsedHotkeyChord, or sequence
35
39
  * @param options - Formatting options
36
40
  * @returns Display text, or individual key labels when parts is true (one array per step for sequences)
37
41
  *
package/dist/format.js CHANGED
@@ -1,23 +1,14 @@
1
- import { MODIFIER_ALIASES, MODIFIER_ORDER, PUNCTUATION_CODE_MAP, normalizeKeyName } from "./constants.js";
1
+ import { MODIFIER_ORDER, normalizeKeyName } from "./constants.js";
2
2
  import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS } from "./display-labels.js";
3
3
  import { detectPlatform } from "./platform.js";
4
- import { assertLogicalKey, splitHotkeyParts } from "./_keyboard-event.js";
5
- import { isModifierKey, normalizeHotkeyFromParsed, normalizeRegisterableHotkey } from "./parse.js";
4
+ import { assertLogicalKey, codeToLogicalKey, modifierAlias, physicalCodeOf, splitChordParts } from "./_keyboard-event.js";
5
+ import { normalizeHotkey } from "./parse.js";
6
6
 
7
7
  //#region src/format.ts
8
- /**
9
- * Converts a ParsedHotkey back to a hotkey string.
10
- *
11
- * @param parsed - The parsed hotkey object
12
- * @returns A hotkey string in canonical form
13
- *
14
- * @example
15
- * ```ts
16
- * formatHotkey({ key: 'S', ctrl: true, shift: true, alt: false, meta: false, modifiers: ['Control', 'Shift'] })
17
- * // Returns: 'Control+Shift+S'
18
- * ```
19
- */
20
8
  function formatHotkey(parsed) {
9
+ return Array.isArray(parsed) ? parsed.map((step) => formatParsedChord(step)) : formatParsedChord(parsed);
10
+ }
11
+ function formatParsedChord(parsed) {
21
12
  if (parsed.code === void 0) assertLogicalKey(parsed.key);
22
13
  const parts = [];
23
14
  for (const modifier of MODIFIER_ORDER) if (parsed.modifiers.includes(modifier)) parts.push(modifier);
@@ -45,8 +36,8 @@ function formatChordParts(hotkey, options) {
45
36
  const symbols = options.useSymbols ?? true;
46
37
  const modifierSymbols = modifierSymbolsEnabled(options);
47
38
  const keySymbols = typeof symbols === "boolean" ? symbols : symbols.keys ?? true;
48
- const normalized = typeof hotkey === "object" && "modifiers" in hotkey ? normalizeHotkeyFromParsed(hotkey, platform) : normalizeRegisterableHotkey(hotkey, platform);
49
- const segments = splitHotkeyParts(normalized);
39
+ const normalized = normalizeHotkey(hotkey, platform);
40
+ const segments = splitChordParts(normalized);
50
41
  if (platform === "mac") {
51
42
  const modifierIndex = (segment) => {
52
43
  const index = MODIFIER_ORDER.indexOf(segment === "Mod" ? "Meta" : segment);
@@ -55,23 +46,16 @@ function formatChordParts(hotkey, options) {
55
46
  segments.sort((a, b) => modifierIndex(a) - modifierIndex(b));
56
47
  }
57
48
  return segments.map((segment) => {
58
- if (isModifierKey(segment)) {
59
- const modifier = MODIFIER_ALIASES[segment] ?? MODIFIER_ALIASES[segment.toLowerCase()];
60
- return platform === "mac" ? modifierSymbols ? MAC_MODIFIER_SYMBOLS[modifier] : MAC_MODIFIER_LABELS[modifier] : platform === "windows" ? WINDOWS_MODIFIER_LABELS[modifier] : LINUX_MODIFIER_LABELS[modifier];
61
- }
62
- const code = /^\[([A-Za-z][A-Za-z0-9]*)\]$/.exec(segment)?.[1];
49
+ const modifier = modifierAlias(segment);
50
+ if (modifier) return platform === "mac" ? modifierSymbols ? MAC_MODIFIER_SYMBOLS[modifier] : MAC_MODIFIER_LABELS[modifier] : platform === "windows" ? WINDOWS_MODIFIER_LABELS[modifier] : LINUX_MODIFIER_LABELS[modifier];
51
+ const code = physicalCodeOf(segment);
63
52
  const label = options.keyLabels?.[code ?? segment];
64
53
  if (label !== void 0) return label;
65
54
  const numpad = code?.startsWith("Numpad") ?? false;
66
55
  let displayKey = code ?? segment;
67
56
  const mappedKey = code ? options.layoutMap?.get(code) : void 0;
68
57
  if (mappedKey) displayKey = normalizeKeyName(mappedKey);
69
- else if (code) {
70
- if (/^Key[A-Z]$/.test(code)) displayKey = code.slice(3);
71
- else if (/^Digit[0-9]$/.test(code)) displayKey = code.slice(5);
72
- else if (numpad) displayKey = code.slice(6);
73
- else displayKey = PUNCTUATION_CODE_MAP[code] ?? code;
74
- }
58
+ else if (code) displayKey = numpad ? code.slice(6) : codeToLogicalKey(code) ?? code;
75
59
  const display = (keySymbols ? KEY_DISPLAY_SYMBOLS : PUNCTUATION_KEY_DISPLAY_LABELS)[displayKey] ?? displayKey;
76
60
  return numpad ? `Numpad ${display.replace(/([a-z])([A-Z])/g, "$1 $2")}` : display;
77
61
  });
package/dist/hint.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MODIFIER_KEYS, normalizeKeyName } from "./constants.js";
2
- import { isHotkeySequence, parseRegisterableHotkey } from "./parse.js";
2
+ import { isHotkeySequence, parseHotkey } from "./parse.js";
3
3
 
4
4
  //#region src/hint.ts
5
5
  /**
@@ -18,7 +18,7 @@ function matchesHeldModifiers(hotkey, heldKeys, options = {}) {
18
18
  const held = [...new Set(heldKeys.map(normalizeKeyName).filter((key) => MODIFIER_KEYS.has(key)))];
19
19
  const first = isHotkeySequence(hotkey) ? hotkey[0] : hotkey;
20
20
  if (first === void 0) return false;
21
- const { modifiers } = parseRegisterableHotkey(first, options.platform);
21
+ const { modifiers } = parseHotkey(first, options.platform);
22
22
  return held.length > 0 && held.every((key) => modifiers.includes(key)) && (!options.exact || held.length === modifiers.length);
23
23
  }
24
24
 
@@ -1,4 +1,4 @@
1
- import { ConflictBehavior, HotkeyCallback, HotkeyChord, HotkeyMeta, HotkeySequence, ParsedHotkey, RegisterableHotkey, RegisterableHotkeyChord } from "./hotkey.types.js";
1
+ import { ConflictBehavior, HotkeyCallback, HotkeyCallbackContextFor, HotkeyChord, HotkeyMeta, HotkeySequence, ParsedHotkeyChord, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence } from "./hotkey.types.js";
2
2
  import { AppHotkeysStore } from "./app-hotkeys.js";
3
3
  import { Store } from "@tanstack/store";
4
4
  //#region src/hotkey-manager.d.ts
@@ -39,14 +39,30 @@ export interface HotkeyOptions {
39
39
  ignoreInputs?: boolean | 'auto';
40
40
  /** The target platform for resolving 'Mod' */
41
41
  platform?: 'mac' | 'windows' | 'linux';
42
- /** Prevent the default browser action when the hotkey matches. Defaults to true */
42
+ /**
43
+ * Prevent the default browser action when the hotkey matches. For a
44
+ * sequence, every step that advances it does. Defaults to true
45
+ */
43
46
  preventDefault?: boolean;
44
- /** Stop event propagation when the hotkey matches. Defaults to true */
47
+ /**
48
+ * Stop event propagation when the hotkey matches. For a sequence, only the
49
+ * last step does, so earlier steps reach other listeners. Defaults to true
50
+ */
45
51
  stopPropagation?: boolean;
46
52
  /** The DOM element to attach the event listener to. Defaults to document. */
47
53
  target?: Target | null;
48
54
  /** Optional metadata (name, description, custom fields via declaration merging) */
49
55
  meta?: HotkeyMeta;
56
+ /**
57
+ * Scope this hotkey belongs to. Defaults to `'global'`, which is always
58
+ * active. Other scopes are active while activated with
59
+ * `HotkeyManager.activateScope`, which framework providers do while
60
+ * mounted. While an exclusive scope is active, only its hotkeys, those of
61
+ * scopes nested in it, and those of scopes it allows fire.
62
+ * When active scopes share a hotkey on the same target, the most recently
63
+ * activated scope handles it.
64
+ */
65
+ scope?: string;
50
66
  /** Id of the app hotkey this registration implements. Adapters set this when registering an app hotkey from `defineAppHotkeys`. */
51
67
  appHotkeyId?: string;
52
68
  }
@@ -64,7 +80,7 @@ export interface HotkeySequenceOptions extends HotkeyOptions {
64
80
  * The options type for a hotkey: chord options for chords (including one-step
65
81
  * tuples), sequence options for sequences.
66
82
  */
67
- export type HotkeyOptionsFor<T extends RegisterableHotkey> = [T] extends [never] ? HotkeyChordOptions & HotkeySequenceOptions : T extends readonly [RegisterableHotkeyChord] ? HotkeyChordOptions : T extends HotkeySequence ? HotkeySequenceOptions : HotkeyChordOptions;
83
+ export type HotkeyOptionsFor<T extends RegisterableHotkey> = [T] extends [never] ? HotkeyChordOptions & HotkeySequenceOptions : T extends readonly [RegisterableHotkeyChord] ? HotkeyChordOptions : T extends RegisterableHotkeySequence ? HotkeySequenceOptions : HotkeyChordOptions;
68
84
  interface RegistrationBase<TOptions extends HotkeyOptions> {
69
85
  /** Unique identifier for this registration */
70
86
  id: string;
@@ -83,7 +99,7 @@ interface RegistrationBase<TOptions extends HotkeyOptions> {
83
99
  export interface HotkeyChordRegistration extends RegistrationBase<HotkeyChordOptions> {
84
100
  kind: 'chord';
85
101
  hotkey: HotkeyChord;
86
- parsedHotkey: ParsedHotkey;
102
+ parsedChord: ParsedHotkeyChord;
87
103
  /** The concrete key/code that activated a requireReset registration. */
88
104
  activeMatch?: {
89
105
  key: string;
@@ -94,7 +110,7 @@ export interface HotkeyChordRegistration extends RegistrationBase<HotkeyChordOpt
94
110
  export interface HotkeySequenceRegistration extends RegistrationBase<HotkeySequenceOptions> {
95
111
  kind: 'sequence';
96
112
  hotkey: HotkeySequence;
97
- parsedSequence: Array<ParsedHotkey>;
113
+ parsedSequence: Array<ParsedHotkeyChord>;
98
114
  /** Steps matched in the current attempt */
99
115
  currentIndex: number;
100
116
  /** `Date.now()` when the last step in the current attempt matched */
@@ -112,14 +128,14 @@ interface RegistrationViewBase {
112
128
  export interface HotkeyChordRegistrationView extends RegistrationViewBase {
113
129
  kind: 'chord';
114
130
  hotkey: HotkeyChord;
115
- parsedHotkey: ParsedHotkey;
131
+ parsedChord: ParsedHotkeyChord;
116
132
  options: HotkeyChordOptions;
117
133
  }
118
134
  /** Public view of a sequence registration, without its callback. */
119
135
  export interface HotkeySequenceRegistrationView extends RegistrationViewBase {
120
136
  kind: 'sequence';
121
137
  hotkey: HotkeySequence;
122
- parsedSequence: Array<ParsedHotkey>;
138
+ parsedSequence: Array<ParsedHotkeyChord>;
123
139
  options: HotkeySequenceOptions;
124
140
  /** Steps matched in the current attempt (0 when idle or just completed). */
125
141
  matchedStepCount: number;
@@ -172,6 +188,44 @@ export interface HotkeyRegistrationHandle<TOptions extends HotkeyOptions = Hotke
172
188
  /** Unregister this hotkey */
173
189
  unregister: () => void;
174
190
  }
191
+ /** Active hotkey scopes, published on `HotkeyManager.scopes`. */
192
+ export interface HotkeyScopesState {
193
+ /**
194
+ * Activated scopes, lowest precedence first. `'global'` is always active
195
+ * and not listed.
196
+ */
197
+ active: ReadonlyArray<string>;
198
+ /** The exclusive scope that owns the keyboard, or `null` when none is active. */
199
+ exclusive: string | null;
200
+ /**
201
+ * Scopes the exclusive scope allows that are active, possibly including
202
+ * `'global'`. Empty when no scope is exclusive.
203
+ */
204
+ allowed: ReadonlyArray<string>;
205
+ /**
206
+ * Scopes that are active but cannot fire because another scope is
207
+ * exclusive, possibly including `'global'`.
208
+ */
209
+ suspended: ReadonlyArray<string>;
210
+ }
211
+ /** Options for `HotkeyManager.activateScope`. */
212
+ export interface ActivateHotkeyScopeOptions {
213
+ /**
214
+ * Suspend every other scope, including `'global'`, until this activation is
215
+ * released. Scopes nested in this one stay active. Pass `{ allow }` to keep
216
+ * other scopes active too, such as `{ allow: ['global'] }`. An allowed scope
217
+ * still has to be activated, except `'global'`.
218
+ */
219
+ exclusive?: boolean | {
220
+ allow: ReadonlyArray<string>;
221
+ };
222
+ /**
223
+ * The scope this one is nested in. A nested scope takes precedence over its
224
+ * ancestors even when it was activated first, as nested providers are when
225
+ * their effects run child-first. Defaults to `'global'`.
226
+ */
227
+ parent?: string;
228
+ }
175
229
  /**
176
230
  * Singleton manager for hotkey registrations, including sequences.
177
231
  *
@@ -225,6 +279,8 @@ export declare class HotkeyManager {
225
279
  * devtools can list declared hotkeys that are not registered.
226
280
  */
227
281
  readonly appHotkeys: Store<AppHotkeysStore | null>;
282
+ /** Active scopes. Subscribe to show which hotkeys are suspended. */
283
+ readonly scopes: Store<HotkeyScopesState>;
228
284
  private constructor();
229
285
  /**
230
286
  * Gets the singleton instance of HotkeyManager.
@@ -234,17 +290,44 @@ export declare class HotkeyManager {
234
290
  * Resets the singleton instance. Useful for testing.
235
291
  */
236
292
  static resetInstance(): void;
293
+ /**
294
+ * Activates a scope until the returned function is called. Activations are
295
+ * counted, so a scope stays active until every activation is released.
296
+ * Framework providers call this while mounted.
297
+ *
298
+ * When active scopes share a hotkey, a nested scope takes precedence over
299
+ * its ancestors, and otherwise the most recently activated scope wins. The
300
+ * same order picks the exclusive scope when several are active, and only
301
+ * that scope's `allow` list applies.
302
+ *
303
+ * @example
304
+ * ```ts
305
+ * const release = manager.activateScope('dialog', { exclusive: true })
306
+ * // Only hotkeys with scope 'dialog', or scopes nested in it, fire now.
307
+ * release()
308
+ *
309
+ * manager.activateScope('dialog', { exclusive: { allow: ['global'] } })
310
+ * // Hotkeys with scope 'global' fire too.
311
+ * ```
312
+ */
313
+ activateScope(scope: string, options?: ActivateHotkeyScopeOptions): () => void;
314
+ /**
315
+ * Whether hotkeys in a scope can fire: the scope is `'global'` or
316
+ * activated, and either no scope is exclusive or this scope is the
317
+ * exclusive one, nested in it, or allowed by it.
318
+ */
319
+ isScopeActive(scope?: string): boolean;
237
320
  /**
238
321
  * Registers a hotkey handler and returns a handle for updating the registration.
239
322
  *
240
- * Pass a chord (`'Mod+S'` or a RawHotkey object) or a sequence (`['G', 'G']`).
323
+ * Pass a chord (`'Mod+S'` or a RawHotkeyChord object) or a sequence (`['G', 'G']`).
241
324
  * A one-step sequence registers as its chord. Chords accept `requireReset`;
242
325
  * sequences accept `timeout`.
243
326
  *
244
327
  * The returned handle allows updating the callback and options without
245
328
  * re-registering, which is useful for avoiding stale closures in React.
246
329
  *
247
- * @param hotkey - A chord string, RawHotkey object, or sequence
330
+ * @param hotkey - A chord string, RawHotkeyChord object, or sequence
248
331
  * @param callback - The function to call when the hotkey is pressed
249
332
  * @param options - Options for the hotkey behavior
250
333
  * @returns A handle for managing the registration
@@ -263,7 +346,7 @@ export declare class HotkeyManager {
263
346
  * handle.unregister()
264
347
  * ```
265
348
  */
266
- register<T extends RegisterableHotkey>(hotkey: T, callback: HotkeyCallback, options?: HotkeyOptionsFor<T>): HotkeyRegistrationHandle<HotkeyOptionsFor<T>>;
349
+ register<const T extends RegisterableHotkey>(hotkey: T, callback: HotkeyCallback<HotkeyCallbackContextFor<T>>, options?: HotkeyOptionsFor<T>): HotkeyRegistrationHandle<HotkeyOptionsFor<T>>;
267
350
  /**
268
351
  * Resets the progress of every in-progress sequence.
269
352
  */