@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,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 { ComponentChildren } from "preact";
5
4
  //#region src/HotkeysProvider.d.ts
6
5
  export interface HotkeysProviderOptions {
7
- hotkey?: Partial<UseHotkeyOptions>;
8
- hotkeyRecorder?: Partial<HotkeyRecorderOptions>;
9
- hotkeySequenceRecorder?: Partial<HotkeySequenceRecorderOptions>;
10
- hotkeySequence?: Partial<UseHotkeySequenceOptions>;
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: ComponentChildren;
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): import("preact").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>): import("preact").JSX.Element;
20
51
  export declare function useHotkeysContext(): HotkeysContextValue | null;
21
52
  export declare function useDefaultHotkeysOptions(): HotkeysProviderOptions;
22
53
  //#endregion
@@ -1,12 +1,46 @@
1
+ import { AppHotkeysStore, publishAppHotkeysStore } from "@tanstack/hotkeys";
1
2
  import { createContext } from "preact";
2
- import { useContext, useMemo } from "preact/hooks";
3
+ import { useContext, useEffect, useMemo, useState } from "preact/hooks";
3
4
  import { jsx } from "preact/jsx-runtime";
4
5
 
5
6
  //#region src/HotkeysProvider.tsx
6
7
  const HotkeysContext = createContext(null);
7
8
  const DEFAULT_OPTIONS = {};
8
- function HotkeysProvider({ children, defaultOptions = DEFAULT_OPTIONS }) {
9
- const contextValue = useMemo(() => ({ defaultOptions }), [defaultOptions]);
9
+ /**
10
+ * Provides default options, app hotkeys, and the user's custom hotkeys.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <HotkeysProvider
15
+ * appHotkeys={appHotkeys}
16
+ * customHotkeys={JSON.parse(localStorage.getItem('hotkeys') ?? '{}')}
17
+ * onCustomHotkeysChange={(custom) =>
18
+ * localStorage.setItem('hotkeys', JSON.stringify(custom))
19
+ * }
20
+ * >
21
+ * <App />
22
+ * </HotkeysProvider>
23
+ * ```
24
+ */
25
+ function HotkeysProvider({ children, appHotkeys, customHotkeys, onCustomHotkeysChange, defaultOptions = DEFAULT_OPTIONS }) {
26
+ const [store] = useState(() => appHotkeys ? new AppHotkeysStore(appHotkeys, {
27
+ customHotkeys,
28
+ onCustomHotkeysChange
29
+ }) : null);
30
+ useEffect(() => {
31
+ store?.setOptions({ onCustomHotkeysChange });
32
+ }, [store, onCustomHotkeysChange]);
33
+ useEffect(() => {
34
+ if (appHotkeys) store?.setAppHotkeys(appHotkeys);
35
+ }, [store, appHotkeys]);
36
+ useEffect(() => {
37
+ if (!store) return;
38
+ return publishAppHotkeysStore(store);
39
+ }, [store]);
40
+ const contextValue = useMemo(() => ({
41
+ appHotkeysStore: store,
42
+ defaultOptions
43
+ }), [store, defaultOptions]);
10
44
  return /* @__PURE__ */ jsx(HotkeysContext.Provider, {
11
45
  value: contextValue,
12
46
  children
@@ -16,7 +50,7 @@ function useHotkeysContext() {
16
50
  return useContext(HotkeysContext);
17
51
  }
18
52
  function useDefaultHotkeysOptions() {
19
- return useContext(HotkeysContext)?.defaultOptions ?? {};
53
+ return useContext(HotkeysContext)?.defaultOptions ?? DEFAULT_OPTIONS;
20
54
  }
21
55
 
22
56
  //#endregion
@@ -0,0 +1,17 @@
1
+ import { useHotkeysContext } from "./HotkeysProvider.js";
2
+ import { Store, useSelector } from "@tanstack/preact-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 { PreactHotkeyRecorder, 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 { PreactHotkeyRecorder, useHotkeyRecorder } from "./useHotkeyRecorder.js";
10
- import { HotkeyRegistrationsResult, useHotkeyRegistrations } from "./useHotkeyRegistrations.js";
11
- import { PreactHotkeySequenceRecorder, useHotkeySequenceRecorder } from "./useHotkeySequenceRecorder.js";
12
- import { useHotkeyHint } from "./useHotkeyHint.js";
13
11
  export * from "@tanstack/hotkeys";
14
- export { HotkeyRegistrationsResult, HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, PreactHotkeyRecorder, PreactHotkeySequenceRecorder, UseHotkeyDefinition, UseHotkeyOptions, UseHotkeySequenceDefinition, UseHotkeySequenceOptions, useDefaultHotkeysOptions, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyHint, useHotkeyRecorder, useHotkeyRegistrations, useHotkeySequence, useHotkeySequenceRecorder, useHotkeySequences, useHotkeys, useHotkeysContext, useKeyHold };
12
+ export { HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, PreactHotkeyRecorder, 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,72 +1,66 @@
1
- import { HotkeyCallback, HotkeyOptions, RegisterableHotkey } from "@tanstack/hotkeys";
1
+ import { AppHotkey, HotkeyCallback, RegisterableHotkey } from "@tanstack/hotkeys";
2
2
  import { RefObject } from "preact";
3
+ import { AdapterAppHotkeyOptions, AdapterChordOptions, AdapterCommonOptions, AdapterHotkeyOptions, AdapterOptionsFor, AdapterSequenceOptions } from "@tanstack/hotkeys/adapter";
3
4
  //#region src/useHotkey.d.ts
4
- export interface UseHotkeyOptions extends Omit<HotkeyOptions, 'target'> {
5
- /**
6
- * The DOM element to attach the event listener to.
7
- * Can be a Preact ref, direct DOM element, or null.
8
- * Defaults to document.
9
- */
10
- target?: RefObject<HTMLElement | null> | HTMLElement | Document | Window | null;
11
- }
5
+ /** A Preact ref, element, document, or window to listen on. */
6
+ export type UseHotkeyTarget = RefObject<HTMLElement | null> | HTMLElement | Document | Window | null;
7
+ /** Options shared by chords and sequences. */
8
+ export type UseHotkeyOptions = AdapterHotkeyOptions<UseHotkeyTarget>;
9
+ /** Options for a chord such as `'Mod+S'`. */
10
+ export type UseHotkeyChordOptions = AdapterChordOptions<UseHotkeyTarget>;
11
+ /** Options for a sequence such as `['G', 'G']`. */
12
+ export type UseHotkeySequenceOptions = AdapterSequenceOptions<UseHotkeyTarget>;
13
+ /** Options applied to every hotkey, such as provider defaults. Each applies to the kinds that support it. */
14
+ export type UseHotkeyCommonOptions = AdapterCommonOptions<UseHotkeyTarget>;
15
+ /** The options type for a hotkey: chord options for chords, sequence options for sequences. */
16
+ export type UseHotkeyOptionsFor<T extends RegisterableHotkey> = AdapterOptionsFor<T, UseHotkeyTarget>;
12
17
  /**
13
- * Preact hook for registering a keyboard hotkey.
18
+ * Options for an app hotkey at the call site. `meta` comes from the declaration.
19
+ * Kind-specific options follow the declared default: `requireReset` for a chord,
20
+ * `timeout` for a sequence, both for an app hotkey without a default. If the
21
+ * user rebinds it to the other kind, options for the old kind no longer apply.
22
+ */
23
+ export type UseAppHotkeyOptions<T extends AppHotkey = AppHotkey> = AdapterAppHotkeyOptions<T, UseHotkeyTarget>;
24
+ /**
25
+ * Preact hook for registering a hotkey: a chord such as `'Mod+S'`, a sequence
26
+ * such as `['G', 'G']`, or an app hotkey from `defineAppHotkeys`.
14
27
  *
15
- * Uses the singleton HotkeyManager for efficient event handling.
16
- * The callback receives both the keyboard event and a context object
17
- * containing the hotkey string and parsed hotkey.
28
+ * Uses the singleton HotkeyManager for efficient event handling. The callback
29
+ * and options sync on every render, so callbacks that reference Preact state
30
+ * always see the latest values. `enabled: false` keeps the registration
31
+ * (visible in devtools) and only suppresses firing.
18
32
  *
19
- * This hook syncs the callback and options on every render to avoid
20
- * stale closures. This means
21
- * callbacks that reference Preact state will always have access to
22
- * the latest values.
33
+ * Chords accept `requireReset`; sequences accept `timeout`. A one-step
34
+ * sequence registers as its chord. An app hotkey uses its effective hotkey
35
+ * from `HotkeysProvider` (including the user's custom hotkey) and its declared
36
+ * meta and options, and registers nothing while unbound.
23
37
  *
24
- * @param hotkey - The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)
38
+ * Options merge in this order: provider defaults, app hotkey declaration,
39
+ * call-site options.
40
+ *
41
+ * @param hotkey - A chord string, RawHotkey object, sequence, or app hotkey
25
42
  * @param callback - The function to call when the hotkey is pressed
26
- * @param options - Options for the hotkey behavior. `enabled: false` keeps the registration (visible in devtools)
27
- * and only suppresses firing; the hook updates the existing handle instead of unregistering.
43
+ * @param options - Options for the hotkey behavior
28
44
  *
29
45
  * @example
30
46
  * ```tsx
31
- * function SaveButton() {
47
+ * function Editor() {
32
48
  * const [count, setCount] = useState(0)
33
49
  *
34
- * // Callback always has access to latest count value
35
- * useHotkey('Mod+S', (event, { hotkey }) => {
36
- * console.log(`Save triggered, count is ${count}`)
37
- * handleSave()
38
- * })
39
- *
40
- * return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
41
- * }
42
- * ```
43
- *
44
- * @example
45
- * ```tsx
46
- * function Modal({ isOpen, onClose }) {
47
- * // enabled option is synced on every render
48
- * useHotkey('Escape', () => {
49
- * onClose()
50
- * }, { enabled: isOpen })
51
- *
52
- * if (!isOpen) return null
53
- * return <div className="modal">...</div>
50
+ * useHotkey('Mod+S', () => save(count))
51
+ * useHotkey(['G', 'G'], () => scrollToTop(), { timeout: 500 })
52
+ * useHotkey('Escape', () => close(), { enabled: isOpen })
54
53
  * }
55
54
  * ```
56
55
  *
57
56
  * @example
58
57
  * ```tsx
59
- * function Editor() {
60
- * const editorRef = useRef<HTMLDivElement>(null)
61
- *
62
- * // Scoped to a specific element
63
- * useHotkey('Mod+S', () => {
64
- * save()
65
- * }, { target: editorRef })
66
- *
67
- * return <div ref={editorRef}>...</div>
58
+ * function Toolbar() {
59
+ * const hotkeys = useAppHotkeys()
60
+ * useHotkey(hotkeys['doc.save'], () => save())
68
61
  * }
69
62
  * ```
70
63
  */
71
- export declare function useHotkey(hotkey: RegisterableHotkey, callback: HotkeyCallback, options?: UseHotkeyOptions): void;
64
+ export declare function useHotkey<T extends AppHotkey>(hotkey: T, callback: HotkeyCallback, options?: UseAppHotkeyOptions<T>): void;
65
+ export declare function useHotkey<T extends RegisterableHotkey>(hotkey: T, callback: HotkeyCallback, options?: UseHotkeyOptionsFor<T>): void;
72
66
  //#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 "preact/hooks";
1
+ import { useHotkeys } from "./useHotkeys.js";
5
2
 
6
3
  //#region src/useHotkey.ts
7
- /**
8
- * Preact 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 Preact 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/preact-store";
3
4
 
4
5
  //#region src/useHotkeyHint.ts
@@ -6,11 +7,15 @@ import { useSelector } from "@tanstack/preact-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