@tanstack/hotkeys 0.21.0 → 0.22.1

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.
@@ -22,7 +22,7 @@ function normalizeKeyboardEvent(event, platform) {
22
22
  meta: Boolean(event.metaKey),
23
23
  altGraph: altGraph && !controlAlt,
24
24
  location: event.location,
25
- isComposing: Boolean(event.isComposing) || key === "Process" || key === "Unidentified"
25
+ isComposing: Boolean(event.isComposing) || event.keyCode === 229 || key === "Process" || key === "Unidentified"
26
26
  };
27
27
  }
28
28
  /**
@@ -93,10 +93,15 @@ function isAsciiLetter(key) {
93
93
  function isLatinLetter(key) {
94
94
  return /^\p{Script_Extensions=Latin}$/u.test(key);
95
95
  }
96
+ let collator;
96
97
  /** Compares letters without case, preserving accents and avoiding expansions such as ß → SS. */
97
98
  function keysEqual(left, right) {
98
- if (/^\p{Letter}$/u.test(left) && /^\p{Letter}$/u.test(right)) return left.localeCompare(right, void 0, { sensitivity: "accent" }) === 0;
99
- return left === right;
99
+ if (left === right) return true;
100
+ if (/^\p{Letter}$/u.test(left) && /^\p{Letter}$/u.test(right)) {
101
+ collator ??= new Intl.Collator(void 0, { sensitivity: "accent" });
102
+ return collator.compare(left, right) === 0;
103
+ }
104
+ return false;
100
105
  }
101
106
  /** Rejects reserved physical-token syntax in logical object fields while allowing Unicode characters. */
102
107
  function assertLogicalKey(key) {
package/dist/_match.js CHANGED
@@ -11,7 +11,7 @@ function matchKeyboardEvent(event, hotkey, platform = detectPlatform()) {
11
11
  matched: false,
12
12
  score: 0
13
13
  };
14
- if (normalized.isComposing && (parsed.code !== void 0 || isPrintableKey(parsed.key))) return noMatch;
14
+ if (normalized.isComposing) return noMatch;
15
15
  if (normalized.altGraph && (parsed.ctrl || parsed.alt)) return noMatch;
16
16
  if (normalized.altGraph && !altGraphTypedCharacter(normalized)) return noMatch;
17
17
  if (parsed.code !== void 0) {
@@ -44,7 +44,7 @@ function withDefaults(defaults, options) {
44
44
  }
45
45
  /** Whether a parsed chord has a key. An empty one would match key-less events. */
46
46
  function chordHasKey(chord) {
47
- return chord.code !== void 0 || !!chord.key;
47
+ return !!(chord.code || chord.key);
48
48
  }
49
49
  /**
50
50
  * Resolves `ignoreInputs: 'auto'` from the hotkey (the first step of a sequence).
@@ -9,11 +9,7 @@ var TargetListeners = class {
9
9
  }
10
10
  add(target, capture, id) {
11
11
  if (typeof document === "undefined") return;
12
- let phases = this.#targets.get(target);
13
- if (!phases) {
14
- phases = /* @__PURE__ */ new Map();
15
- this.#targets.set(target, phases);
16
- }
12
+ const phases = this.#targets.get(target) ?? /* @__PURE__ */ new Map();
17
13
  let group = phases.get(capture);
18
14
  if (!group) {
19
15
  const ids = /* @__PURE__ */ new Set();
@@ -41,9 +37,10 @@ var TargetListeners = class {
41
37
  keydown: listener("keydown"),
42
38
  keyup: listener("keyup")
43
39
  };
44
- phases.set(capture, group);
45
40
  target.addEventListener("keydown", group.keydown, capture);
46
41
  target.addEventListener("keyup", group.keyup, capture);
42
+ phases.set(capture, group);
43
+ this.#targets.set(target, phases);
47
44
  }
48
45
  group.ids.add(id);
49
46
  }
package/dist/adapter.d.ts CHANGED
@@ -2,8 +2,8 @@ import { Store, createStore } from "./_store.js";
2
2
  import { HotkeyCallback, HotkeyChord, HotkeySequence, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence } from "./hotkey.types.js";
3
3
  import { ActivateHotkeyScopeOptions, HotkeyChordOptions, HotkeyOptions, HotkeySequenceOptions, Target } from "./hotkey-manager.js";
4
4
  import { AppHotkey, AppHotkeysStore } from "./app-hotkeys.js";
5
- import { createAppHotkeysStore } from "./_app-hotkeys-factory.js";
6
5
  import { definedOptions, withDefaults } from "./_registration.js";
6
+ import { createAppHotkeysStore } from "@tanstack/hotkeys";
7
7
  //#region src/adapter.d.ts
8
8
  /**
9
9
  * A hotkey and its callback, with optional per-binding options. Chords accept
package/dist/adapter.js CHANGED
@@ -1,10 +1,6 @@
1
- import { detectPlatform } from "./platform.js";
2
- import { isHotkeySequence, normalizeHotkey, parseHotkey } from "./parse.js";
3
1
  import { chordHasKey, defaultHotkeyOptions, definedOptions, withDefaults } from "./_registration.js";
4
2
  import { createStore } from "./_store.js";
5
- import { getHotkeyManager } from "./hotkey-manager.js";
6
- import { createAppHotkeysStore } from "./_app-hotkeys-factory.js";
7
- import { getAppHotkeyRegistrationOptions, isAppHotkey, resolveAppHotkey } from "./app-hotkeys.js";
3
+ import { createAppHotkeysStore, detectPlatform, getAppHotkeyRegistrationOptions, getHotkeyManager, isAppHotkey, isHotkeySequence, normalizeHotkey, parseHotkey, resolveAppHotkey } from "@tanstack/hotkeys";
8
4
 
9
5
  //#region src/adapter.ts
10
6
  /** Reconciles adapter registrations while retaining handles for option updates. */
@@ -57,6 +57,24 @@ function chordString(chord, parsed) {
57
57
  function generateId() {
58
58
  return `hotkey_${++registrationIdCounter}`;
59
59
  }
60
+ /** A handle for a registration that listens for nothing. */
61
+ function inactiveHandle(callback) {
62
+ let currentCallback = callback;
63
+ return {
64
+ id: generateId(),
65
+ unregister: () => {},
66
+ get callback() {
67
+ return currentCallback;
68
+ },
69
+ set callback(newCallback) {
70
+ currentCallback = newCallback;
71
+ },
72
+ setOptions: () => {},
73
+ get isActive() {
74
+ return false;
75
+ }
76
+ };
77
+ }
60
78
  /** Defaults for a registration of the given kind. */
61
79
  function registrationDefaults(kind) {
62
80
  return {
@@ -303,26 +321,8 @@ var HotkeyManager = class HotkeyManager {
303
321
  this.registrations.setState((prev) => new Map(prev));
304
322
  }
305
323
  register(input, callback, options = {}) {
306
- if (input == null) throw new Error("A hotkey is required. Skip registration when there is no hotkey.");
307
324
  const hotkey = simplifyHotkey(input);
308
- if (isHotkeySequence(hotkey) && hotkey.length === 0) throw new Error("Sequence must contain at least one hotkey");
309
- if (typeof document === "undefined" && !options.target) {
310
- let currentCallback = callback;
311
- return {
312
- id: generateId(),
313
- unregister: () => {},
314
- get callback() {
315
- return currentCallback;
316
- },
317
- set callback(newCallback) {
318
- currentCallback = newCallback;
319
- },
320
- setOptions: () => {},
321
- get isActive() {
322
- return false;
323
- }
324
- };
325
- }
325
+ if (hotkey == null || isHotkeySequence(hotkey) && hotkey.length === 0 || typeof document === "undefined" && !options.target) return inactiveHandle(callback);
326
326
  const id = generateId();
327
327
  const platform = options.platform ?? this.#platform;
328
328
  const target = options.target ?? document;
@@ -361,14 +361,14 @@ var HotkeyManager = class HotkeyManager {
361
361
  };
362
362
  }
363
363
  const steps = registration.kind === "sequence" ? registration.parsedSequence : [registration.parsedChord];
364
- if (!steps.every(chordHasKey)) throw new Error(`Hotkey '${formatForMessage(registration.hotkey)}' has no key. Skip registration when there is no hotkey.`);
364
+ if (!steps.every(chordHasKey)) return inactiveHandle(callback);
365
365
  if (process.env.NODE_ENV !== "production") {
366
366
  for (const { key } of steps) if (key !== void 0 && !isKeyNameShape(key)) console.warn(`Unknown key '${key}' in '${formatForMessage(registration.hotkey)}'. Key names are case-sensitive, such as 'Escape' and 'F1', so this hotkey may never fire.`);
367
367
  }
368
368
  const conflicting = this.#findConflictingRegistration(registration);
369
369
  if (conflicting) handleConflict(conflicting.id, formatForMessage(registration.hotkey), options.conflictBehavior ?? "warn", (conflictId) => this.#unregister(conflictId));
370
- this.registrations.setState((prev) => new Map(prev).set(id, registration));
371
370
  this.#listeners.add(target, !!registration.options.capture, id);
371
+ this.registrations.setState((prev) => new Map(prev).set(id, registration));
372
372
  const manager = this;
373
373
  return {
374
374
  get id() {
@@ -517,7 +517,7 @@ var HotkeyManager = class HotkeyManager {
517
517
  this.#fire(registration, event);
518
518
  continue;
519
519
  }
520
- if (!registration.options.requireReset || !registration.hasFired) {
520
+ if (!registration.options.requireReset || !registration.hasFired || !event.repeat) {
521
521
  if (registration.options.requireReset) {
522
522
  registration.hasFired = true;
523
523
  registration.activeMatch = match.identity;
@@ -648,13 +648,13 @@ var HotkeyManager = class HotkeyManager {
648
648
  };
649
649
  }
650
650
  /**
651
- * Finds an existing registration on the same target and scope whose steps
651
+ * Finds an existing registration on the same target, scope, and event type whose steps
652
652
  * resolve to the same identity. Each side keeps its own platform, so `Mod+S` on macOS
653
653
  * and `Mod+S` on Windows are different bindings.
654
654
  */
655
655
  #findConflictingRegistration(candidate) {
656
656
  const steps = resolvedSteps(candidate);
657
- for (const registration of this.registrations.state.values()) if (registration.target === candidate.target && registration.options.scope === candidate.options.scope && sameResolvedSteps(resolvedSteps(registration), steps)) return registration;
657
+ for (const registration of this.registrations.state.values()) if (registration.target === candidate.target && registration.options.scope === candidate.options.scope && registration.options.eventType === candidate.options.eventType && sameResolvedSteps(resolvedSteps(registration), steps)) return registration;
658
658
  return null;
659
659
  }
660
660
  /**
package/dist/index.d.ts CHANGED
@@ -13,4 +13,5 @@ import { HeldModifierOptions, matchesHeldModifiers } from "./hint.js";
13
13
  import { HotkeyConflict, HotkeyConflictOptions, findHotkeyConflicts } from "./conflicts.js";
14
14
  import { KeyStateTracker, KeyStateTrackerState, getKeyStateTracker } from "./key-state-tracker.js";
15
15
  import { HotkeyChordRecorderOptions, HotkeyChordRecorderValidationContext, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeySequenceRecorderOptions, HotkeySequenceRecorderValidationContext, RecorderKeyMode, RecorderOptions, RecorderRejection } from "./hotkey-recorder.js";
16
- export { ALL_KEYS, ActivateHotkeyScopeOptions, AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CanonicalModifier, ConflictBehavior, CreateHotkeyChordHandlerOptions, CustomHotkeys, DEFAULT_SEQUENCE_TIMEOUT, DefinedAppHotkeys, DisplayHotkey, DisplayHotkeyChord, EDITING_KEYS, EditingKey, FUNCTION_KEYS, FormatDisplayOptions, FourModifierHotkeyChord, FunctionKey, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyCallbackContextFor, HotkeyChord, HotkeyChordCallbackContext, HotkeyChordOptions, HotkeyChordRecorderOptions, HotkeyChordRecorderValidationContext, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyConflict, HotkeyConflictOptions, HotkeyManager, HotkeyMeta, HotkeyOptions, HotkeyOptionsFor, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationStatus, HotkeyRegistrationView, HotkeyScopesState, HotkeySequence, HotkeySequenceCallbackContext, HotkeySequenceOptions, HotkeySequenceRecorderOptions, HotkeySequenceRecorderValidationContext, HotkeySequenceRegistration, HotkeySequenceRegistrationView, IndividualKey, KEY_DISPLAY_SYMBOLS, Key, KeyStateTracker, KeyStateTrackerState, KeyboardEventMatch, LETTER_KEYS, LINUX_MODIFIER_LABELS, LetterKey, LogicalKey, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, Modifier, MultiHotkeyChordHandler, NAVIGATION_KEYS, NUMBER_KEYS, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, ParsedHotkeyChord, ParsedModifiers, PhysicalKey, PhysicalKeyCode, PunctuationKey, RawHotkeyChord, RawModifiers, RecorderKeyMode, RecorderOptions, RecorderRejection, Register, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence, RegisteredAppHotkeys, SingleModifierHotkeyChord, Target, ThreeModifierHotkeyChord, TwoModifierHotkeyChord, ValidatableHotkey, ValidationResult, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createHotkeyChordHandler, createMultiHotkeyChordHandler, createSequenceMatcher, defineAppHotkeys, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, getAppHotkeyRegistrationOptions, getHotkeyManager, getKeyStateTracker, hasNonModifierKey, isAppHotkey, isCustomHotkey, isHotkeySequence, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeKeyName, parseHotkey, parseKeyboardEvent, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey, resolveModifier, simplifyHotkey, toHotkeyRegistrationView, validateHotkey };
16
+ import { createAppHotkeysStore } from "./_app-hotkeys-factory.js";
17
+ export { ALL_KEYS, ActivateHotkeyScopeOptions, AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CanonicalModifier, ConflictBehavior, CreateHotkeyChordHandlerOptions, CustomHotkeys, DEFAULT_SEQUENCE_TIMEOUT, DefinedAppHotkeys, DisplayHotkey, DisplayHotkeyChord, EDITING_KEYS, EditingKey, FUNCTION_KEYS, FormatDisplayOptions, FourModifierHotkeyChord, FunctionKey, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyCallbackContextFor, HotkeyChord, HotkeyChordCallbackContext, HotkeyChordOptions, HotkeyChordRecorderOptions, HotkeyChordRecorderValidationContext, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyConflict, HotkeyConflictOptions, HotkeyManager, HotkeyMeta, HotkeyOptions, HotkeyOptionsFor, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationStatus, HotkeyRegistrationView, HotkeyScopesState, HotkeySequence, HotkeySequenceCallbackContext, HotkeySequenceOptions, HotkeySequenceRecorderOptions, HotkeySequenceRecorderValidationContext, HotkeySequenceRegistration, HotkeySequenceRegistrationView, IndividualKey, KEY_DISPLAY_SYMBOLS, Key, KeyStateTracker, KeyStateTrackerState, KeyboardEventMatch, LETTER_KEYS, LINUX_MODIFIER_LABELS, LetterKey, LogicalKey, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, Modifier, MultiHotkeyChordHandler, NAVIGATION_KEYS, NUMBER_KEYS, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, ParsedHotkeyChord, ParsedModifiers, PhysicalKey, PhysicalKeyCode, PunctuationKey, RawHotkeyChord, RawModifiers, RecorderKeyMode, RecorderOptions, RecorderRejection, Register, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence, RegisteredAppHotkeys, SingleModifierHotkeyChord, Target, ThreeModifierHotkeyChord, TwoModifierHotkeyChord, ValidatableHotkey, ValidationResult, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createAppHotkeysStore, createHotkeyChordHandler, createMultiHotkeyChordHandler, createSequenceMatcher, defineAppHotkeys, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, getAppHotkeyRegistrationOptions, getHotkeyManager, getKeyStateTracker, hasNonModifierKey, isAppHotkey, isCustomHotkey, isHotkeySequence, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeKeyName, parseHotkey, parseKeyboardEvent, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey, resolveModifier, simplifyHotkey, toHotkeyRegistrationView, validateHotkey };
package/dist/index.js CHANGED
@@ -8,8 +8,9 @@ import { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual, createHotkeyChordHandler, cr
8
8
  import { matchesHeldModifiers } from "./hint.js";
9
9
  import { HotkeyManager, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
10
10
  import { findHotkeyConflicts } from "./conflicts.js";
11
+ import { createAppHotkeysStore } from "./_app-hotkeys-factory.js";
11
12
  import { AppHotkeysStore, defineAppHotkeys, getAppHotkeyRegistrationOptions, isAppHotkey, isCustomHotkey, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey } from "./app-hotkeys.js";
12
13
  import { KeyStateTracker, getKeyStateTracker } from "./key-state-tracker.js";
13
14
  import { HotkeyRecorder } from "./hotkey-recorder.js";
14
15
 
15
- export { ALL_KEYS, AppHotkeysStore, DEFAULT_SEQUENCE_TIMEOUT, EDITING_KEYS, FUNCTION_KEYS, HotkeyManager, HotkeyRecorder, KEY_DISPLAY_SYMBOLS, KeyStateTracker, LETTER_KEYS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, NAVIGATION_KEYS, NUMBER_KEYS, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createHotkeyChordHandler, createMultiHotkeyChordHandler, createSequenceMatcher, defineAppHotkeys, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, getAppHotkeyRegistrationOptions, getHotkeyManager, getKeyStateTracker, hasNonModifierKey, isAppHotkey, isCustomHotkey, isHotkeySequence, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeKeyName, parseHotkey, parseKeyboardEvent, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey, resolveModifier, simplifyHotkey, toHotkeyRegistrationView, validateHotkey };
16
+ export { ALL_KEYS, AppHotkeysStore, DEFAULT_SEQUENCE_TIMEOUT, EDITING_KEYS, FUNCTION_KEYS, HotkeyManager, HotkeyRecorder, KEY_DISPLAY_SYMBOLS, KeyStateTracker, LETTER_KEYS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, NAVIGATION_KEYS, NUMBER_KEYS, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createAppHotkeysStore, createHotkeyChordHandler, createMultiHotkeyChordHandler, createSequenceMatcher, defineAppHotkeys, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, getAppHotkeyRegistrationOptions, getHotkeyManager, getKeyStateTracker, hasNonModifierKey, isAppHotkey, isCustomHotkey, isHotkeySequence, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeKeyName, parseHotkey, parseKeyboardEvent, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey, resolveModifier, simplifyHotkey, toHotkeyRegistrationView, validateHotkey };
@@ -100,11 +100,14 @@ var KeyStateTracker = class KeyStateTracker {
100
100
  for (const [entryIdentity, entry] of this.#heldEntries) if (entry.key === key) this.#heldEntries.delete(entryIdentity);
101
101
  }
102
102
  this.#reconcileModifiers(event);
103
- if (MODIFIER_KEYS.has(key)) {
104
- for (const [entryIdentity, entry] of this.#heldEntries) if (!MODIFIER_KEYS.has(entry.key)) this.#heldEntries.delete(entryIdentity);
105
- }
103
+ if (MODIFIER_KEYS.has(key)) this.#releaseNonModifiers();
106
104
  this.#syncState();
107
105
  };
106
+ const onContextMenu = (event) => {
107
+ if (event.defaultPrevented || !event.isTrusted) return;
108
+ const reconciled = this.#reconcileModifiers(event);
109
+ if (this.#releaseNonModifiers() || reconciled) this.#syncState();
110
+ };
108
111
  const onMouse = (event) => {
109
112
  if (this.#reconcileModifiers(event)) this.#syncState();
110
113
  };
@@ -139,6 +142,16 @@ var KeyStateTracker = class KeyStateTracker {
139
142
  "blur",
140
143
  onBlur,
141
144
  false
145
+ ],
146
+ [
147
+ "contextmenu",
148
+ onContextMenu,
149
+ false
150
+ ],
151
+ [
152
+ "visibilitychange",
153
+ () => document.hidden && onBlur(),
154
+ false
142
155
  ]
143
156
  ];
144
157
  for (const [type, listener, capture] of this.#listeners) window.addEventListener(type, listener, capture);
@@ -159,6 +172,15 @@ var KeyStateTracker = class KeyStateTracker {
159
172
  }
160
173
  return changed;
161
174
  }
175
+ /** Forgets every held key that isn't a modifier. Returns whether any was held. */
176
+ #releaseNonModifiers() {
177
+ let changed = false;
178
+ for (const [identity, entry] of this.#heldEntries) if (!MODIFIER_KEYS.has(entry.key)) {
179
+ this.#heldEntries.delete(identity);
180
+ changed = true;
181
+ }
182
+ return changed;
183
+ }
162
184
  /**
163
185
  * Syncs the internal Set to the Store state.
164
186
  */
@@ -50,9 +50,13 @@ export declare function assertValidHotkey(hotkey: ValidatableHotkey): void;
50
50
  *
51
51
  * @example
52
52
  * ```ts
53
- * checkHotkey('Alt+C')
54
- * // Console: Warning: Alt+C may not work reliably on macOS...
53
+ * checkHotkey('Control+escape')
54
+ * // Console: Hotkey 'Control+escape' warnings: Unknown key: 'escape'. Key names are case-sensitive, such as 'Escape' and 'F1'.
55
55
  * // Returns: true
56
+ *
57
+ * checkHotkey('Ctl+K')
58
+ * // Console: Hotkey 'Ctl+K' errors: Unknown modifier: 'Ctl'
59
+ * // Returns: false
56
60
  * ```
57
61
  */
58
62
  export declare function checkHotkey(hotkey: ValidatableHotkey): boolean;
package/dist/validate.js CHANGED
@@ -94,9 +94,13 @@ function assertValidHotkey(hotkey) {
94
94
  *
95
95
  * @example
96
96
  * ```ts
97
- * checkHotkey('Alt+C')
98
- * // Console: Warning: Alt+C may not work reliably on macOS...
97
+ * checkHotkey('Control+escape')
98
+ * // Console: Hotkey 'Control+escape' warnings: Unknown key: 'escape'. Key names are case-sensitive, such as 'Escape' and 'F1'.
99
99
  * // Returns: true
100
+ *
101
+ * checkHotkey('Ctl+K')
102
+ * // Console: Hotkey 'Ctl+K' errors: Unknown modifier: 'Ctl'
103
+ * // Returns: false
100
104
  * ```
101
105
  */
102
106
  function checkHotkey(hotkey) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/hotkeys",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
4
4
  "description": "Type-safe, framework-agnostic keyboard hotkey management for the browser",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -50,7 +50,7 @@ Use the first mechanism that fits:
50
50
  1. **Lifetime.** A hotkey registered in a component exists while that component is mounted. A dialog's `Escape` belongs in the dialog.
51
51
  2. **`enabled`.** A condition inside a mounted component.
52
52
  3. **Scope.** A group that turns on and off together, such as a dialog, pane, or tab. An `exclusive` scope suspends every other scope, including global hotkeys, while it is active.
53
- 4. **`target`.** Only while focus is inside one element, which must be focusable (`tabindex="0"`).
53
+ 4. **`target`.** Only while focus is on one element or inside it. If nothing there can take focus, give the element `tabindex="0"`.
54
54
 
55
55
  When nesting scopes, stacking dialogs, or keeping some hotkeys working during an exclusive scope, read [scopes](references/scopes.md).
56
56
 
@@ -4,4 +4,4 @@
4
4
  - **Reset between tests.** Call `HotkeyManager.resetInstance()`, plus `KeyStateTracker.resetInstance()` for held-key tests, in `afterEach`. Registrations and scope activations otherwise outlive a test that fails before unmounting.
5
5
  - **Dispatch real events.** Use `new KeyboardEvent('keydown', { key: 's', code: 'KeyS', metaKey: true, bubbles: true, cancelable: true })`. Physical bindings such as `[KeyS]` match only `code`. Check `event.defaultPrevented` to assert the browser action was blocked.
6
6
  - **Dispatch where focus is.** For a hotkey with a `target`, or to test typing in a field, focus the element and dispatch on it with `bubbles: true`.
7
- - **Repeats and sequences.** `repeat: true` simulates holding a key; it fires chords again unless they set `requireReset`, and it never advances a sequence. Use fake timers to test a sequence's `timeout` (1000 ms by default).
7
+ - **Repeats and sequences.** `repeat: true` simulates holding a key; it fires chords again unless they set `requireReset`, and it never advances a sequence. A keydown without `repeat` is a new press, so it fires a `requireReset` chord even without a keyup in between. Use fake timers to test a sequence's `timeout` (1000 ms by default).