@tanstack/react-hotkeys 0.18.0 → 0.20.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,8 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { AppHotkeysStore, publishAppHotkeysStore } from "@tanstack/hotkeys";
3
+ import { publishAppHotkeysStore } from "@tanstack/hotkeys";
4
4
  import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
5
- import { activateHotkeysProviderScope, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults } from "@tanstack/hotkeys/adapter";
5
+ import { activateHotkeysProviderScope, createAppHotkeysStore, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults } from "@tanstack/hotkeys/adapter";
6
6
 
7
7
  //#region src/HotkeysProvider.tsx
8
8
  const HotkeysContext = createContext(null);
@@ -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,10 +1,11 @@
1
1
  'use client';
2
2
 
3
3
  import { useHotkeysContext } from "./HotkeysProvider.js";
4
- import { Store, useSelector } from "@tanstack/react-store";
4
+ import { useSelector } from "./_useSelector.js";
5
+ import { createStore } from "@tanstack/hotkeys/adapter";
5
6
 
6
7
  //#region src/_appHotkeysStore.ts
7
- const NO_APP_HOTKEYS = new Store(null);
8
+ const NO_APP_HOTKEYS = createStore(null);
8
9
  /**
9
10
  * The provider's app hotkeys store, re-rendering when app hotkeys or custom
10
11
  * hotkeys change. Null outside a provider with `appHotkeys`.
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useRef, useSyncExternalStore } from "react";
4
+
5
+ //#region src/_useSelector.ts
6
+ /**
7
+ * Subscribes to a store and returns the selected value, re-rendering when it
8
+ * changes (by `Object.is`). The selector runs again only when the store's
9
+ * state or the selector itself changes.
10
+ */
11
+ function useSelector(store, selector) {
12
+ const subscribe = useCallback((onChange) => store.subscribe(onChange).unsubscribe, [store]);
13
+ const memo = useRef(null);
14
+ const getSelection = () => {
15
+ const state = store.get();
16
+ const last = memo.current;
17
+ if (last && last.selector === selector && Object.is(last.state, state)) return last.selected;
18
+ memo.current = {
19
+ state,
20
+ selector,
21
+ selected: selector(state)
22
+ };
23
+ return memo.current.selected;
24
+ };
25
+ return useSyncExternalStore(subscribe, getSelection, getSelection);
26
+ }
27
+
28
+ //#endregion
29
+ export { useSelector };
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
+ import { useSelector } from "./_useSelector.js";
3
4
  import { getKeyStateTracker } from "@tanstack/hotkeys";
4
- import { useSelector } from "@tanstack/react-store";
5
5
 
6
6
  //#region src/useHeldKeyCodes.ts
7
7
  /**
@@ -2,8 +2,7 @@
2
2
  /**
3
3
  * React hook that returns an array of currently held keyboard keys.
4
4
  *
5
- * This hook uses `useSelector` from `@tanstack/react-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,14 +1,13 @@
1
1
  'use client';
2
2
 
3
+ import { useSelector } from "./_useSelector.js";
3
4
  import { getKeyStateTracker } from "@tanstack/hotkeys";
4
- import { useSelector } from "@tanstack/react-store";
5
5
 
6
6
  //#region src/useHeldKeys.ts
7
7
  /**
8
8
  * React hook that returns an array of currently held keyboard keys.
9
9
  *
10
- * This hook uses `useSelector` from `@tanstack/react-store` to subscribe
11
- * to the global KeyStateTracker and updates whenever keys are pressed
10
+ * This hook subscribes to the global KeyStateTracker and updates whenever keys are pressed
12
11
  * or released.
13
12
  *
14
13
  * @returns Array of currently held key names
@@ -37,7 +37,8 @@ export type UseAppHotkeyOptions<T extends AppHotkey = AppHotkey> = AdapterAppHot
37
37
  * Options merge in this order: provider defaults, app hotkey declaration,
38
38
  * call-site options.
39
39
  *
40
- * @param hotkey - A chord string, RawHotkeyChord object, sequence, or app hotkey
40
+ * @param hotkey - A chord string, RawHotkeyChord object, sequence, or app hotkey.
41
+ * `undefined`, `null`, or `''` registers nothing, for an optional shortcut.
41
42
  * @param callback - The function to call when the hotkey is pressed
42
43
  * @param options - Options for the hotkey behavior
43
44
  *
@@ -60,6 +61,6 @@ export type UseAppHotkeyOptions<T extends AppHotkey = AppHotkey> = AdapterAppHot
60
61
  * }
61
62
  * ```
62
63
  */
63
- export declare function useHotkey<T extends AppHotkey>(hotkey: T, callback: HotkeyCallback, options?: UseAppHotkeyOptions<T>): void;
64
- export declare function useHotkey<const T extends RegisterableHotkey>(hotkey: T, callback: HotkeyCallback<HotkeyCallbackContextFor<T>>, options?: UseHotkeyOptionsFor<T>): void;
64
+ export declare function useHotkey<T extends AppHotkey>(hotkey: T | null | undefined, callback: HotkeyCallback, options?: UseAppHotkeyOptions<T>): void;
65
+ export declare function useHotkey<const T extends RegisterableHotkey>(hotkey: T | null | undefined, callback: HotkeyCallback<HotkeyCallbackContextFor<T>>, options?: UseHotkeyOptionsFor<T>): void;
65
66
  //#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,15 +1,16 @@
1
1
  'use client';
2
2
 
3
+ import { useSelector } from "./_useSelector.js";
3
4
  import { useAppHotkeysStoreState } from "./_appHotkeysStore.js";
4
5
  import { getHotkeyManager, getKeyStateTracker, isAppHotkey, matchesHeldModifiers, resolveAppHotkey } from "@tanstack/hotkeys";
5
- import { useSelector } from "@tanstack/react-store";
6
6
 
7
7
  //#region src/useHotkeyHint.ts
8
8
  /**
9
9
  * Returns a boolean indicating whether held modifiers reveal this shortcut.
10
10
  * The hint shows only while the hotkey can fire: some registration of it is
11
11
  * enabled and in an active scope. An app hotkey matches its registrations by
12
- * id and uses its effective hotkey (false while unbound).
12
+ * id and uses its effective hotkey (false while unbound). No hotkey
13
+ * (`undefined`, `null`, or `''`) returns false.
13
14
  * Uses the nonempty-subset/AltGr rules of `matchesHeldModifiers`; `exact`
14
15
  * requires every modifier. A sequence reveals its hint from its first step.
15
16
  * 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,16 +1,14 @@
1
1
  'use client';
2
2
 
3
3
  import { useDefaultHotkeysOptions } from "./HotkeysProvider.js";
4
+ import { useSelector } from "./_useSelector.js";
4
5
  import { HotkeyRecorder } from "@tanstack/hotkeys";
5
6
  import { useEffect, useRef } from "react";
6
- import { useSelector } from "@tanstack/react-store";
7
+ import { withDefaults } from "@tanstack/hotkeys/adapter";
7
8
 
8
9
  //#region src/useHotkeyRecorder.ts
9
10
  function useHotkeyRecorder(options) {
10
- const mergedOptions = {
11
- ...useDefaultHotkeysOptions().hotkeyRecorder,
12
- ...options
13
- };
11
+ const mergedOptions = withDefaults(useDefaultHotkeysOptions().hotkeyRecorder, options);
14
12
  const recorderRef = useRef(null);
15
13
  if (!recorderRef.current) recorderRef.current = new HotkeyRecorder(mergedOptions);
16
14
  recorderRef.current.setOptions(mergedOptions);
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
+ import { useSelector } from "./_useSelector.js";
3
4
  import { getHotkeyManager, toHotkeyRegistrationView } from "@tanstack/hotkeys";
4
- import { useSelector } from "@tanstack/react-store";
5
5
 
6
6
  //#region src/useHotkeyRegistrations.ts
7
7
  /**
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
+ import { useSelector } from "./_useSelector.js";
3
4
  import { getHotkeyManager } from "@tanstack/hotkeys";
4
- import { useSelector } from "@tanstack/react-store";
5
5
 
6
6
  //#region src/useHotkeyScopesState.ts
7
7
  /**
@@ -3,8 +3,7 @@ import { IndividualKey } from "@tanstack/hotkeys";
3
3
  /**
4
4
  * React hook that returns whether a specific key is currently being held.
5
5
  *
6
- * This hook uses `useSelector` from `@tanstack/react-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,14 +1,13 @@
1
1
  'use client';
2
2
 
3
+ import { useSelector } from "./_useSelector.js";
3
4
  import { getKeyStateTracker } from "@tanstack/hotkeys";
4
- import { useSelector } from "@tanstack/react-store";
5
5
 
6
6
  //#region src/useKeyHold.ts
7
7
  /**
8
8
  * React hook that returns whether a specific key is currently being held.
9
9
  *
10
- * This hook uses `useSelector` from `@tanstack/react-store` to subscribe
11
- * to the global KeyStateTracker and uses a selector to determine if
10
+ * This hook subscribes to the global KeyStateTracker and uses a selector to determine if
12
11
  * the specified key is held.
13
12
  *
14
13
  * @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/react-hotkeys",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "React adapter for TanStack Hotkeys",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -35,11 +35,11 @@
35
35
  "skills"
36
36
  ],
37
37
  "dependencies": {
38
- "@tanstack/hotkeys": "0.19.0",
39
- "@tanstack/react-store": "^0.11.2"
38
+ "@tanstack/hotkeys": "0.21.0"
40
39
  },
41
40
  "devDependencies": {
42
41
  "@eslint-react/eslint-plugin": "^4.2.3",
42
+ "@tanstack/react-store": "^0.11.2",
43
43
  "@testing-library/react": "^16.3.3",
44
44
  "@types/react": "^19.3.0",
45
45
  "@vitejs/plugin-react": "^6.1.1",
@@ -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
  - **`matchesKeyboardEvent` takes the DOM event.** In a widget's `onKeyDown`, pass `event.nativeEvent`.