@tanstack/preact-hotkeys 0.19.1 → 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.
@@ -1,9 +1,15 @@
1
1
  import { UseHotkeyCommonOptions } from "./useHotkey.js";
2
- import { AppHotkeys, AppHotkeysStore, CustomHotkeys, HotkeyChordRecorderOptions, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
2
+ import { AppHotkeys, AppHotkeysStore, CustomHotkeys, HotkeyChordRecorderOptions, HotkeyPlatform, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
3
3
  import { ComponentChildren } from "preact";
4
4
  import { HotkeysProviderScopeProps } from "@tanstack/hotkeys/adapter";
5
5
  //#region src/HotkeysProvider.d.ts
6
6
  export interface HotkeysProviderOptions {
7
+ /**
8
+ * The user's platform, for labels from `useFormatForDisplay`, hotkeys, and
9
+ * recorders. Defaults to detection. When server rendering, set it from the
10
+ * request with `detectPlatform(userAgent)`.
11
+ */
12
+ platform?: HotkeyPlatform;
7
13
  /**
8
14
  * Defaults for every hotkey registered by `useHotkey` and `useHotkeys`.
9
15
  * `requireReset` applies to chords and `timeout` applies to sequences.
@@ -1,6 +1,6 @@
1
1
  import { publishAppHotkeysStore } from "@tanstack/hotkeys";
2
2
  import { createContext } from "preact";
3
- import { useContext, useEffect, useMemo, useState } from "preact/hooks";
3
+ import { useContext, useLayoutEffect, useMemo, useState } from "preact/hooks";
4
4
  import { activateHotkeysProviderScope, createAppHotkeysStore, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults } from "@tanstack/hotkeys/adapter";
5
5
  import { jsx } from "preact/jsx-runtime";
6
6
 
@@ -56,10 +56,10 @@ function HotkeysProvider({ children, appHotkeys, customHotkeys, onCustomHotkeysC
56
56
  active,
57
57
  parent: parent.defaultOptions.hotkey?.scope
58
58
  });
59
- useEffect(() => {
59
+ useLayoutEffect(() => {
60
60
  return scopeKey ? activateHotkeysProviderScope(scopeKey) : void 0;
61
61
  }, [scopeKey]);
62
- useEffect(() => {
62
+ useLayoutEffect(() => {
63
63
  store?.setOptions({
64
64
  customHotkeys,
65
65
  onCustomHotkeysChange
@@ -69,10 +69,10 @@ function HotkeysProvider({ children, appHotkeys, customHotkeys, onCustomHotkeysC
69
69
  customHotkeys,
70
70
  onCustomHotkeysChange
71
71
  ]);
72
- useEffect(() => {
72
+ useLayoutEffect(() => {
73
73
  if (appHotkeys) store?.setAppHotkeys(appHotkeys);
74
74
  }, [store, appHotkeys]);
75
- useEffect(() => {
75
+ useLayoutEffect(() => {
76
76
  if (!store) return;
77
77
  return publishAppHotkeysStore(store);
78
78
  }, [store]);
package/dist/index.d.ts CHANGED
@@ -5,9 +5,10 @@ import { PreactHotkeyRecorder, useHotkeyRecorder } from "./useHotkeyRecorder.js"
5
5
  import { useHotkeyRegistrations } from "./useHotkeyRegistrations.js";
6
6
  import { useHotkeyScopesState } from "./useHotkeyScopesState.js";
7
7
  import { useHotkeyHint } from "./useHotkeyHint.js";
8
+ import { useFormatForDisplay } from "./useFormatForDisplay.js";
8
9
  import { UseCustomHotkeysResult, useAppHotkeyList, useAppHotkeys, useCustomHotkeys } from "./useAppHotkeys.js";
9
10
  import { useHeldKeys } from "./useHeldKeys.js";
10
11
  import { useHeldKeyCodes } from "./useHeldKeyCodes.js";
11
12
  import { useKeyHold } from "./useKeyHold.js";
12
13
  export * from "@tanstack/hotkeys";
13
- export { HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, PreactHotkeyRecorder, UseAppHotkeyOptions, UseCustomHotkeysResult, UseHotkeyChordOptions, UseHotkeyCommonOptions, UseHotkeyDefinition, UseHotkeyOptions, UseHotkeyOptionsFor, UseHotkeySequenceOptions, UseHotkeyTarget, useAppHotkeyList, useAppHotkeys, useCustomHotkeys, useDefaultHotkeysOptions, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyHint, useHotkeyRecorder, useHotkeyRegistrations, useHotkeyScopesState, useHotkeys, useHotkeysContext, useKeyHold };
14
+ export { HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, PreactHotkeyRecorder, UseAppHotkeyOptions, UseCustomHotkeysResult, UseHotkeyChordOptions, UseHotkeyCommonOptions, UseHotkeyDefinition, UseHotkeyOptions, UseHotkeyOptionsFor, UseHotkeySequenceOptions, UseHotkeyTarget, useAppHotkeyList, useAppHotkeys, useCustomHotkeys, useDefaultHotkeysOptions, useFormatForDisplay, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyHint, useHotkeyRecorder, useHotkeyRegistrations, useHotkeyScopesState, useHotkeys, useHotkeysContext, useKeyHold };
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { useHotkeyRecorder } from "./useHotkeyRecorder.js";
5
5
  import { useHotkeyRegistrations } from "./useHotkeyRegistrations.js";
6
6
  import { useHotkeyScopesState } from "./useHotkeyScopesState.js";
7
7
  import { useHotkeyHint } from "./useHotkeyHint.js";
8
+ import { useFormatForDisplay } from "./useFormatForDisplay.js";
8
9
  import { useAppHotkeyList, useAppHotkeys, useCustomHotkeys } from "./useAppHotkeys.js";
9
10
  import { useHeldKeys } from "./useHeldKeys.js";
10
11
  import { useHeldKeyCodes } from "./useHeldKeyCodes.js";
@@ -12,4 +13,4 @@ import { useKeyHold } from "./useKeyHold.js";
12
13
 
13
14
  export * from "@tanstack/hotkeys"
14
15
 
15
- export { HotkeysProvider, useAppHotkeyList, useAppHotkeys, useCustomHotkeys, useDefaultHotkeysOptions, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyHint, useHotkeyRecorder, useHotkeyRegistrations, useHotkeyScopesState, useHotkeys, useHotkeysContext, useKeyHold };
16
+ export { HotkeysProvider, useAppHotkeyList, useAppHotkeys, useCustomHotkeys, useDefaultHotkeysOptions, useFormatForDisplay, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyHint, useHotkeyRecorder, useHotkeyRegistrations, useHotkeyScopesState, useHotkeys, useHotkeysContext, useKeyHold };
@@ -0,0 +1,27 @@
1
+ import { formatForDisplay } from "@tanstack/hotkeys";
2
+ //#region src/useFormatForDisplay.d.ts
3
+ /**
4
+ * Returns `formatForDisplay` with the platform from `HotkeysProvider`, or the
5
+ * detected platform without one. A `platform` passed to the returned function
6
+ * wins.
7
+ *
8
+ * Call the hook once at the top of the component; the returned function can
9
+ * be called anywhere, such as in a loop or a condition. Server-rendered apps
10
+ * should set the provider's `defaultOptions.platform` from the request with
11
+ * `detectPlatform(userAgent)`, so the server HTML matches the user's platform.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * function SaveButton() {
16
+ * const format = useFormatForDisplay()
17
+ *
18
+ * return (
19
+ * <button>
20
+ * Save <kbd>{format('Mod+S')}</kbd>
21
+ * </button>
22
+ * )
23
+ * }
24
+ * ```
25
+ */
26
+ export declare function useFormatForDisplay(): typeof formatForDisplay;
27
+ //#endregion
@@ -0,0 +1,38 @@
1
+ import { useDefaultHotkeysOptions } from "./HotkeysProvider.js";
2
+ import { formatForDisplay } from "@tanstack/hotkeys";
3
+ import { useCallback } from "preact/hooks";
4
+
5
+ //#region src/useFormatForDisplay.ts
6
+ /**
7
+ * Returns `formatForDisplay` with the platform from `HotkeysProvider`, or the
8
+ * detected platform without one. A `platform` passed to the returned function
9
+ * wins.
10
+ *
11
+ * Call the hook once at the top of the component; the returned function can
12
+ * be called anywhere, such as in a loop or a condition. Server-rendered apps
13
+ * should set the provider's `defaultOptions.platform` from the request with
14
+ * `detectPlatform(userAgent)`, so the server HTML matches the user's platform.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * function SaveButton() {
19
+ * const format = useFormatForDisplay()
20
+ *
21
+ * return (
22
+ * <button>
23
+ * Save <kbd>{format('Mod+S')}</kbd>
24
+ * </button>
25
+ * )
26
+ * }
27
+ * ```
28
+ */
29
+ function useFormatForDisplay() {
30
+ const platform = useDefaultHotkeysOptions().platform;
31
+ return useCallback((hotkey, options) => formatForDisplay(hotkey, {
32
+ ...options,
33
+ platform: options?.platform ?? platform
34
+ }), [platform]);
35
+ }
36
+
37
+ //#endregion
38
+ export { useFormatForDisplay };
@@ -1,7 +1,7 @@
1
1
  import { useDefaultHotkeysOptions } from "./HotkeysProvider.js";
2
2
  import { useSelector } from "./_useSelector.js";
3
3
  import { HotkeyRecorder } from "@tanstack/hotkeys";
4
- import { useEffect, useRef } from "preact/hooks";
4
+ import { useLayoutEffect, useRef, useState } from "preact/hooks";
5
5
  import { withDefaults } from "@tanstack/hotkeys/adapter";
6
6
 
7
7
  //#region src/useHotkeyRecorder.ts
@@ -14,19 +14,22 @@ function useHotkeyRecorder(options) {
14
14
  const isRecording = useSelector(recorder.store, (state) => state.isRecording);
15
15
  const steps = useSelector(recorder.store, (state) => state.steps);
16
16
  const recordedHotkey = useSelector(recorder.store, (state) => state.recordedHotkey);
17
- useEffect(() => {
17
+ useLayoutEffect(() => {
18
18
  return () => {
19
19
  recorderRef.current?.destroy();
20
20
  };
21
21
  }, []);
22
- return {
23
- isRecording,
24
- steps,
25
- recordedHotkey,
22
+ const [actions] = useState(() => ({
26
23
  startRecording: () => recorderRef.current?.start(),
27
24
  stopRecording: () => recorderRef.current?.stop(),
28
25
  cancelRecording: () => recorderRef.current?.cancel(),
29
26
  commitRecording: () => recorderRef.current?.commit()
27
+ }));
28
+ return {
29
+ isRecording,
30
+ steps,
31
+ recordedHotkey,
32
+ ...actions
30
33
  };
31
34
  }
32
35
 
@@ -1,7 +1,7 @@
1
1
  import { useHotkeysContext } from "./HotkeysProvider.js";
2
2
  import { useAppHotkeysStoreState } from "./_appHotkeysStore.js";
3
3
  import { resolveTarget } from "./utils.js";
4
- import { useEffect, useRef } from "preact/hooks";
4
+ import { useLayoutEffect, useRef } from "preact/hooks";
5
5
  import { createHotkeyBindings, prepareHotkeyDefinitions } from "@tanstack/hotkeys/adapter";
6
6
 
7
7
  //#region src/useHotkeys.ts
@@ -62,7 +62,7 @@ function useHotkeys(hotkeys, commonOptions = {}) {
62
62
  appHotkeysStore
63
63
  };
64
64
  const bindingsRef = useRef(null);
65
- useEffect(() => {
65
+ useLayoutEffect(() => {
66
66
  bindingsRef.current ??= createHotkeyBindings();
67
67
  const { hotkeys, ...context } = latestRef.current;
68
68
  bindingsRef.current.update(prepareHotkeyDefinitions(hotkeys, {
@@ -70,7 +70,7 @@ function useHotkeys(hotkeys, commonOptions = {}) {
70
70
  resolveTarget
71
71
  }));
72
72
  });
73
- useEffect(() => {
73
+ useLayoutEffect(() => {
74
74
  return () => {
75
75
  bindingsRef.current?.destroy();
76
76
  bindingsRef.current = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/preact-hotkeys",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "description": "Preact adapter for TanStack Hotkeys",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -35,7 +35,7 @@
35
35
  "skills"
36
36
  ],
37
37
  "dependencies": {
38
- "@tanstack/hotkeys": "0.22.0"
38
+ "@tanstack/hotkeys": "0.23.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@preact/preset-vite": "^2.10.6",
@@ -52,6 +52,7 @@ export function SaveButton({ onSave }: { onSave: () => void }) {
52
52
  | `<HotkeysProvider>` | Set default options, a scope (`exclusive` for dialogs), `enabled`, and app hotkeys for the components inside it. |
53
53
  | `useHotkeysContext()` | Read the nearest provider's merged defaults and enabled state, or `null` outside a provider. |
54
54
  | `useDefaultHotkeysOptions()` | Read the default options merged from every provider above. |
55
+ | `useFormatForDisplay()` | Get `formatForDisplay` with the provider's `platform` filled in. Use it for labels in server-rendered apps; call it at the top of the component. |
55
56
  | `useAppHotkeys()` | Get the declared app hotkeys with the user's custom hotkeys applied, to pass to the register API. |
56
57
  | `useAppHotkeyList()` | List every app hotkey with its status, including ones whose components aren't mounted, for a cheat sheet or settings screen. |
57
58
  | `useCustomHotkeys()` | Read, change, and reset the user's custom hotkeys, and check a new one for conflicts. |
@@ -101,10 +102,12 @@ A hook uses the nearest `HotkeysProvider` above its component. Registering `Esca
101
102
 
102
103
  ## Pitfalls
103
104
 
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
+ - **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` for an element the component always renders.
106
+ - **Keep elements that mount later in state.** The hook reads a ref only after its component renders, so it misses an element that mounts later, such as one a child shows from its own state. Use `const [el, setEl] = useState<HTMLElement | null>(null)`, `target: el`, and `ref={setEl}`; the hotkey registers when the element mounts and unregisters when it unmounts.
105
107
  - **No dependency arrays or `useCallback`.** The hooks read the hotkey, callback, and options on every render, so inline callbacks always see current state.
106
108
  - **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
109
  - **In tests,** set `<HotkeysProvider defaultOptions={{ hotkey: { platform: 'mac' } }}>`.
110
+ - **Server-rendered labels need the user's platform.** On the server, `formatForDisplay` can't see the user's OS, so it renders Linux labels that differ after hydration for Mac and Windows users. Set the provider's `defaultOptions.platform` to `detectPlatform(userAgent)` from the request's `User-Agent` header, and format labels with `const format = useFormatForDisplay()` at the top of the component, then `format(hotkey)`. Keep plain `formatForDisplay` for labels that render only in the browser.
108
111
 
109
112
  When declaring app hotkeys, building a cheat sheet, or letting users rebind shortcuts, load `@tanstack/hotkeys#app-hotkeys` and read [app hotkeys in Preact](references/app-hotkeys.md).
110
113