@tanstack/preact-hotkeys 0.13.1 → 0.14.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,119 +0,0 @@
1
- import { useDefaultHotkeysOptions } from "./HotkeysProvider.js";
2
- import { isRef } from "./utils.js";
3
- import { formatHotkeySequence, getSequenceManager } from "@tanstack/hotkeys";
4
- import { useEffect, useRef } from "preact/hooks";
5
-
6
- //#region src/useHotkeySequence.ts
7
- /**
8
- * Preact hook for registering a keyboard shortcut sequence (Vim-style).
9
- *
10
- * This hook allows you to register multi-key sequences like 'g g' or 'd d'
11
- * that trigger when the full sequence is pressed within a timeout.
12
- *
13
- * Each step may include modifiers. You can chain the same modifier across
14
- * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,
15
- * Control, Alt, or Meta pressed alone) are ignored while matching—they do not
16
- * advance the sequence or reset progress.
17
- *
18
- * @param sequence - Array of hotkey strings that form the sequence
19
- * @param callback - Function to call when the sequence is completed
20
- * @param options - Options for the sequence behavior. `enabled: false` keeps the registration (visible in devtools)
21
- * and only suppresses firing; the hook updates the existing handle instead of unregistering.
22
- *
23
- * @example
24
- * ```tsx
25
- * function VimEditor() {
26
- * // 'g g' to go to top
27
- * useHotkeySequence(['G', 'G'], () => {
28
- * scrollToTop()
29
- * })
30
- *
31
- * // 'd d' to delete line
32
- * useHotkeySequence(['D', 'D'], () => {
33
- * deleteLine()
34
- * })
35
- *
36
- * // 'd i w' to delete inner word
37
- * useHotkeySequence(['D', 'I', 'W'], () => {
38
- * deleteInnerWord()
39
- * }, { timeout: 500 })
40
- *
41
- * // Same modifier on consecutive steps (bare Shift between chords is ignored)
42
- * useHotkeySequence(['Shift+R', 'Shift+T'], () => {
43
- * nextAction()
44
- * })
45
- *
46
- * return <div>...</div>
47
- * }
48
- * ```
49
- */
50
- function useHotkeySequence(sequence, callback, options = {}) {
51
- const mergedOptions = {
52
- ...useDefaultHotkeysOptions().hotkeySequence,
53
- ...options
54
- };
55
- const manager = getSequenceManager();
56
- const registrationRef = useRef(null);
57
- const callbackRef = useRef(callback);
58
- const optionsRef = useRef(mergedOptions);
59
- const managerRef = useRef(manager);
60
- const sequenceRef = useRef(sequence);
61
- callbackRef.current = callback;
62
- optionsRef.current = mergedOptions;
63
- managerRef.current = manager;
64
- sequenceRef.current = sequence;
65
- const prevTargetRef = useRef(null);
66
- const prevSequenceRef = useRef(null);
67
- const hotkeySequenceString = formatHotkeySequence(sequence);
68
- const { target: _target, ...optionsWithoutTarget } = mergedOptions;
69
- useEffect(() => {
70
- if (sequenceRef.current.length === 0) {
71
- if (registrationRef.current?.isActive) {
72
- registrationRef.current.unregister();
73
- registrationRef.current = null;
74
- }
75
- prevTargetRef.current = null;
76
- prevSequenceRef.current = null;
77
- return;
78
- }
79
- const resolvedTarget = isRef(optionsRef.current.target) ? optionsRef.current.target.current : optionsRef.current.target ?? (typeof document !== "undefined" ? document : null);
80
- if (!resolvedTarget) {
81
- if (registrationRef.current?.isActive) {
82
- registrationRef.current.unregister();
83
- registrationRef.current = null;
84
- }
85
- prevTargetRef.current = null;
86
- prevSequenceRef.current = null;
87
- return;
88
- }
89
- const targetChanged = prevTargetRef.current !== null && prevTargetRef.current !== resolvedTarget;
90
- const sequenceChanged = prevSequenceRef.current !== null && prevSequenceRef.current !== hotkeySequenceString;
91
- if (registrationRef.current?.isActive && (targetChanged || sequenceChanged)) {
92
- registrationRef.current.unregister();
93
- registrationRef.current = null;
94
- }
95
- if (!registrationRef.current || !registrationRef.current.isActive) registrationRef.current = managerRef.current.register(sequenceRef.current, (event, context) => callbackRef.current(event, context), {
96
- ...optionsRef.current,
97
- target: resolvedTarget
98
- });
99
- prevTargetRef.current = resolvedTarget;
100
- prevSequenceRef.current = hotkeySequenceString;
101
- });
102
- useEffect(() => {
103
- return () => {
104
- if (registrationRef.current?.isActive) {
105
- registrationRef.current.unregister();
106
- registrationRef.current = null;
107
- }
108
- };
109
- }, []);
110
- useEffect(() => {
111
- if (registrationRef.current?.isActive) {
112
- registrationRef.current.callback = (event, context) => callbackRef.current(event, context);
113
- registrationRef.current.setOptions(optionsWithoutTarget);
114
- }
115
- });
116
- }
117
-
118
- //#endregion
119
- export { useHotkeySequence };
@@ -1,16 +0,0 @@
1
- import { HotkeySequence, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
2
- //#region src/useHotkeySequenceRecorder.d.ts
3
- export interface PreactHotkeySequenceRecorder {
4
- isRecording: boolean;
5
- steps: HotkeySequence;
6
- recordedSequence: HotkeySequence | null;
7
- startRecording: () => void;
8
- stopRecording: () => void;
9
- cancelRecording: () => void;
10
- commitRecording: () => void;
11
- }
12
- /**
13
- * Preact hook for recording multi-chord sequences (Vim-style shortcuts).
14
- */
15
- export declare function useHotkeySequenceRecorder(options: HotkeySequenceRecorderOptions): PreactHotkeySequenceRecorder;
16
- //#endregion
@@ -1,38 +0,0 @@
1
- import { useDefaultHotkeysOptions } from "./HotkeysProvider.js";
2
- import { HotkeySequenceRecorder } from "@tanstack/hotkeys";
3
- import { useEffect, useRef } from "preact/hooks";
4
- import { useSelector } from "@tanstack/preact-store";
5
-
6
- //#region src/useHotkeySequenceRecorder.ts
7
- /**
8
- * Preact hook for recording multi-chord sequences (Vim-style shortcuts).
9
- */
10
- function useHotkeySequenceRecorder(options) {
11
- const mergedOptions = {
12
- ...useDefaultHotkeysOptions().hotkeySequenceRecorder,
13
- ...options
14
- };
15
- const recorderRef = useRef(null);
16
- if (!recorderRef.current) recorderRef.current = new HotkeySequenceRecorder(mergedOptions);
17
- recorderRef.current.setOptions(mergedOptions);
18
- const isRecording = useSelector(recorderRef.current.store, (state) => state.isRecording);
19
- const steps = useSelector(recorderRef.current.store, (state) => state.steps);
20
- const recordedSequence = useSelector(recorderRef.current.store, (state) => state.recordedSequence);
21
- useEffect(() => {
22
- return () => {
23
- recorderRef.current?.destroy();
24
- };
25
- }, []);
26
- return {
27
- isRecording,
28
- steps,
29
- recordedSequence,
30
- startRecording: () => recorderRef.current?.start(),
31
- stopRecording: () => recorderRef.current?.stop(),
32
- cancelRecording: () => recorderRef.current?.cancel(),
33
- commitRecording: () => recorderRef.current?.commit()
34
- };
35
- }
36
-
37
- //#endregion
38
- export { useHotkeySequenceRecorder };
@@ -1,60 +0,0 @@
1
- import { UseHotkeySequenceOptions } from "./useHotkeySequence.js";
2
- import { HotkeyCallback, HotkeySequence } from "@tanstack/hotkeys";
3
- //#region src/useHotkeySequences.d.ts
4
- /**
5
- * A single sequence definition for use with `useHotkeySequences`.
6
- */
7
- export interface UseHotkeySequenceDefinition {
8
- /** Array of hotkey strings that form the sequence */
9
- sequence: HotkeySequence;
10
- /** The function to call when the sequence is completed */
11
- callback: HotkeyCallback;
12
- /** Per-sequence options (merged on top of commonOptions) */
13
- options?: UseHotkeySequenceOptions;
14
- }
15
- /**
16
- * Preact hook for registering multiple keyboard shortcut sequences at once (Vim-style).
17
- *
18
- * Uses the singleton SequenceManager. Accepts a dynamic array of definitions so you can
19
- * register variable-length lists without violating the rules of hooks.
20
- *
21
- * Options are merged in this order:
22
- * HotkeysProvider defaults < commonOptions < per-definition options
23
- *
24
- * Callbacks and options are synced on every render to avoid stale closures.
25
- *
26
- * Definitions with an empty `sequence` are skipped (no registration).
27
- *
28
- * @param definitions - Array of sequence definitions to register
29
- * @param commonOptions - Shared options applied to all sequences (overridden by per-definition options).
30
- * Per-row `enabled: false` still registers that sequence: `SequenceManager` suppresses execution only (the row
31
- * stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle
32
- * via `setOptions` (no unregister/re-register churn).
33
- *
34
- * @example
35
- * ```tsx
36
- * function VimPalette() {
37
- * useHotkeySequences([
38
- * { sequence: ['G', 'G'], callback: () => scrollToTop() },
39
- * { sequence: ['D', 'D'], callback: () => deleteLine() },
40
- * { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord(), options: { timeout: 500 } },
41
- * ])
42
- * }
43
- * ```
44
- *
45
- * @example
46
- * ```tsx
47
- * function DynamicSequences({ items }) {
48
- * useHotkeySequences(
49
- * items.map((item) => ({
50
- * sequence: item.chords,
51
- * callback: item.action,
52
- * options: { enabled: item.enabled },
53
- * })),
54
- * { preventDefault: true },
55
- * )
56
- * }
57
- * ```
58
- */
59
- export declare function useHotkeySequences(definitions: Array<UseHotkeySequenceDefinition>, commonOptions?: UseHotkeySequenceOptions): void;
60
- //#endregion
@@ -1,125 +0,0 @@
1
- import { useDefaultHotkeysOptions } from "./HotkeysProvider.js";
2
- import { isRef } from "./utils.js";
3
- import { formatHotkeySequence, getSequenceManager } from "@tanstack/hotkeys";
4
- import { useEffect, useRef } from "preact/hooks";
5
-
6
- //#region src/useHotkeySequences.ts
7
- /**
8
- * Preact hook for registering multiple keyboard shortcut sequences at once (Vim-style).
9
- *
10
- * Uses the singleton SequenceManager. Accepts a dynamic array of definitions so you can
11
- * register variable-length lists without violating the rules of hooks.
12
- *
13
- * Options are merged in this order:
14
- * HotkeysProvider defaults < commonOptions < per-definition options
15
- *
16
- * Callbacks and options are synced on every render to avoid stale closures.
17
- *
18
- * Definitions with an empty `sequence` are skipped (no registration).
19
- *
20
- * @param definitions - Array of sequence definitions to register
21
- * @param commonOptions - Shared options applied to all sequences (overridden by per-definition options).
22
- * Per-row `enabled: false` still registers that sequence: `SequenceManager` suppresses execution only (the row
23
- * stays in the store and appears in TanStack Hotkeys devtools). Toggling `enabled` updates the existing handle
24
- * via `setOptions` (no unregister/re-register churn).
25
- *
26
- * @example
27
- * ```tsx
28
- * function VimPalette() {
29
- * useHotkeySequences([
30
- * { sequence: ['G', 'G'], callback: () => scrollToTop() },
31
- * { sequence: ['D', 'D'], callback: () => deleteLine() },
32
- * { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord(), options: { timeout: 500 } },
33
- * ])
34
- * }
35
- * ```
36
- *
37
- * @example
38
- * ```tsx
39
- * function DynamicSequences({ items }) {
40
- * useHotkeySequences(
41
- * items.map((item) => ({
42
- * sequence: item.chords,
43
- * callback: item.action,
44
- * options: { enabled: item.enabled },
45
- * })),
46
- * { preventDefault: true },
47
- * )
48
- * }
49
- * ```
50
- */
51
- function useHotkeySequences(definitions, commonOptions = {}) {
52
- const defaultOptions = useDefaultHotkeysOptions().hotkeySequence;
53
- const manager = getSequenceManager();
54
- const registrationsRef = useRef(/* @__PURE__ */ new Map());
55
- const definitionsRef = useRef(definitions);
56
- const sequenceStringsRef = useRef([]);
57
- const commonOptionsRef = useRef(commonOptions);
58
- const defaultOptionsRef = useRef(defaultOptions);
59
- const managerRef = useRef(manager);
60
- const sequenceStrings = definitions.map((def) => formatHotkeySequence(def.sequence));
61
- definitionsRef.current = definitions;
62
- sequenceStringsRef.current = sequenceStrings;
63
- commonOptionsRef.current = commonOptions;
64
- defaultOptionsRef.current = defaultOptions;
65
- managerRef.current = manager;
66
- useEffect(() => {
67
- const prevRegistrations = registrationsRef.current;
68
- const nextRegistrations = /* @__PURE__ */ new Map();
69
- const rows = [];
70
- for (let i = 0; i < definitionsRef.current.length; i++) {
71
- const def = definitionsRef.current[i];
72
- const seqStr = sequenceStringsRef.current[i];
73
- const seq = def.sequence;
74
- if (seq.length === 0) continue;
75
- const mergedOptions = {
76
- ...defaultOptionsRef.current,
77
- ...commonOptionsRef.current,
78
- ...def.options
79
- };
80
- const resolvedTarget = isRef(mergedOptions.target) ? mergedOptions.target.current : mergedOptions.target ?? (typeof document !== "undefined" ? document : null);
81
- if (!resolvedTarget) continue;
82
- const registrationKey = `${i}:${seqStr}`;
83
- rows.push({
84
- registrationKey,
85
- def,
86
- seq,
87
- seqStr,
88
- mergedOptions,
89
- resolvedTarget
90
- });
91
- }
92
- const nextKeys = new Set(rows.map((r) => r.registrationKey));
93
- for (const [key, record] of prevRegistrations) if (!nextKeys.has(key) && record.handle.isActive) record.handle.unregister();
94
- for (const row of rows) {
95
- const { registrationKey, def, seq, mergedOptions, resolvedTarget } = row;
96
- const existing = prevRegistrations.get(registrationKey);
97
- if (existing?.handle.isActive && existing.target === resolvedTarget) {
98
- existing.handle.callback = def.callback;
99
- const { target: _target, ...optionsWithoutTarget } = mergedOptions;
100
- existing.handle.setOptions(optionsWithoutTarget);
101
- nextRegistrations.set(registrationKey, existing);
102
- continue;
103
- }
104
- if (existing?.handle.isActive) existing.handle.unregister();
105
- const handle = managerRef.current.register(seq, def.callback, {
106
- ...mergedOptions,
107
- target: resolvedTarget
108
- });
109
- nextRegistrations.set(registrationKey, {
110
- handle,
111
- target: resolvedTarget
112
- });
113
- }
114
- registrationsRef.current = nextRegistrations;
115
- });
116
- useEffect(() => {
117
- return () => {
118
- for (const { handle } of registrationsRef.current.values()) if (handle.isActive) handle.unregister();
119
- registrationsRef.current = /* @__PURE__ */ new Map();
120
- };
121
- }, []);
122
- }
123
-
124
- //#endregion
125
- export { useHotkeySequences };