@tanstack/preact-hotkeys 0.17.1 → 0.19.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.
@@ -27,8 +27,10 @@ export interface HotkeysProviderProps<T extends AppHotkeys = AppHotkeys> extends
27
27
  */
28
28
  appHotkeys?: T;
29
29
  /**
30
- * Starting custom hotkeys, such as saved data. Read once; the provider owns
31
- * the state afterwards. Invalid entries are ignored with a warning.
30
+ * The user's saved custom hotkeys. When the value changes, such as after
31
+ * saved data loads or the user signs in, it replaces the current custom
32
+ * hotkeys. `undefined` leaves them unchanged; `null` clears them. Invalid
33
+ * entries are ignored with a warning.
32
34
  */
33
35
  customHotkeys?: CustomHotkeys<T> | null;
34
36
  /** Called with the custom hotkeys after every change, ready to save. */
@@ -1,7 +1,7 @@
1
- import { AppHotkeysStore, publishAppHotkeysStore } from "@tanstack/hotkeys";
1
+ import { publishAppHotkeysStore } from "@tanstack/hotkeys";
2
2
  import { createContext } from "preact";
3
3
  import { useContext, useEffect, useMemo, useState } from "preact/hooks";
4
- import { activateHotkeysProviderScope, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults } from "@tanstack/hotkeys/adapter";
4
+ import { activateHotkeysProviderScope, createAppHotkeysStore, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults } from "@tanstack/hotkeys/adapter";
5
5
  import { jsx } from "preact/jsx-runtime";
6
6
 
7
7
  //#region src/HotkeysProvider.tsx
@@ -45,7 +45,7 @@ const NO_PARENT = {
45
45
  */
46
46
  function HotkeysProvider({ children, appHotkeys, customHotkeys, onCustomHotkeysChange, defaultOptions = DEFAULT_OPTIONS, scope, exclusive = false, enabled = true }) {
47
47
  const parent = useContext(HotkeysContext) ?? NO_PARENT;
48
- const [store] = useState(() => appHotkeys ? new AppHotkeysStore(appHotkeys, {
48
+ const [store] = useState(() => appHotkeys ? createAppHotkeysStore(appHotkeys, {
49
49
  customHotkeys,
50
50
  onCustomHotkeysChange
51
51
  }) : null);
@@ -60,8 +60,15 @@ function HotkeysProvider({ children, appHotkeys, customHotkeys, onCustomHotkeysC
60
60
  return scopeKey ? activateHotkeysProviderScope(scopeKey) : void 0;
61
61
  }, [scopeKey]);
62
62
  useEffect(() => {
63
- store?.setOptions({ onCustomHotkeysChange });
64
- }, [store, onCustomHotkeysChange]);
63
+ store?.setOptions({
64
+ customHotkeys,
65
+ onCustomHotkeysChange
66
+ });
67
+ }, [
68
+ store,
69
+ customHotkeys,
70
+ onCustomHotkeysChange
71
+ ]);
65
72
  useEffect(() => {
66
73
  if (appHotkeys) store?.setAppHotkeys(appHotkeys);
67
74
  }, [store, appHotkeys]);
@@ -1,8 +1,9 @@
1
1
  import { useHotkeysContext } from "./HotkeysProvider.js";
2
- import { Store, useSelector } from "@tanstack/preact-store";
2
+ import { useSelector } from "./_useSelector.js";
3
+ import { createStore } from "@tanstack/hotkeys/adapter";
3
4
 
4
5
  //#region src/_appHotkeysStore.ts
5
- const NO_APP_HOTKEYS = new Store(null);
6
+ const NO_APP_HOTKEYS = createStore(null);
6
7
  /**
7
8
  * The provider's app hotkeys store, re-rendering when app hotkeys or custom
8
9
  * hotkeys change. Null outside a provider with `appHotkeys`.
@@ -0,0 +1,54 @@
1
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
2
+
3
+ //#region src/_useSelector.ts
4
+ /**
5
+ * Subscribes to a store and returns the selected value, re-rendering when it
6
+ * changes (by `Object.is`). The selector runs again only when the store's
7
+ * state or the selector itself changes. Preact 10 has no
8
+ * `useSyncExternalStore` outside `preact/compat`, so this follows its approach.
9
+ */
10
+ function useSelector(store, selector) {
11
+ const select = useMemo(() => {
12
+ let memo;
13
+ return (state) => {
14
+ if (!memo || !Object.is(memo.state, state)) memo = {
15
+ state,
16
+ selected: selector(state)
17
+ };
18
+ return memo.selected;
19
+ };
20
+ }, [selector]);
21
+ const selected = useRef(void 0);
22
+ const getSelection = () => {
23
+ const next = select(store.get());
24
+ if (!Object.is(selected.current, next)) selected.current = next;
25
+ return selected.current;
26
+ };
27
+ const value = getSelection();
28
+ const [{ instance }, forceUpdate] = useState({ instance: {
29
+ value,
30
+ getSelection
31
+ } });
32
+ const changed = () => {
33
+ try {
34
+ return !Object.is(instance.value, instance.getSelection());
35
+ } catch {
36
+ return true;
37
+ }
38
+ };
39
+ useLayoutEffect(() => {
40
+ instance.value = value;
41
+ instance.getSelection = getSelection;
42
+ if (changed()) forceUpdate({ instance });
43
+ });
44
+ useEffect(() => {
45
+ if (changed()) forceUpdate({ instance });
46
+ return store.subscribe(() => {
47
+ if (changed()) forceUpdate({ instance });
48
+ }).unsubscribe;
49
+ }, [instance, store]);
50
+ return value;
51
+ }
52
+
53
+ //#endregion
54
+ export { useSelector };
@@ -1,5 +1,5 @@
1
+ import { useSelector } from "./_useSelector.js";
1
2
  import { getKeyStateTracker } from "@tanstack/hotkeys";
2
- import { useSelector } from "@tanstack/preact-store";
3
3
 
4
4
  //#region src/useHeldKeyCodes.ts
5
5
  /**
@@ -2,8 +2,7 @@
2
2
  /**
3
3
  * Preact hook that returns an array of currently held keyboard keys.
4
4
  *
5
- * This hook uses `useSelector` from `@tanstack/preact-store` to subscribe
6
- * to the global KeyStateTracker and updates whenever keys are pressed
5
+ * This hook subscribes to the global KeyStateTracker and updates whenever keys are pressed
7
6
  * or released.
8
7
  *
9
8
  * @returns Array of currently held key names
@@ -1,12 +1,11 @@
1
+ import { useSelector } from "./_useSelector.js";
1
2
  import { getKeyStateTracker } from "@tanstack/hotkeys";
2
- import { useSelector } from "@tanstack/preact-store";
3
3
 
4
4
  //#region src/useHeldKeys.ts
5
5
  /**
6
6
  * Preact hook that returns an array of currently held keyboard keys.
7
7
  *
8
- * This hook uses `useSelector` from `@tanstack/preact-store` to subscribe
9
- * to the global KeyStateTracker and updates whenever keys are pressed
8
+ * This hook subscribes to the global KeyStateTracker and updates whenever keys are pressed
10
9
  * or released.
11
10
  *
12
11
  * @returns Array of currently held key names
@@ -38,7 +38,8 @@ export type UseAppHotkeyOptions<T extends AppHotkey = AppHotkey> = AdapterAppHot
38
38
  * Options merge in this order: provider defaults, app hotkey declaration,
39
39
  * call-site options.
40
40
  *
41
- * @param hotkey - A chord string, RawHotkeyChord object, sequence, or app hotkey
41
+ * @param hotkey - A chord string, RawHotkeyChord object, sequence, or app hotkey.
42
+ * `undefined`, `null`, or `''` registers nothing, for an optional shortcut.
42
43
  * @param callback - The function to call when the hotkey is pressed
43
44
  * @param options - Options for the hotkey behavior
44
45
  *
@@ -61,6 +62,6 @@ export type UseAppHotkeyOptions<T extends AppHotkey = AppHotkey> = AdapterAppHot
61
62
  * }
62
63
  * ```
63
64
  */
64
- export declare function useHotkey<T extends AppHotkey>(hotkey: T, callback: HotkeyCallback, options?: UseAppHotkeyOptions<T>): void;
65
- export declare function useHotkey<const T extends RegisterableHotkey>(hotkey: T, callback: HotkeyCallback<HotkeyCallbackContextFor<T>>, options?: UseHotkeyOptionsFor<T>): void;
65
+ export declare function useHotkey<T extends AppHotkey>(hotkey: T | null | undefined, callback: HotkeyCallback, options?: UseAppHotkeyOptions<T>): void;
66
+ export declare function useHotkey<const T extends RegisterableHotkey>(hotkey: T | null | undefined, callback: HotkeyCallback<HotkeyCallbackContextFor<T>>, options?: UseHotkeyOptionsFor<T>): void;
66
67
  //#endregion
@@ -4,10 +4,11 @@ import { AppHotkey, HeldModifierOptions, RegisterableHotkey } from "@tanstack/ho
4
4
  * Returns a boolean indicating whether held modifiers reveal this shortcut.
5
5
  * The hint shows only while the hotkey can fire: some registration of it is
6
6
  * enabled and in an active scope. An app hotkey matches its registrations by
7
- * id and uses its effective hotkey (false while unbound).
7
+ * id and uses its effective hotkey (false while unbound). No hotkey
8
+ * (`undefined`, `null`, or `''`) returns false.
8
9
  * Uses the nonempty-subset/AltGr rules of `matchesHeldModifiers`; `exact`
9
10
  * requires every modifier. A sequence reveals its hint from its first step.
10
11
  * Only visibility changes cause a rerender. This helper does not register a shortcut. A hotkey on an element counts only while focus is inside it.
11
12
  */
12
- export declare function useHotkeyHint(hotkey: RegisterableHotkey | AppHotkey, options?: HeldModifierOptions): boolean;
13
+ export declare function useHotkeyHint(hotkey: RegisterableHotkey | AppHotkey | null | undefined, options?: HeldModifierOptions): boolean;
13
14
  //#endregion
@@ -1,13 +1,14 @@
1
+ import { useSelector } from "./_useSelector.js";
1
2
  import { useAppHotkeysStoreState } from "./_appHotkeysStore.js";
2
3
  import { getHotkeyManager, getKeyStateTracker, isAppHotkey, matchesHeldModifiers, resolveAppHotkey } from "@tanstack/hotkeys";
3
- import { useSelector } from "@tanstack/preact-store";
4
4
 
5
5
  //#region src/useHotkeyHint.ts
6
6
  /**
7
7
  * Returns a boolean indicating whether held modifiers reveal this shortcut.
8
8
  * The hint shows only while the hotkey can fire: some registration of it is
9
9
  * enabled and in an active scope. An app hotkey matches its registrations by
10
- * id and uses its effective hotkey (false while unbound).
10
+ * id and uses its effective hotkey (false while unbound). No hotkey
11
+ * (`undefined`, `null`, or `''`) returns false.
11
12
  * Uses the nonempty-subset/AltGr rules of `matchesHeldModifiers`; `exact`
12
13
  * requires every modifier. A sequence reveals its hint from its first step.
13
14
  * Only visibility changes cause a rerender. This helper does not register a shortcut. A hotkey on an element counts only while focus is inside it.
@@ -1,14 +1,12 @@
1
1
  import { useDefaultHotkeysOptions } from "./HotkeysProvider.js";
2
+ import { useSelector } from "./_useSelector.js";
2
3
  import { HotkeyRecorder } from "@tanstack/hotkeys";
3
4
  import { useEffect, useRef } from "preact/hooks";
4
- import { useSelector } from "@tanstack/preact-store";
5
+ import { withDefaults } from "@tanstack/hotkeys/adapter";
5
6
 
6
7
  //#region src/useHotkeyRecorder.ts
7
8
  function useHotkeyRecorder(options) {
8
- const mergedOptions = {
9
- ...useDefaultHotkeysOptions().hotkeyRecorder,
10
- ...options
11
- };
9
+ const mergedOptions = withDefaults(useDefaultHotkeysOptions().hotkeyRecorder, options);
12
10
  const recorderRef = useRef(null);
13
11
  if (!recorderRef.current) recorderRef.current = new HotkeyRecorder(mergedOptions);
14
12
  recorderRef.current.setOptions(mergedOptions);
@@ -1,5 +1,5 @@
1
+ import { useSelector } from "./_useSelector.js";
1
2
  import { getHotkeyManager, toHotkeyRegistrationView } from "@tanstack/hotkeys";
2
- import { useSelector } from "@tanstack/preact-store";
3
3
 
4
4
  //#region src/useHotkeyRegistrations.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
+ import { useSelector } from "./_useSelector.js";
1
2
  import { getHotkeyManager } from "@tanstack/hotkeys";
2
- import { useSelector } from "@tanstack/preact-store";
3
3
 
4
4
  //#region src/useHotkeyScopesState.ts
5
5
  /**
@@ -3,8 +3,7 @@ import { IndividualKey } from "@tanstack/hotkeys";
3
3
  /**
4
4
  * Preact hook that returns whether a specific key is currently being held.
5
5
  *
6
- * This hook uses `useSelector` from `@tanstack/preact-store` to subscribe
7
- * to the global KeyStateTracker and uses a selector to determine if
6
+ * This hook subscribes to the global KeyStateTracker and uses a selector to determine if
8
7
  * the specified key is held.
9
8
  *
10
9
  * @param key - The key to check (e.g., 'Shift', 'Control', 'A')
@@ -1,12 +1,11 @@
1
+ import { useSelector } from "./_useSelector.js";
1
2
  import { getKeyStateTracker } from "@tanstack/hotkeys";
2
- import { useSelector } from "@tanstack/preact-store";
3
3
 
4
4
  //#region src/useKeyHold.ts
5
5
  /**
6
6
  * Preact hook that returns whether a specific key is currently being held.
7
7
  *
8
- * This hook uses `useSelector` from `@tanstack/preact-store` to subscribe
9
- * to the global KeyStateTracker and uses a selector to determine if
8
+ * This hook subscribes to the global KeyStateTracker and uses a selector to determine if
10
9
  * the specified key is held.
11
10
  *
12
11
  * @param key - The key to check (e.g., 'Shift', 'Control', 'A')
package/dist/utils.js CHANGED
@@ -6,11 +6,11 @@ function isRef(value) {
6
6
  return value !== null && typeof value === "object" && "current" in value;
7
7
  }
8
8
  /**
9
- * Resolves a hook target. A ref that is still null skips registration;
10
- * a null target means document.
9
+ * Resolves a hook target. A null target, or a ref that is still null, skips
10
+ * registration. Omit the target to listen on the document.
11
11
  */
12
12
  function resolveTarget(target) {
13
- return isRef(target) ? target.current : target ?? void 0;
13
+ return isRef(target) ? target.current : target;
14
14
  }
15
15
 
16
16
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/preact-hotkeys",
3
- "version": "0.17.1",
3
+ "version": "0.19.0",
4
4
  "description": "Preact adapter for TanStack Hotkeys",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -35,8 +35,7 @@
35
35
  "skills"
36
36
  ],
37
37
  "dependencies": {
38
- "@tanstack/hotkeys": "0.19.0",
39
- "@tanstack/preact-store": "^0.13.5"
38
+ "@tanstack/hotkeys": "0.21.0"
40
39
  },
41
40
  "devDependencies": {
42
41
  "@preact/preset-vite": "^2.10.6",
@@ -101,7 +101,7 @@ A hook uses the nearest `HotkeysProvider` above its component. Registering `Esca
101
101
 
102
102
  ## Pitfalls
103
103
 
104
- - **Pass the ref, not `ref.current`.** `target: listRef.current` is `null` on the first render, and a `null` target means `document`, so the hotkey works page-wide. Pass `target: listRef`; a ref that is still empty waits for its element.
104
+ - **Pass the ref, not `ref.current`.** `target: listRef.current` is `null` on the first render, and a `null` target registers nothing, so the hotkey doesn't work until the component renders again. Pass `target: listRef`; a ref that is still empty waits for its element.
105
105
  - **No dependency arrays or `useCallback`.** The hooks read the hotkey, callback, and options on every render, so inline callbacks always see current state.
106
106
  - **Call the hooks directly.** A `useShortcut(keys: string)` wrapper has to cast strings to hotkeys, which turns off type checking and drops sequences, options, app hotkeys, and the typed callback context.
107
107
  - **In tests,** set `<HotkeysProvider defaultOptions={{ hotkey: { platform: 'mac' } }}>`.