@tanstack/react-hotkeys 0.13.0 → 0.14.1

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,22 +1,53 @@
1
- import { UseHotkeyOptions } from "./useHotkey.js";
2
- import { UseHotkeySequenceOptions } from "./useHotkeySequence.js";
3
- import { HotkeyRecorderOptions, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
1
+ import { UseHotkeyCommonOptions } from "./useHotkey.js";
2
+ import { AppHotkeys, AppHotkeysStore, CustomHotkeys, HotkeyChordRecorderOptions, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
4
3
  import React, { ReactNode } from "react";
5
4
  //#region src/HotkeysProvider.d.ts
6
5
  export interface HotkeysProviderOptions {
7
- hotkey?: Partial<UseHotkeyOptions>;
8
- hotkeyRecorder?: Partial<HotkeyRecorderOptions>;
9
- hotkeySequence?: Partial<UseHotkeySequenceOptions>;
10
- hotkeySequenceRecorder?: Partial<HotkeySequenceRecorderOptions>;
6
+ /**
7
+ * Defaults for every hotkey registered by `useHotkey` and `useHotkeys`.
8
+ * `requireReset` applies to chords and `timeout` applies to sequences.
9
+ */
10
+ hotkey?: UseHotkeyCommonOptions;
11
+ /** Defaults for every `useHotkeyRecorder`. */
12
+ hotkeyRecorder?: Partial<Omit<HotkeyChordRecorderOptions & HotkeySequenceRecorderOptions, 'maxSteps' | 'onRecord' | 'validate'>>;
11
13
  }
12
14
  interface HotkeysContextValue {
15
+ appHotkeysStore: AppHotkeysStore | null;
13
16
  defaultOptions: HotkeysProviderOptions;
14
17
  }
15
- export interface HotkeysProviderProps {
18
+ export interface HotkeysProviderProps<T extends AppHotkeys = AppHotkeys> {
16
19
  children: ReactNode;
20
+ /**
21
+ * Every app hotkey, from `defineAppHotkeys`. Listed by `useAppHotkeyList`
22
+ * whether or not their components have mounted.
23
+ */
24
+ appHotkeys?: T;
25
+ /**
26
+ * Starting custom hotkeys, such as saved data. Read once; the provider owns
27
+ * the state afterwards. Invalid entries are ignored with a warning.
28
+ */
29
+ customHotkeys?: CustomHotkeys<T> | null;
30
+ /** Called with the custom hotkeys after every change, ready to save. */
31
+ onCustomHotkeysChange?: (customHotkeys: CustomHotkeys<T>) => void;
17
32
  defaultOptions?: HotkeysProviderOptions;
18
33
  }
19
- export declare function HotkeysProvider({ children, defaultOptions }: HotkeysProviderProps): React.JSX.Element;
34
+ /**
35
+ * Provides default options, app hotkeys, and the user's custom hotkeys.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * <HotkeysProvider
40
+ * appHotkeys={appHotkeys}
41
+ * customHotkeys={JSON.parse(localStorage.getItem('hotkeys') ?? '{}')}
42
+ * onCustomHotkeysChange={(custom) =>
43
+ * localStorage.setItem('hotkeys', JSON.stringify(custom))
44
+ * }
45
+ * >
46
+ * <App />
47
+ * </HotkeysProvider>
48
+ * ```
49
+ */
50
+ export declare function HotkeysProvider<T extends AppHotkeys = AppHotkeys>({ children, appHotkeys, customHotkeys, onCustomHotkeysChange, defaultOptions }: HotkeysProviderProps<T>): React.JSX.Element;
20
51
  export declare function useHotkeysContext(): HotkeysContextValue | null;
21
52
  export declare function useDefaultHotkeysOptions(): HotkeysProviderOptions;
22
53
  //#endregion
@@ -1,17 +1,51 @@
1
- import React, { createContext, useContext, useMemo } from "react";
1
+ import { AppHotkeysStore, publishAppHotkeysStore } from "@tanstack/hotkeys";
2
+ import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
2
3
 
3
4
  //#region src/HotkeysProvider.tsx
4
5
  const HotkeysContext = createContext(null);
5
6
  const DEFAULT_OPTIONS = {};
6
- function HotkeysProvider({ children, defaultOptions = DEFAULT_OPTIONS }) {
7
- const contextValue = useMemo(() => ({ defaultOptions }), [defaultOptions]);
7
+ /**
8
+ * Provides default options, app hotkeys, and the user's custom hotkeys.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * <HotkeysProvider
13
+ * appHotkeys={appHotkeys}
14
+ * customHotkeys={JSON.parse(localStorage.getItem('hotkeys') ?? '{}')}
15
+ * onCustomHotkeysChange={(custom) =>
16
+ * localStorage.setItem('hotkeys', JSON.stringify(custom))
17
+ * }
18
+ * >
19
+ * <App />
20
+ * </HotkeysProvider>
21
+ * ```
22
+ */
23
+ function HotkeysProvider({ children, appHotkeys, customHotkeys, onCustomHotkeysChange, defaultOptions = DEFAULT_OPTIONS }) {
24
+ const [store] = useState(() => appHotkeys ? new AppHotkeysStore(appHotkeys, {
25
+ customHotkeys,
26
+ onCustomHotkeysChange
27
+ }) : null);
28
+ useEffect(() => {
29
+ store?.setOptions({ onCustomHotkeysChange });
30
+ }, [store, onCustomHotkeysChange]);
31
+ useEffect(() => {
32
+ if (appHotkeys) store?.setAppHotkeys(appHotkeys);
33
+ }, [store, appHotkeys]);
34
+ useEffect(() => {
35
+ if (!store) return;
36
+ return publishAppHotkeysStore(store);
37
+ }, [store]);
38
+ const contextValue = useMemo(() => ({
39
+ appHotkeysStore: store,
40
+ defaultOptions
41
+ }), [store, defaultOptions]);
8
42
  return /* @__PURE__ */ React.createElement(HotkeysContext.Provider, { value: contextValue }, children);
9
43
  }
10
44
  function useHotkeysContext() {
11
45
  return useContext(HotkeysContext);
12
46
  }
13
47
  function useDefaultHotkeysOptions() {
14
- return useContext(HotkeysContext)?.defaultOptions ?? {};
48
+ return useContext(HotkeysContext)?.defaultOptions ?? DEFAULT_OPTIONS;
15
49
  }
16
50
 
17
51
  //#endregion
@@ -0,0 +1,17 @@
1
+ import { useHotkeysContext } from "./HotkeysProvider.js";
2
+ import { Store, useSelector } from "@tanstack/react-store";
3
+
4
+ //#region src/_appHotkeysStore.ts
5
+ const NO_APP_HOTKEYS = new Store(null);
6
+ /**
7
+ * The provider's app hotkeys store, re-rendering when app hotkeys or custom
8
+ * hotkeys change. Null outside a provider with `appHotkeys`.
9
+ */
10
+ function useAppHotkeysStoreState() {
11
+ const store = useHotkeysContext()?.appHotkeysStore ?? null;
12
+ useSelector(store?.store ?? NO_APP_HOTKEYS, (state) => state);
13
+ return store;
14
+ }
15
+
16
+ //#endregion
17
+ export { useAppHotkeysStoreState };
package/dist/index.d.ts CHANGED
@@ -1,14 +1,12 @@
1
- import { UseHotkeyOptions, useHotkey } from "./useHotkey.js";
2
- import { UseHotkeySequenceOptions, useHotkeySequence } from "./useHotkeySequence.js";
1
+ import { UseAppHotkeyOptions, UseHotkeyChordOptions, UseHotkeyCommonOptions, UseHotkeyOptions, UseHotkeyOptionsFor, UseHotkeySequenceOptions, UseHotkeyTarget, useHotkey } from "./useHotkey.js";
3
2
  import { HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, useDefaultHotkeysOptions, useHotkeysContext } from "./HotkeysProvider.js";
4
3
  import { UseHotkeyDefinition, useHotkeys } from "./useHotkeys.js";
4
+ import { ReactHotkeyRecorder, useHotkeyRecorder } from "./useHotkeyRecorder.js";
5
+ import { useHotkeyRegistrations } from "./useHotkeyRegistrations.js";
6
+ import { useHotkeyHint } from "./useHotkeyHint.js";
7
+ import { UseCustomHotkeysResult, useAppHotkeyList, useAppHotkeys, useCustomHotkeys } from "./useAppHotkeys.js";
5
8
  import { useHeldKeys } from "./useHeldKeys.js";
6
9
  import { useHeldKeyCodes } from "./useHeldKeyCodes.js";
7
10
  import { useKeyHold } from "./useKeyHold.js";
8
- import { UseHotkeySequenceDefinition, useHotkeySequences } from "./useHotkeySequences.js";
9
- import { ReactHotkeyRecorder, useHotkeyRecorder } from "./useHotkeyRecorder.js";
10
- import { HotkeyRegistrationsResult, useHotkeyRegistrations } from "./useHotkeyRegistrations.js";
11
- import { ReactHotkeySequenceRecorder, useHotkeySequenceRecorder } from "./useHotkeySequenceRecorder.js";
12
- import { useHotkeyHint } from "./useHotkeyHint.js";
13
11
  export * from "@tanstack/hotkeys";
14
- export { HotkeyRegistrationsResult, HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, ReactHotkeyRecorder, ReactHotkeySequenceRecorder, UseHotkeyDefinition, UseHotkeyOptions, UseHotkeySequenceDefinition, UseHotkeySequenceOptions, useDefaultHotkeysOptions, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyHint, useHotkeyRecorder, useHotkeyRegistrations, useHotkeySequence, useHotkeySequenceRecorder, useHotkeySequences, useHotkeys, useHotkeysContext, useKeyHold };
12
+ export { HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, ReactHotkeyRecorder, UseAppHotkeyOptions, UseCustomHotkeysResult, UseHotkeyChordOptions, UseHotkeyCommonOptions, UseHotkeyDefinition, UseHotkeyOptions, UseHotkeyOptionsFor, UseHotkeySequenceOptions, UseHotkeyTarget, useAppHotkeyList, useAppHotkeys, useCustomHotkeys, useDefaultHotkeysOptions, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyHint, useHotkeyRecorder, useHotkeyRegistrations, useHotkeys, useHotkeysContext, useKeyHold };
package/dist/index.js CHANGED
@@ -1,16 +1,14 @@
1
1
  import { HotkeysProvider, useDefaultHotkeysOptions, useHotkeysContext } from "./HotkeysProvider.js";
2
- import { useHotkey } from "./useHotkey.js";
3
2
  import { useHotkeys } from "./useHotkeys.js";
4
- import { useHeldKeys } from "./useHeldKeys.js";
5
- import { useHeldKeyCodes } from "./useHeldKeyCodes.js";
6
- import { useKeyHold } from "./useKeyHold.js";
7
- import { useHotkeySequence } from "./useHotkeySequence.js";
8
- import { useHotkeySequences } from "./useHotkeySequences.js";
3
+ import { useHotkey } from "./useHotkey.js";
9
4
  import { useHotkeyRecorder } from "./useHotkeyRecorder.js";
10
5
  import { useHotkeyRegistrations } from "./useHotkeyRegistrations.js";
11
- import { useHotkeySequenceRecorder } from "./useHotkeySequenceRecorder.js";
12
6
  import { useHotkeyHint } from "./useHotkeyHint.js";
7
+ import { useAppHotkeyList, useAppHotkeys, useCustomHotkeys } from "./useAppHotkeys.js";
8
+ import { useHeldKeys } from "./useHeldKeys.js";
9
+ import { useHeldKeyCodes } from "./useHeldKeyCodes.js";
10
+ import { useKeyHold } from "./useKeyHold.js";
13
11
 
14
12
  export * from "@tanstack/hotkeys"
15
13
 
16
- export { HotkeysProvider, useDefaultHotkeysOptions, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyHint, useHotkeyRecorder, useHotkeyRegistrations, useHotkeySequence, useHotkeySequenceRecorder, useHotkeySequences, useHotkeys, useHotkeysContext, useKeyHold };
14
+ export { HotkeysProvider, useAppHotkeyList, useAppHotkeys, useCustomHotkeys, useDefaultHotkeysOptions, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyHint, useHotkeyRecorder, useHotkeyRegistrations, useHotkeys, useHotkeysContext, useKeyHold };
@@ -0,0 +1,71 @@
1
+ import { AppHotkey, AppHotkeyListItem, CustomHotkeys, Hotkey, RegisteredAppHotkeys } from "@tanstack/hotkeys";
2
+ //#region src/useAppHotkeys.d.ts
3
+ /**
4
+ * Returns the effective app hotkeys from `HotkeysProvider`, typed by
5
+ * `Register`: each declaration with the user's custom hotkey applied.
6
+ * Pass an entry to `useHotkey` or `useHotkeys` to register it.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * const hotkeys = useAppHotkeys()
11
+ * useHotkey(hotkeys['doc.save'], () => save())
12
+ * ```
13
+ */
14
+ export declare function useAppHotkeys(): RegisteredAppHotkeys;
15
+ /**
16
+ * Lists every app hotkey with its live status, in declaration order, for
17
+ * cheat sheets and settings screens. App hotkeys whose components have not
18
+ * mounted are `'inactive'`; those without a hotkey are `'unbound'`.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * function CheatSheet() {
23
+ * const items = useAppHotkeyList()
24
+ * return (
25
+ * <ul>
26
+ * {items.map(({ appHotkey, status }) => (
27
+ * <li key={appHotkey.id} data-status={status}>
28
+ * {appHotkey.meta?.name}:{' '}
29
+ * {appHotkey.hotkey ? formatForDisplay(appHotkey.hotkey) : 'Unassigned'}
30
+ * </li>
31
+ * ))}
32
+ * </ul>
33
+ * )
34
+ * }
35
+ * ```
36
+ */
37
+ export declare function useAppHotkeyList(): Array<AppHotkeyListItem<RegisteredAppHotkeys[keyof RegisteredAppHotkeys]>>;
38
+ /** Custom hotkey state and actions returned by `useCustomHotkeys`. */
39
+ export interface UseCustomHotkeysResult {
40
+ /** The user's hotkeys that differ from the defaults. */
41
+ customHotkeys: CustomHotkeys;
42
+ /**
43
+ * Sets the user's hotkey for an app hotkey. `null` unbinds it.
44
+ * Returns conflicting app hotkeys; when any exist, nothing changes.
45
+ */
46
+ setCustomHotkey: (id: keyof RegisteredAppHotkeys & string, hotkey: Hotkey | null) => Array<AppHotkey>;
47
+ /** Restores the default hotkey for an app hotkey. */
48
+ resetCustomHotkey: (id: keyof RegisteredAppHotkeys & string) => void;
49
+ /** Restores every default hotkey. */
50
+ resetCustomHotkeys: () => void;
51
+ /** Other app hotkeys that would collide if `id` used `hotkey`. */
52
+ findConflicts: (id: keyof RegisteredAppHotkeys & string, hotkey: Hotkey) => Array<AppHotkey>;
53
+ }
54
+ /**
55
+ * Reads and changes the user's custom hotkeys. Changes apply to every
56
+ * registered app hotkey immediately and are passed to the provider's
57
+ * `onCustomHotkeysChange`, ready to save.
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * const { setCustomHotkey, findConflicts } = useCustomHotkeys()
62
+ * const recorder = useHotkeyRecorder({
63
+ * maxSteps: 3,
64
+ * validate: (hotkey) =>
65
+ * findConflicts('doc.save', hotkey).length === 0 || 'Already in use',
66
+ * onRecord: (hotkey) => setCustomHotkey('doc.save', hotkey),
67
+ * })
68
+ * ```
69
+ */
70
+ export declare function useCustomHotkeys(): UseCustomHotkeysResult;
71
+ //#endregion
@@ -0,0 +1,79 @@
1
+ import { useAppHotkeysStoreState } from "./_appHotkeysStore.js";
2
+ import { useHotkeyRegistrations } from "./useHotkeyRegistrations.js";
3
+
4
+ //#region src/useAppHotkeys.ts
5
+ function useRequiredStore(hook) {
6
+ const store = useAppHotkeysStoreState();
7
+ if (!store) throw new Error(`${hook} requires a HotkeysProvider with an appHotkeys prop`);
8
+ return store;
9
+ }
10
+ /**
11
+ * Returns the effective app hotkeys from `HotkeysProvider`, typed by
12
+ * `Register`: each declaration with the user's custom hotkey applied.
13
+ * Pass an entry to `useHotkey` or `useHotkeys` to register it.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const hotkeys = useAppHotkeys()
18
+ * useHotkey(hotkeys['doc.save'], () => save())
19
+ * ```
20
+ */
21
+ function useAppHotkeys() {
22
+ return useRequiredStore("useAppHotkeys").appHotkeys;
23
+ }
24
+ /**
25
+ * Lists every app hotkey with its live status, in declaration order, for
26
+ * cheat sheets and settings screens. App hotkeys whose components have not
27
+ * mounted are `'inactive'`; those without a hotkey are `'unbound'`.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * function CheatSheet() {
32
+ * const items = useAppHotkeyList()
33
+ * return (
34
+ * <ul>
35
+ * {items.map(({ appHotkey, status }) => (
36
+ * <li key={appHotkey.id} data-status={status}>
37
+ * {appHotkey.meta?.name}:{' '}
38
+ * {appHotkey.hotkey ? formatForDisplay(appHotkey.hotkey) : 'Unassigned'}
39
+ * </li>
40
+ * ))}
41
+ * </ul>
42
+ * )
43
+ * }
44
+ * ```
45
+ */
46
+ function useAppHotkeyList() {
47
+ const store = useRequiredStore("useAppHotkeyList");
48
+ const registrations = useHotkeyRegistrations();
49
+ return store.getList(registrations);
50
+ }
51
+ /**
52
+ * Reads and changes the user's custom hotkeys. Changes apply to every
53
+ * registered app hotkey immediately and are passed to the provider's
54
+ * `onCustomHotkeysChange`, ready to save.
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * const { setCustomHotkey, findConflicts } = useCustomHotkeys()
59
+ * const recorder = useHotkeyRecorder({
60
+ * maxSteps: 3,
61
+ * validate: (hotkey) =>
62
+ * findConflicts('doc.save', hotkey).length === 0 || 'Already in use',
63
+ * onRecord: (hotkey) => setCustomHotkey('doc.save', hotkey),
64
+ * })
65
+ * ```
66
+ */
67
+ function useCustomHotkeys() {
68
+ const store = useRequiredStore("useCustomHotkeys");
69
+ return {
70
+ customHotkeys: store.customHotkeys,
71
+ setCustomHotkey: (id, hotkey) => store.setCustomHotkey(id, hotkey),
72
+ resetCustomHotkey: (id) => store.resetCustomHotkey(id),
73
+ resetCustomHotkeys: () => store.resetCustomHotkeys(),
74
+ findConflicts: (id, hotkey) => store.findConflicts(id, hotkey)
75
+ };
76
+ }
77
+
78
+ //#endregion
79
+ export { useAppHotkeyList, useAppHotkeys, useCustomHotkeys };
@@ -1,71 +1,65 @@
1
- import { HotkeyCallback, HotkeyOptions, RegisterableHotkey } from "@tanstack/hotkeys";
1
+ import { AppHotkey, HotkeyCallback, RegisterableHotkey } from "@tanstack/hotkeys";
2
+ import { AdapterAppHotkeyOptions, AdapterChordOptions, AdapterCommonOptions, AdapterHotkeyOptions, AdapterOptionsFor, AdapterSequenceOptions } from "@tanstack/hotkeys/adapter";
2
3
  //#region src/useHotkey.d.ts
3
- export interface UseHotkeyOptions extends Omit<HotkeyOptions, 'target'> {
4
- /**
5
- * The DOM element to attach the event listener to.
6
- * Can be a React ref, direct DOM element, or null.
7
- * Defaults to document.
8
- */
9
- target?: React.RefObject<HTMLElement | null> | HTMLElement | Document | Window | null;
10
- }
4
+ /** A React ref, element, document, or window to listen on. */
5
+ export type UseHotkeyTarget = React.RefObject<HTMLElement | null> | HTMLElement | Document | Window | null;
6
+ /** Options shared by chords and sequences. */
7
+ export type UseHotkeyOptions = AdapterHotkeyOptions<UseHotkeyTarget>;
8
+ /** Options for a chord such as `'Mod+S'`. */
9
+ export type UseHotkeyChordOptions = AdapterChordOptions<UseHotkeyTarget>;
10
+ /** Options for a sequence such as `['G', 'G']`. */
11
+ export type UseHotkeySequenceOptions = AdapterSequenceOptions<UseHotkeyTarget>;
12
+ /** Options applied to every hotkey, such as provider defaults. Each applies to the kinds that support it. */
13
+ export type UseHotkeyCommonOptions = AdapterCommonOptions<UseHotkeyTarget>;
14
+ /** The options type for a hotkey: chord options for chords, sequence options for sequences. */
15
+ export type UseHotkeyOptionsFor<T extends RegisterableHotkey> = AdapterOptionsFor<T, UseHotkeyTarget>;
11
16
  /**
12
- * React hook for registering a keyboard hotkey.
17
+ * Options for an app hotkey at the call site. `meta` comes from the declaration.
18
+ * Kind-specific options follow the declared default: `requireReset` for a chord,
19
+ * `timeout` for a sequence, both for an app hotkey without a default. If the
20
+ * user rebinds it to the other kind, options for the old kind no longer apply.
21
+ */
22
+ export type UseAppHotkeyOptions<T extends AppHotkey = AppHotkey> = AdapterAppHotkeyOptions<T, UseHotkeyTarget>;
23
+ /**
24
+ * React hook for registering a hotkey: a chord such as `'Mod+S'`, a sequence
25
+ * such as `['G', 'G']`, or an app hotkey from `defineAppHotkeys`.
13
26
  *
14
- * Uses the singleton HotkeyManager for efficient event handling.
15
- * The callback receives both the keyboard event and a context object
16
- * containing the hotkey string and parsed hotkey.
27
+ * Uses the singleton HotkeyManager for efficient event handling. The callback
28
+ * and options sync on every render, so callbacks that reference React state
29
+ * always see the latest values. `enabled: false` keeps the registration
30
+ * (visible in devtools) and only suppresses firing.
17
31
  *
18
- * This hook syncs the callback and options on every render to avoid
19
- * stale closures. This means
20
- * callbacks that reference React state will always have access to
21
- * the latest values.
32
+ * Chords accept `requireReset`; sequences accept `timeout`. A one-step
33
+ * sequence registers as its chord. An app hotkey uses its effective hotkey
34
+ * from `HotkeysProvider` (including the user's custom hotkey) and its declared
35
+ * meta and options, and registers nothing while unbound.
22
36
  *
23
- * @param hotkey - The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)
37
+ * Options merge in this order: provider defaults, app hotkey declaration,
38
+ * call-site options.
39
+ *
40
+ * @param hotkey - A chord string, RawHotkey object, sequence, or app hotkey
24
41
  * @param callback - The function to call when the hotkey is pressed
25
- * @param options - Options for the hotkey behavior. `enabled: false` keeps the registration (visible in devtools)
26
- * and only suppresses firing; the hook updates the existing handle instead of unregistering.
42
+ * @param options - Options for the hotkey behavior
27
43
  *
28
44
  * @example
29
45
  * ```tsx
30
- * function SaveButton() {
46
+ * function Editor() {
31
47
  * const [count, setCount] = useState(0)
32
48
  *
33
- * // Callback always has access to latest count value
34
- * useHotkey('Mod+S', (event, { hotkey }) => {
35
- * console.log(`Save triggered, count is ${count}`)
36
- * handleSave()
37
- * })
38
- *
39
- * return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
40
- * }
41
- * ```
42
- *
43
- * @example
44
- * ```tsx
45
- * function Modal({ isOpen, onClose }) {
46
- * // enabled option is synced on every render
47
- * useHotkey('Escape', () => {
48
- * onClose()
49
- * }, { enabled: isOpen })
50
- *
51
- * if (!isOpen) return null
52
- * return <div className="modal">...</div>
49
+ * useHotkey('Mod+S', () => save(count))
50
+ * useHotkey(['G', 'G'], () => scrollToTop(), { timeout: 500 })
51
+ * useHotkey('Escape', () => close(), { enabled: isOpen })
53
52
  * }
54
53
  * ```
55
54
  *
56
55
  * @example
57
56
  * ```tsx
58
- * function Editor() {
59
- * const editorRef = useRef<HTMLDivElement>(null)
60
- *
61
- * // Scoped to a specific element
62
- * useHotkey('Mod+S', () => {
63
- * save()
64
- * }, { target: editorRef })
65
- *
66
- * return <div ref={editorRef}>...</div>
57
+ * function Toolbar() {
58
+ * const hotkeys = useAppHotkeys()
59
+ * useHotkey(hotkeys['doc.save'], () => save())
67
60
  * }
68
61
  * ```
69
62
  */
70
- export declare function useHotkey(hotkey: RegisterableHotkey, callback: HotkeyCallback, options?: UseHotkeyOptions): void;
63
+ export declare function useHotkey<T extends AppHotkey>(hotkey: T, callback: HotkeyCallback, options?: UseAppHotkeyOptions<T>): void;
64
+ export declare function useHotkey<T extends RegisterableHotkey>(hotkey: T, callback: HotkeyCallback, options?: UseHotkeyOptionsFor<T>): void;
71
65
  //#endregion
package/dist/useHotkey.js CHANGED
@@ -1,124 +1,12 @@
1
- import { useDefaultHotkeysOptions } from "./HotkeysProvider.js";
2
- import { isRef } from "./utils.js";
3
- import { detectPlatform, getHotkeyManager, normalizeRegisterableHotkey } from "@tanstack/hotkeys";
4
- import { useEffect, useRef } from "react";
1
+ import { useHotkeys } from "./useHotkeys.js";
5
2
 
6
3
  //#region src/useHotkey.ts
7
- /**
8
- * React hook for registering a keyboard hotkey.
9
- *
10
- * Uses the singleton HotkeyManager for efficient event handling.
11
- * The callback receives both the keyboard event and a context object
12
- * containing the hotkey string and parsed hotkey.
13
- *
14
- * This hook syncs the callback and options on every render to avoid
15
- * stale closures. This means
16
- * callbacks that reference React state will always have access to
17
- * the latest values.
18
- *
19
- * @param hotkey - The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)
20
- * @param callback - The function to call when the hotkey is pressed
21
- * @param options - Options for the hotkey behavior. `enabled: false` keeps the registration (visible in devtools)
22
- * and only suppresses firing; the hook updates the existing handle instead of unregistering.
23
- *
24
- * @example
25
- * ```tsx
26
- * function SaveButton() {
27
- * const [count, setCount] = useState(0)
28
- *
29
- * // Callback always has access to latest count value
30
- * useHotkey('Mod+S', (event, { hotkey }) => {
31
- * console.log(`Save triggered, count is ${count}`)
32
- * handleSave()
33
- * })
34
- *
35
- * return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
36
- * }
37
- * ```
38
- *
39
- * @example
40
- * ```tsx
41
- * function Modal({ isOpen, onClose }) {
42
- * // enabled option is synced on every render
43
- * useHotkey('Escape', () => {
44
- * onClose()
45
- * }, { enabled: isOpen })
46
- *
47
- * if (!isOpen) return null
48
- * return <div className="modal">...</div>
49
- * }
50
- * ```
51
- *
52
- * @example
53
- * ```tsx
54
- * function Editor() {
55
- * const editorRef = useRef<HTMLDivElement>(null)
56
- *
57
- * // Scoped to a specific element
58
- * useHotkey('Mod+S', () => {
59
- * save()
60
- * }, { target: editorRef })
61
- *
62
- * return <div ref={editorRef}>...</div>
63
- * }
64
- * ```
65
- */
66
- function useHotkey(hotkey, callback, options = {}) {
67
- const mergedOptions = {
68
- ...useDefaultHotkeysOptions().hotkey,
69
- ...options
70
- };
71
- const manager = getHotkeyManager();
72
- const registrationRef = useRef(null);
73
- const callbackRef = useRef(callback);
74
- const optionsRef = useRef(mergedOptions);
75
- const managerRef = useRef(manager);
76
- callbackRef.current = callback;
77
- optionsRef.current = mergedOptions;
78
- managerRef.current = manager;
79
- const prevTargetRef = useRef(null);
80
- const prevHotkeyRef = useRef(null);
81
- const platform = mergedOptions.platform ?? detectPlatform();
82
- const hotkeyString = normalizeRegisterableHotkey(hotkey, platform);
83
- const { target: _target, ...optionsWithoutTarget } = mergedOptions;
84
- useEffect(() => {
85
- const resolvedTarget = isRef(optionsRef.current.target) ? optionsRef.current.target.current : optionsRef.current.target ?? (typeof document !== "undefined" ? document : null);
86
- if (!resolvedTarget) {
87
- if (registrationRef.current?.isActive) {
88
- registrationRef.current.unregister();
89
- registrationRef.current = null;
90
- }
91
- prevTargetRef.current = null;
92
- prevHotkeyRef.current = null;
93
- return;
94
- }
95
- const targetChanged = prevTargetRef.current !== null && prevTargetRef.current !== resolvedTarget;
96
- const hotkeyChanged = prevHotkeyRef.current !== null && prevHotkeyRef.current !== hotkeyString;
97
- if (registrationRef.current?.isActive && (targetChanged || hotkeyChanged)) {
98
- registrationRef.current.unregister();
99
- registrationRef.current = null;
100
- }
101
- if (!registrationRef.current || !registrationRef.current.isActive) registrationRef.current = managerRef.current.register(hotkeyString, callbackRef.current, {
102
- ...optionsRef.current,
103
- target: resolvedTarget
104
- });
105
- prevTargetRef.current = resolvedTarget;
106
- prevHotkeyRef.current = hotkeyString;
107
- });
108
- useEffect(() => {
109
- return () => {
110
- if (registrationRef.current?.isActive) {
111
- registrationRef.current.unregister();
112
- registrationRef.current = null;
113
- }
114
- };
115
- }, []);
116
- useEffect(() => {
117
- if (registrationRef.current?.isActive) {
118
- registrationRef.current.callback = callback;
119
- registrationRef.current.setOptions(optionsWithoutTarget);
120
- }
121
- });
4
+ function useHotkey(hotkey, callback, options) {
5
+ useHotkeys([{
6
+ hotkey,
7
+ callback,
8
+ options
9
+ }]);
122
10
  }
123
11
 
124
12
  //#endregion
@@ -1,11 +1,13 @@
1
- import { HeldModifierOptions, RegisterableHotkey } from "@tanstack/hotkeys";
1
+ import { AppHotkey, HeldModifierOptions, RegisterableHotkey } from "@tanstack/hotkeys";
2
2
  //#region src/useHotkeyHint.d.ts
3
3
  /**
4
4
  * Returns a boolean indicating whether held modifiers reveal this shortcut.
5
5
  * Only visibility changes from held-key updates cause a rerender.
6
6
  * Uses the nonempty-subset/AltGr rules of `matchesHeldModifiers`; `exact` requires
7
- * every modifier. Combine with the action's enabled state before showing a badge.
8
- * This helper does not register a shortcut or check whether its target is focused.
7
+ * every modifier. A sequence reveals its hint from its first step, and an app
8
+ * hotkey uses its effective hotkey (false while unbound). Combine with the
9
+ * action's enabled state before showing a badge. This helper does not register
10
+ * a shortcut or check whether its target is focused.
9
11
  */
10
- export declare function useHotkeyHint(hotkey: RegisterableHotkey, options?: HeldModifierOptions): boolean;
12
+ export declare function useHotkeyHint(hotkey: RegisterableHotkey | AppHotkey, options?: HeldModifierOptions): boolean;
11
13
  //#endregion
@@ -1,4 +1,5 @@
1
- import { getKeyStateTracker, matchesHeldModifiers } from "@tanstack/hotkeys";
1
+ import { useAppHotkeysStoreState } from "./_appHotkeysStore.js";
2
+ import { getKeyStateTracker, isAppHotkey, matchesHeldModifiers, resolveAppHotkey } from "@tanstack/hotkeys";
2
3
  import { useSelector } from "@tanstack/react-store";
3
4
 
4
5
  //#region src/useHotkeyHint.ts
@@ -6,11 +7,15 @@ import { useSelector } from "@tanstack/react-store";
6
7
  * Returns a boolean indicating whether held modifiers reveal this shortcut.
7
8
  * Only visibility changes from held-key updates cause a rerender.
8
9
  * Uses the nonempty-subset/AltGr rules of `matchesHeldModifiers`; `exact` requires
9
- * every modifier. Combine with the action's enabled state before showing a badge.
10
- * This helper does not register a shortcut or check whether its target is focused.
10
+ * every modifier. A sequence reveals its hint from its first step, and an app
11
+ * hotkey uses its effective hotkey (false while unbound). Combine with the
12
+ * action's enabled state before showing a badge. This helper does not register
13
+ * a shortcut or check whether its target is focused.
11
14
  */
12
15
  function useHotkeyHint(hotkey, options = {}) {
13
- return useSelector(getKeyStateTracker().store, (state) => matchesHeldModifiers(hotkey, state.heldKeys, options));
16
+ const store = useAppHotkeysStoreState();
17
+ const resolved = isAppHotkey(hotkey) ? resolveAppHotkey(hotkey, store).hotkey : hotkey;
18
+ return useSelector(getKeyStateTracker().store, (state) => resolved !== null && matchesHeldModifiers(resolved, state.heldKeys, options));
14
19
  }
15
20
 
16
21
  //#endregion