@tanstack/hotkeys 0.17.0 → 0.18.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.
@@ -17,6 +17,17 @@ function getTargetDocument(target) {
17
17
  return target.ownerDocument;
18
18
  }
19
19
  /**
20
+ * Whether key presses reach a listener on `target` right now. Document and
21
+ * window listeners receive every key press in their document. An element
22
+ * receives them only while focus is inside it, including inside a shadow
23
+ * root it contains.
24
+ */
25
+ function targetHasFocus(target) {
26
+ if (isWindow(target) || target.nodeType === 9) return true;
27
+ const active = target.getRootNode().activeElement;
28
+ return !!active && target.contains(active);
29
+ }
30
+ /**
20
31
  * Checks if an element is an input-like element that should be ignored for hotkeys.
21
32
  *
22
33
  * This includes:
@@ -191,4 +202,4 @@ function isEventForTarget(event, target) {
191
202
  }
192
203
 
193
204
  //#endregion
194
- export { getActiveElementForListenerTarget, getTargetDocument, isEventForTarget, isInputElement, isTypingEvent, shouldIgnoreInputEvent };
205
+ export { getActiveElementForListenerTarget, getTargetDocument, isEventForTarget, isInputElement, isTypingEvent, shouldIgnoreInputEvent, targetHasFocus };
@@ -227,13 +227,12 @@ var AppHotkeysStore = class {
227
227
  if (list) list.push(registration);
228
228
  else byId.set(id, [registration]);
229
229
  }
230
- const manager = getHotkeyManager();
231
230
  return Object.values(this.store.state.appHotkeys).map((appHotkey) => {
232
231
  const registered = byId.get(appHotkey.id) ?? [];
233
- const enabled = registered.filter((r) => r.options.enabled !== false);
232
+ const has = (status) => registered.some((r) => r.status === status);
234
233
  return {
235
234
  appHotkey,
236
- status: appHotkey.hotkey === null ? "unbound" : registered.length === 0 ? "inactive" : enabled.length === 0 ? "disabled" : enabled.some((r) => manager.isScopeActive(r.options.scope)) ? "active" : "suspended",
235
+ status: appHotkey.hotkey === null ? "unbound" : registered.length === 0 ? "inactive" : has("active") ? "active" : has("suspended") ? "suspended" : "disabled",
237
236
  isCustom: isCustomHotkey(appHotkey, this.#platform),
238
237
  registrations: registered
239
238
  };
@@ -1,5 +1,5 @@
1
1
  import { ConflictBehavior, HotkeyCallback, HotkeyCallbackContextFor, HotkeyChord, HotkeyMeta, HotkeySequence, ParsedHotkeyChord, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence } from "./hotkey.types.js";
2
- import { AppHotkeysStore } from "./app-hotkeys.js";
2
+ import { AppHotkey, AppHotkeysStore } from "./app-hotkeys.js";
3
3
  import { Store } from "@tanstack/store";
4
4
  //#region src/hotkey-manager.d.ts
5
5
  /** An element, document, or window that hotkeys can listen on. */
@@ -41,9 +41,14 @@ export interface HotkeyOptions {
41
41
  platform?: 'mac' | 'windows' | 'linux';
42
42
  /**
43
43
  * Prevent the default browser action when the hotkey matches. For a
44
- * sequence, every step that advances it does. Defaults to true
44
+ * sequence, every step that advances it does. Defaults to true.
45
+ *
46
+ * `'always'` also prevents it while a chord can't fire because it is
47
+ * disabled or its scope is inactive or suspended, unless another hotkey
48
+ * handles the key. It never stops propagation for such a chord. Sequences
49
+ * treat `'always'` as `true`.
45
50
  */
46
- preventDefault?: boolean;
51
+ preventDefault?: boolean | 'always';
47
52
  /**
48
53
  * Stop event propagation when the hotkey matches. For a sequence, only the
49
54
  * last step does, so earlier steps reach other listeners. Defaults to true
@@ -118,8 +123,18 @@ export interface HotkeySequenceRegistration extends RegistrationBase<HotkeySeque
118
123
  }
119
124
  /** A registered hotkey in the HotkeyManager. */
120
125
  export type HotkeyRegistration = HotkeyChordRegistration | HotkeySequenceRegistration;
126
+ /**
127
+ * Whether a registration is on. Focus isn't checked: a registration on an
128
+ * element also needs focus inside that element to fire.
129
+ * - `'active'`: enabled, and its scope is active
130
+ * - `'suspended'`: enabled, but its scope is inactive or suspended by an exclusive scope
131
+ * - `'disabled'`: registered with `enabled: false`
132
+ */
133
+ export type HotkeyRegistrationStatus = 'active' | 'suspended' | 'disabled';
121
134
  interface RegistrationViewBase {
122
135
  id: string;
136
+ /** Whether the registration is enabled and in an active scope. */
137
+ status: HotkeyRegistrationStatus;
123
138
  target: Target;
124
139
  triggerCount: number;
125
140
  hasFired: boolean;
@@ -372,6 +387,17 @@ export declare class HotkeyManager {
372
387
  * @returns True if a matching registration exists
373
388
  */
374
389
  isRegistered(hotkey: RegisterableHotkey, target?: Target): boolean;
390
+ /**
391
+ * Checks whether a hotkey can fire now: at least one registration is
392
+ * enabled, in an active scope, and listening where key presses go. A
393
+ * registration on `document` or `window` always is; one on an element is
394
+ * only while focus is inside that element. An app hotkey matches
395
+ * registrations by id, so custom hotkeys apply.
396
+ *
397
+ * @param hotkey - A chord, sequence, or app hotkey
398
+ * @returns True if a registration that can fire exists
399
+ */
400
+ hasActiveRegistration(hotkey: RegisterableHotkey | AppHotkey): boolean;
375
401
  /**
376
402
  * Destroys the manager and removes all listeners.
377
403
  */
@@ -5,7 +5,7 @@ import { isHotkeySequence, isModifierKey, parseHotkey, simplifyHotkey } from "./
5
5
  import { formatHotkey } from "./format.js";
6
6
  import { matchKeyboardEvent, parsedChordsEqual } from "./_match.js";
7
7
  import { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual } from "./match.js";
8
- import { getTargetDocument, isEventForTarget, shouldIgnoreInputEvent } from "./_event-target.js";
8
+ import { getTargetDocument, isEventForTarget, shouldIgnoreInputEvent, targetHasFocus } from "./_event-target.js";
9
9
  import { TargetListeners } from "./_target-listeners.js";
10
10
  import { managerRef } from "./_manager-ref.js";
11
11
  import { isRecordingEvent } from "./_recording-guard.js";
@@ -13,10 +13,16 @@ import { GLOBAL_SCOPE, defaultHotkeyOptions, getDefaultIgnoreInputs, handleConfl
13
13
  import { Store } from "@tanstack/store";
14
14
 
15
15
  //#region src/hotkey-manager.ts
16
+ /** Whether a registration can fire now. Without a manager, scopes don't apply. */
17
+ function getRegistrationStatus(options, manager) {
18
+ if (options.enabled === false) return "disabled";
19
+ return manager?.isScopeActive(options.scope) === false ? "suspended" : "active";
20
+ }
16
21
  /** Creates a public view from an internal registration, stripping the callback. */
17
22
  function toHotkeyRegistrationView(reg) {
18
23
  const base = {
19
24
  id: reg.id,
25
+ status: getRegistrationStatus(reg.options, managerRef.current),
20
26
  target: reg.target,
21
27
  triggerCount: reg.triggerCount,
22
28
  hasFired: reg.hasFired
@@ -270,7 +276,20 @@ var HotkeyManager = class HotkeyManager {
270
276
  }
271
277
  /** Whether a registration can fire now: enabled, in an active scope. */
272
278
  #isLive(registration) {
273
- return registration.options.enabled !== false && this.isScopeActive(registration.options.scope);
279
+ return getRegistrationStatus(registration.options, this) === "active";
280
+ }
281
+ /**
282
+ * Whether a hotkey that can fire would handle this key on any listener the
283
+ * event reaches, including other targets and phases. A sequence counts when
284
+ * the key matches its next step or its first step.
285
+ */
286
+ #handledByLiveHotkey(event, eventType) {
287
+ const path = event.composedPath();
288
+ for (const registration of this.registrations.state.values()) {
289
+ if (!this.#isLive(registration) || registration.options.eventType !== eventType || !path.includes(registration.target) || ignoresInputs(registration) && shouldIgnoreInputEvent(event, registration.target, registration.target)) continue;
290
+ if ((registration.kind === "chord" ? [registration.parsedChord] : [registration.parsedSequence[registration.currentIndex], registration.parsedSequence[0]]).some((step) => matchKeyboardEvent(event, step, registration.options.platform).matched)) return true;
291
+ }
292
+ return false;
274
293
  }
275
294
  /** Notifies subscribers after in-place updates such as counts and sequence progress. */
276
295
  #notify() {
@@ -455,14 +474,20 @@ var HotkeyManager = class HotkeyManager {
455
474
  if (isRecordingEvent(event) || this.#sequenceOwned.get(event)) return;
456
475
  const matches = /* @__PURE__ */ new Map();
457
476
  let bestScore = 0;
477
+ let reserved = false;
458
478
  for (const registration of registrations) {
459
- if (!this.#isLive(registration) || registration.options.eventType !== eventType || !isEventForTarget(event, target) || ignoresInputs(registration) && shouldIgnoreInputEvent(event, target, registration.target)) continue;
479
+ const live = this.#isLive(registration);
480
+ if (!live && registration.options.preventDefault !== "always" || registration.options.eventType !== eventType || !isEventForTarget(event, target) || ignoresInputs(registration) && shouldIgnoreInputEvent(event, target, registration.target)) continue;
460
481
  const result = matchKeyboardEvent(event, registration.parsedChord, registration.options.platform);
461
- if (result.matched) {
462
- matches.set(registration.id, result);
463
- bestScore = Math.max(bestScore, result.score);
482
+ if (!result.matched) continue;
483
+ if (!live) {
484
+ reserved = true;
485
+ continue;
464
486
  }
487
+ matches.set(registration.id, result);
488
+ bestScore = Math.max(bestScore, result.score);
465
489
  }
490
+ if (reserved && matches.size === 0 && !this.#handledByLiveHotkey(event, eventType)) event.preventDefault();
466
491
  let bestRank = -1;
467
492
  for (const registration of registrations) if (matches.get(registration.id)?.score === bestScore) bestRank = Math.max(bestRank, this.#scopeRank(registration.options.scope));
468
493
  for (const snapshot of registrations) {
@@ -679,6 +704,21 @@ var HotkeyManager = class HotkeyManager {
679
704
  return false;
680
705
  }
681
706
  /**
707
+ * Checks whether a hotkey can fire now: at least one registration is
708
+ * enabled, in an active scope, and listening where key presses go. A
709
+ * registration on `document` or `window` always is; one on an element is
710
+ * only while focus is inside that element. An app hotkey matches
711
+ * registrations by id, so custom hotkeys apply.
712
+ *
713
+ * @param hotkey - A chord, sequence, or app hotkey
714
+ * @returns True if a registration that can fire exists
715
+ */
716
+ hasActiveRegistration(hotkey) {
717
+ const id = typeof hotkey === "object" && "~appHotkey" in hotkey ? hotkey.id : void 0;
718
+ for (const registration of this.registrations.state.values()) if (this.#isLive(registration) && targetHasFocus(registration.target) && (id === void 0 ? areHotkeysEqual(registration.hotkey, hotkey, registration.options.platform) : registration.options.appHotkeyId === id)) return true;
719
+ return false;
720
+ }
721
+ /**
682
722
  * Destroys the manager and removes all listeners.
683
723
  */
684
724
  destroy() {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CanonicalModifier, EditingKey, FunctionKey, IndividualKey, Key, LetterKey, LogicalKey, Modifier, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PhysicalKey, PhysicalKeyCode, PunctuationKey } from "./key.types.js";
2
2
  import { ConflictBehavior, FourModifierHotkeyChord, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyCallbackContextFor, HotkeyChord, HotkeyChordCallbackContext, HotkeyMeta, HotkeySequence, HotkeySequenceCallbackContext, ParsedHotkeyChord, ParsedModifiers, RawHotkeyChord, RawModifiers, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence, SingleModifierHotkeyChord, ThreeModifierHotkeyChord, TwoModifierHotkeyChord } from "./hotkey.types.js";
3
- import { ActivateHotkeyScopeOptions, HotkeyChordOptions, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyManager, HotkeyOptions, HotkeyOptionsFor, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, HotkeyScopesState, HotkeySequenceOptions, HotkeySequenceRegistration, HotkeySequenceRegistrationView, Target, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
3
+ import { ActivateHotkeyScopeOptions, HotkeyChordOptions, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyManager, HotkeyOptions, HotkeyOptionsFor, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationStatus, HotkeyRegistrationView, HotkeyScopesState, HotkeySequenceOptions, HotkeySequenceRegistration, HotkeySequenceRegistrationView, Target, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
4
4
  import { AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CustomHotkeys, DefinedAppHotkeys, Register, RegisteredAppHotkeys, defineAppHotkeys, getAppHotkeyRegistrationOptions, isAppHotkey, isCustomHotkey, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey } from "./app-hotkeys.js";
5
5
  import { ALL_KEYS, EDITING_KEYS, FUNCTION_KEYS, LETTER_KEYS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, NAVIGATION_KEYS, NUMBER_KEYS, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, isSingleLetterKey, normalizeKeyName } from "./constants.js";
6
6
  import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS } from "./display-labels.js";
@@ -13,4 +13,4 @@ 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, 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
+ 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/hotkeys",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Type-safe, framework-agnostic keyboard hotkey management for the browser",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -56,7 +56,7 @@ When nesting scopes, stacking dialogs, or keeping some hotkeys working during an
56
56
 
57
57
  ## Pitfalls
58
58
 
59
- - **A disabled hotkey doesn't block the browser.** With `enabled: false`, `Mod+S` opens the Save Page dialog. To skip the action, keep the hotkey enabled and return early from the callback.
59
+ - **A disabled hotkey doesn't block the browser.** With `enabled: false`, `Mod+S` opens the Save Page dialog. Add `preventDefault: 'always'` to keep the action blocked while the hotkey can't fire, whether it's disabled or its scope is off. Skip it for hotkeys turned off to give the key back to the browser, such as `Mod+C`, `Mod+Z`, `Tab`, or single keys behind a user's off switch. Don't keep the hotkey enabled and return early from the callback; the hotkey would also stay `active` in app hotkey lists and show its hint.
60
60
  - **Modifiers match exactly.** `ArrowDown` never fires while Shift is held, so register `Shift+ArrowDown` instead of checking `event.shiftKey`. Shift that a symbol needs is the exception: `?` matches Shift+/.
61
61
  - **Chords repeat while held.** Set `requireReset: true` on toggles, or holding the keys flickers them.
62
62
  - **Letters follow the typed character.** For position-based controls such as WASD, use physical bindings such as `'[KeyW]'`. Keep letters for mnemonic commands such as `Mod+S`.
@@ -9,3 +9,5 @@
9
9
  - A hotkey's `scope` option, or an app hotkey's declared `scope`, overrides the provider's.
10
10
  - Scopes follow the component tree, not the DOM, so a portaled dialog keeps its scope.
11
11
  - The hotkey that opens an exclusive dialog is suspended with the rest of the page. To close the dialog with the same keys, register that hotkey inside the dialog too.
12
+ - To show which scope owns the keyboard, use the adapter's scope state API, such as `useHotkeyScopesState()` in React. Don't subscribe to `manager.scopes` by hand.
13
+ - To list only hotkeys that are on, filter the adapter's registrations by `status === 'active'`. `'suspended'` means the hotkey's scope is inactive or suspended, and `'disabled'` means `enabled: false`.