@tanstack/hotkeys 0.14.0 → 0.15.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.
@@ -20,7 +20,8 @@ var TargetListeners = class {
20
20
  const listener = (eventType) => (rawEvent) => {
21
21
  const event = rawEvent;
22
22
  let processed = this.#processed.get(event);
23
- if (!processed || processed.groups.has(ids)) {
23
+ const first = !processed || processed.groups.has(ids);
24
+ if (!processed || first) {
24
25
  processed = {
25
26
  ids: /* @__PURE__ */ new Set(),
26
27
  groups: /* @__PURE__ */ new Set()
@@ -33,7 +34,7 @@ var TargetListeners = class {
33
34
  processed.ids.add(registrationId);
34
35
  pending.add(registrationId);
35
36
  }
36
- this.dispatch(event, target, eventType, capture, pending);
37
+ this.dispatch(event, target, eventType, capture, pending, first);
37
38
  };
38
39
  group = {
39
40
  ids,
@@ -5,7 +5,7 @@ import { isHotkeySequence, isModifierKey, parseHotkey, rawHotkeyToParsedHotkey,
5
5
  import { formatHotkey } from "./format.js";
6
6
  import { matchKeyboardEvent } from "./_match.js";
7
7
  import { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual } from "./match.js";
8
- import { isEventForTarget, shouldIgnoreInputEvent } from "./_event-target.js";
8
+ import { getTargetDocument, isEventForTarget, shouldIgnoreInputEvent } from "./_event-target.js";
9
9
  import { TargetListeners } from "./_target-listeners.js";
10
10
  import { isRecordingEvent } from "./_recording-guard.js";
11
11
  import { defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual } from "./_registration.js";
@@ -125,6 +125,10 @@ var HotkeyManager = class HotkeyManager {
125
125
  appHotkeys = new Store(null);
126
126
  #platform;
127
127
  #listeners = new TargetListeners((...args) => this.#processTargetEvent(...args));
128
+ /** Events that continue a sequence in progress, decided before any listener runs. */
129
+ #sequenceOwned = /* @__PURE__ */ new WeakMap();
130
+ /** Physical keys whose keydown a sequence owned, so their repeats and keyup are owned too. */
131
+ #sequenceOwnedKeys = /* @__PURE__ */ new Set();
128
132
  constructor() {
129
133
  this.#platform = detectPlatform();
130
134
  }
@@ -285,9 +289,11 @@ var HotkeyManager = class HotkeyManager {
285
289
  }
286
290
  /**
287
291
  * Processes keyboard events for a specific target and event type.
288
- * Chords and sequences match independently; both may fire for one event.
292
+ * A key that continues a sequence in progress belongs to that sequence, so
293
+ * chords skip it. Chords and first steps of sequences may both fire.
289
294
  */
290
- #processTargetEvent(event, target, eventType, capture, ids) {
295
+ #processTargetEvent(event, target, eventType, capture, ids, first) {
296
+ if (first) this.#sequenceOwned.set(event, this.#isOwnedBySequence(event, eventType));
291
297
  this.#processChords(event, target, eventType, capture, this.#registrationsIn(ids, capture, "chord"));
292
298
  this.#processSequences(event, target, eventType, capture, this.#registrationsIn(ids, capture, "sequence"));
293
299
  }
@@ -298,7 +304,7 @@ var HotkeyManager = class HotkeyManager {
298
304
  registration.activeMatch = void 0;
299
305
  }
300
306
  }
301
- if (isRecordingEvent(event)) return;
307
+ if (isRecordingEvent(event) || this.#sequenceOwned.get(event)) return;
302
308
  const matches = /* @__PURE__ */ new Map();
303
309
  let bestScore = 0;
304
310
  for (const registration of registrations) {
@@ -329,6 +335,34 @@ var HotkeyManager = class HotkeyManager {
329
335
  }
330
336
  }
331
337
  }
338
+ /**
339
+ * Whether the event continues or completes a sequence in progress whose
340
+ * target the event reaches. The repeats and keyup of a keydown that a
341
+ * sequence owns belong to the sequence too.
342
+ */
343
+ #isOwnedBySequence(event, eventType) {
344
+ const key = event.code || normalizeKeyName(event.key);
345
+ if (eventType === "keyup") {
346
+ const ownedKeydown = this.#sequenceOwnedKeys.delete(key);
347
+ return this.#continuesSequence(event, eventType) || ownedKeydown;
348
+ }
349
+ if (event.repeat) return this.#sequenceOwnedKeys.has(key);
350
+ const owned = this.#continuesSequence(event, eventType);
351
+ if (owned) this.#sequenceOwnedKeys.add(key);
352
+ else this.#sequenceOwnedKeys.delete(key);
353
+ return owned;
354
+ }
355
+ #continuesSequence(event, eventType) {
356
+ if (isRecordingEvent(event) || normalizeKeyboardEvent(event).isComposing || isModifierKey(normalizeKeyName(event.key))) return false;
357
+ const path = event.composedPath();
358
+ const now = Date.now();
359
+ for (const registration of this.registrations.state.values()) {
360
+ if (registration.kind !== "sequence" || registration.currentIndex === 0 || !registration.options.enabled || registration.options.eventType !== eventType || now - registration.lastKeyTime > (registration.options.timeout ?? 1e3)) continue;
361
+ const target = registration.target;
362
+ if (path.includes(target.nodeType === 1 ? target : getTargetDocument(target)) && !(ignoresInputs(registration) && shouldIgnoreInputEvent(event, target, target)) && matchKeyboardEvent(event, registration.parsedSequence[registration.currentIndex], registration.options.platform).matched) return true;
363
+ }
364
+ return false;
365
+ }
332
366
  #processSequences(event, target, eventType, capture, registrations) {
333
367
  if (registrations.length === 0) return;
334
368
  if (isRecordingEvent(event)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/hotkeys",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Type-safe, framework-agnostic keyboard hotkey management for the browser",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",