@tanstack/preact-hotkeys 0.9.1 → 0.11.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.
Files changed (68) hide show
  1. package/README.md +15 -1
  2. package/dist/index.cjs +2 -0
  3. package/dist/index.d.cts +2 -1
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.js +2 -1
  6. package/dist/useHeldKeyCodes.cjs +1 -1
  7. package/dist/useHeldKeyCodes.cjs.map +1 -1
  8. package/dist/useHeldKeyCodes.js +2 -2
  9. package/dist/useHeldKeyCodes.js.map +1 -1
  10. package/dist/useHeldKeys.cjs +2 -2
  11. package/dist/useHeldKeys.cjs.map +1 -1
  12. package/dist/useHeldKeys.d.cts +1 -1
  13. package/dist/useHeldKeys.d.ts +1 -1
  14. package/dist/useHeldKeys.js +3 -3
  15. package/dist/useHeldKeys.js.map +1 -1
  16. package/dist/useHotkey.cjs +9 -5
  17. package/dist/useHotkey.cjs.map +1 -1
  18. package/dist/useHotkey.js +9 -5
  19. package/dist/useHotkey.js.map +1 -1
  20. package/dist/useHotkeyHint.cjs +18 -0
  21. package/dist/useHotkeyHint.cjs.map +1 -0
  22. package/dist/useHotkeyHint.d.cts +14 -0
  23. package/dist/useHotkeyHint.d.ts +14 -0
  24. package/dist/useHotkeyHint.js +18 -0
  25. package/dist/useHotkeyHint.js.map +1 -0
  26. package/dist/useHotkeyRecorder.cjs +2 -2
  27. package/dist/useHotkeyRecorder.cjs.map +1 -1
  28. package/dist/useHotkeyRecorder.js +3 -3
  29. package/dist/useHotkeyRecorder.js.map +1 -1
  30. package/dist/useHotkeyRegistrations.cjs +5 -2
  31. package/dist/useHotkeyRegistrations.cjs.map +1 -1
  32. package/dist/useHotkeyRegistrations.js +6 -3
  33. package/dist/useHotkeyRegistrations.js.map +1 -1
  34. package/dist/useHotkeySequence.cjs +9 -5
  35. package/dist/useHotkeySequence.cjs.map +1 -1
  36. package/dist/useHotkeySequence.js +9 -5
  37. package/dist/useHotkeySequence.js.map +1 -1
  38. package/dist/useHotkeySequenceRecorder.cjs +3 -3
  39. package/dist/useHotkeySequenceRecorder.cjs.map +1 -1
  40. package/dist/useHotkeySequenceRecorder.js +4 -4
  41. package/dist/useHotkeySequenceRecorder.js.map +1 -1
  42. package/dist/useHotkeySequences.cjs +3 -15
  43. package/dist/useHotkeySequences.cjs.map +1 -1
  44. package/dist/useHotkeySequences.js +3 -15
  45. package/dist/useHotkeySequences.js.map +1 -1
  46. package/dist/useHotkeys.cjs +3 -15
  47. package/dist/useHotkeys.cjs.map +1 -1
  48. package/dist/useHotkeys.js +3 -15
  49. package/dist/useHotkeys.js.map +1 -1
  50. package/dist/useKeyHold.cjs +2 -2
  51. package/dist/useKeyHold.cjs.map +1 -1
  52. package/dist/useKeyHold.d.cts +1 -1
  53. package/dist/useKeyHold.d.ts +1 -1
  54. package/dist/useKeyHold.js +3 -3
  55. package/dist/useKeyHold.js.map +1 -1
  56. package/package.json +4 -4
  57. package/src/index.ts +1 -0
  58. package/src/useHeldKeyCodes.ts +2 -2
  59. package/src/useHeldKeys.ts +3 -3
  60. package/src/useHotkey.ts +11 -8
  61. package/src/useHotkeyHint.ts +19 -0
  62. package/src/useHotkeyRecorder.ts +4 -4
  63. package/src/useHotkeyRegistrations.ts +12 -6
  64. package/src/useHotkeySequence.ts +14 -10
  65. package/src/useHotkeySequenceRecorder.ts +4 -4
  66. package/src/useHotkeySequences.ts +4 -18
  67. package/src/useHotkeys.ts +4 -18
  68. package/src/useKeyHold.ts +3 -3
@@ -1,4 +1,4 @@
1
- import { useStore } from '@tanstack/preact-store'
1
+ import { useSelector } from '@tanstack/preact-store'
2
2
  import { getKeyStateTracker } from '@tanstack/hotkeys'
3
3
 
4
4
  /**
@@ -29,5 +29,5 @@ import { getKeyStateTracker } from '@tanstack/hotkeys'
29
29
  */
30
30
  export function useHeldKeyCodes(): Record<string, string> {
31
31
  const tracker = getKeyStateTracker()
32
- return useStore(tracker.store, (state) => state.heldCodes)
32
+ return useSelector(tracker.store, (state) => state.heldCodes)
33
33
  }
@@ -1,10 +1,10 @@
1
- import { useStore } from '@tanstack/preact-store'
1
+ import { useSelector } from '@tanstack/preact-store'
2
2
  import { getKeyStateTracker } from '@tanstack/hotkeys'
3
3
 
4
4
  /**
5
5
  * Preact hook that returns an array of currently held keyboard keys.
6
6
  *
7
- * This hook uses `useStore` from `@tanstack/preact-store` to subscribe
7
+ * This hook uses `useSelector` from `@tanstack/preact-store` to subscribe
8
8
  * to the global KeyStateTracker and updates whenever keys are pressed
9
9
  * or released.
10
10
  *
@@ -25,5 +25,5 @@ import { getKeyStateTracker } from '@tanstack/hotkeys'
25
25
  */
26
26
  export function useHeldKeys(): Array<string> {
27
27
  const tracker = getKeyStateTracker()
28
- return useStore(tracker.store, (state) => state.heldKeys)
28
+ return useSelector(tracker.store, (state) => state.heldKeys)
29
29
  }
package/src/useHotkey.ts CHANGED
@@ -169,7 +169,9 @@ export function useHotkey(
169
169
  // Update tracking refs
170
170
  prevTargetRef.current = resolvedTarget
171
171
  prevHotkeyRef.current = hotkeyString
172
+ })
172
173
 
174
+ useEffect(() => {
173
175
  // Cleanup on unmount
174
176
  return () => {
175
177
  if (registrationRef.current?.isActive) {
@@ -177,12 +179,13 @@ export function useHotkey(
177
179
  registrationRef.current = null
178
180
  }
179
181
  }
180
- }, [hotkeyString])
181
-
182
- // Sync callback and options on EVERY render (outside useEffect)
183
- // This avoids stale closures - the callback always has access to latest state
184
- if (registrationRef.current?.isActive) {
185
- registrationRef.current.callback = callback
186
- registrationRef.current.setOptions(optionsWithoutTarget)
187
- }
182
+ }, [])
183
+
184
+ // Publish option changes after commit, never during render.
185
+ useEffect(() => {
186
+ if (registrationRef.current?.isActive) {
187
+ registrationRef.current.callback = callback
188
+ registrationRef.current.setOptions(optionsWithoutTarget)
189
+ }
190
+ })
188
191
  }
@@ -0,0 +1,19 @@
1
+ import { useSelector } from '@tanstack/preact-store'
2
+ import { getKeyStateTracker, matchesHeldModifiers } from '@tanstack/hotkeys'
3
+ import type { HeldModifierOptions, RegisterableHotkey } from '@tanstack/hotkeys'
4
+
5
+ /**
6
+ * Returns a boolean indicating whether held modifiers reveal this shortcut.
7
+ * Only visibility changes from held-key updates cause a rerender.
8
+ * 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.
11
+ */
12
+ export function useHotkeyHint(
13
+ hotkey: RegisterableHotkey,
14
+ options: HeldModifierOptions = {},
15
+ ): boolean {
16
+ return useSelector(getKeyStateTracker().store, (state) =>
17
+ matchesHeldModifiers(hotkey, state.heldKeys, options),
18
+ )
19
+ }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef } from 'preact/hooks'
2
- import { useStore } from '@tanstack/preact-store'
2
+ import { useSelector } from '@tanstack/preact-store'
3
3
  import { HotkeyRecorder } from '@tanstack/hotkeys'
4
4
  import { useDefaultHotkeysOptions } from './HotkeysProvider'
5
5
  import type { Hotkey, HotkeyRecorderOptions } from '@tanstack/hotkeys'
@@ -74,12 +74,12 @@ export function useHotkeyRecorder(
74
74
  // This ensures callbacks always have access to latest values
75
75
  recorderRef.current.setOptions(mergedOptions)
76
76
 
77
- // Subscribe to recorder state using useStore (same pattern as useHeldKeys)
78
- const isRecording = useStore(
77
+ // Subscribe to recorder state using useSelector (same pattern as useHeldKeys)
78
+ const isRecording = useSelector(
79
79
  recorderRef.current.store,
80
80
  (state) => state.isRecording,
81
81
  )
82
- const recordedHotkey = useStore(
82
+ const recordedHotkey = useSelector(
83
83
  recorderRef.current.store,
84
84
  (state) => state.recordedHotkey,
85
85
  )
@@ -1,4 +1,5 @@
1
- import { useStore } from '@tanstack/preact-store'
1
+ import { useMemo } from 'preact/hooks'
2
+ import { useSelector } from '@tanstack/preact-store'
2
3
  import {
3
4
  getHotkeyManager,
4
5
  getSequenceManager,
@@ -33,12 +34,17 @@ export function useHotkeyRegistrations(): HotkeyRegistrationsResult {
33
34
  const hotkeyManager = getHotkeyManager()
34
35
  const sequenceManager = getSequenceManager()
35
36
 
36
- const hotkeys = useStore(hotkeyManager.registrations, (state) =>
37
- Array.from(state.values()).map(toHotkeyRegistrationView),
37
+ // Select stable store snapshots before deriving arrays; fresh selector arrays
38
+ // make Preact's external-store subscription render continuously.
39
+ const hotkeyState = useSelector(hotkeyManager.registrations)
40
+ const sequenceState = useSelector(sequenceManager.registrations)
41
+ const hotkeys = useMemo(
42
+ () => Array.from(hotkeyState.values()).map(toHotkeyRegistrationView),
43
+ [hotkeyState],
38
44
  )
39
-
40
- const sequences = useStore(sequenceManager.registrations, (state) =>
41
- Array.from(state.values()),
45
+ const sequences = useMemo(
46
+ () => Array.from(sequenceState.values()),
47
+ [sequenceState],
42
48
  )
43
49
 
44
50
  return { hotkeys, sequences }
@@ -167,7 +167,9 @@ export function useHotkeySequence(
167
167
  // Update tracking refs
168
168
  prevTargetRef.current = resolvedTarget
169
169
  prevSequenceRef.current = hotkeySequenceString
170
+ })
170
171
 
172
+ useEffect(() => {
171
173
  // Cleanup on unmount
172
174
  return () => {
173
175
  if (registrationRef.current?.isActive) {
@@ -175,14 +177,16 @@ export function useHotkeySequence(
175
177
  registrationRef.current = null
176
178
  }
177
179
  }
178
- }, [hotkeySequenceString])
179
-
180
- // Sync callback and options on EVERY render (outside useEffect)
181
- if (registrationRef.current?.isActive) {
182
- registrationRef.current.callback = (
183
- event: KeyboardEvent,
184
- context: HotkeyCallbackContext,
185
- ) => callbackRef.current(event, context)
186
- registrationRef.current.setOptions(optionsWithoutTarget)
187
- }
180
+ }, [])
181
+
182
+ // Publish option changes after commit, never during render.
183
+ useEffect(() => {
184
+ if (registrationRef.current?.isActive) {
185
+ registrationRef.current.callback = (
186
+ event: KeyboardEvent,
187
+ context: HotkeyCallbackContext,
188
+ ) => callbackRef.current(event, context)
189
+ registrationRef.current.setOptions(optionsWithoutTarget)
190
+ }
191
+ })
188
192
  }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef } from 'preact/hooks'
2
- import { useStore } from '@tanstack/preact-store'
2
+ import { useSelector } from '@tanstack/preact-store'
3
3
  import { HotkeySequenceRecorder } from '@tanstack/hotkeys'
4
4
  import { useDefaultHotkeysOptions } from './HotkeysProvider'
5
5
  import type {
@@ -36,12 +36,12 @@ export function useHotkeySequenceRecorder(
36
36
 
37
37
  recorderRef.current.setOptions(mergedOptions)
38
38
 
39
- const isRecording = useStore(
39
+ const isRecording = useSelector(
40
40
  recorderRef.current.store,
41
41
  (state) => state.isRecording,
42
42
  )
43
- const steps = useStore(recorderRef.current.store, (state) => state.steps)
44
- const recordedSequence = useStore(
43
+ const steps = useSelector(recorderRef.current.store, (state) => state.steps)
44
+ const recordedSequence = useSelector(
45
45
  recorderRef.current.store,
46
46
  (state) => state.recordedSequence,
47
47
  )
@@ -154,6 +154,10 @@ export function useHotkeySequences(
154
154
 
155
155
  const existing = prevRegistrations.get(registrationKey)
156
156
  if (existing?.handle.isActive && existing.target === resolvedTarget) {
157
+ // Publish callback/options only after commit, never during render.
158
+ existing.handle.callback = def.callback
159
+ const { target: _target, ...optionsWithoutTarget } = mergedOptions
160
+ existing.handle.setOptions(optionsWithoutTarget)
157
161
  nextRegistrations.set(registrationKey, existing)
158
162
  continue
159
163
  }
@@ -185,22 +189,4 @@ export function useHotkeySequences(
185
189
  registrationsRef.current = new Map()
186
190
  }
187
191
  }, [])
188
-
189
- for (let i = 0; i < definitions.length; i++) {
190
- const def = definitions[i]!
191
- const seqStr = sequenceStrings[i]!
192
- const registrationKey = `${i}:${seqStr}`
193
- const handle = registrationsRef.current.get(registrationKey)?.handle
194
-
195
- if (handle?.isActive && def.sequence.length > 0) {
196
- handle.callback = def.callback
197
- const mergedOptions = {
198
- ...defaultOptions,
199
- ...commonOptions,
200
- ...def.options,
201
- } as UseHotkeySequenceOptions
202
- const { target: _target, ...optionsWithoutTarget } = mergedOptions
203
- handle.setOptions(optionsWithoutTarget)
204
- }
205
- }
206
192
  }
package/src/useHotkeys.ts CHANGED
@@ -155,6 +155,10 @@ export function useHotkeys(
155
155
 
156
156
  const existing = prevRegistrations.get(registrationKey)
157
157
  if (existing?.handle.isActive && existing.target === resolvedTarget) {
158
+ // Publish callback/options only after commit, never during render.
159
+ existing.handle.callback = def.callback
160
+ const { target: _target, ...optionsWithoutTarget } = mergedOptions
161
+ existing.handle.setOptions(optionsWithoutTarget)
158
162
  nextRegistrations.set(registrationKey, existing)
159
163
  continue
160
164
  }
@@ -186,22 +190,4 @@ export function useHotkeys(
186
190
  registrationsRef.current = new Map()
187
191
  }
188
192
  }, [])
189
-
190
- for (let i = 0; i < hotkeys.length; i++) {
191
- const def = hotkeys[i]!
192
- const hotkeyStr = hotkeyStrings[i]!
193
- const registrationKey = `${i}:${hotkeyStr}`
194
- const handle = registrationsRef.current.get(registrationKey)?.handle
195
-
196
- if (handle?.isActive) {
197
- handle.callback = def.callback
198
- const mergedOptions = {
199
- ...defaultOptions,
200
- ...commonOptions,
201
- ...def.options,
202
- } as UseHotkeyOptions
203
- const { target: _target, ...optionsWithoutTarget } = mergedOptions
204
- handle.setOptions(optionsWithoutTarget)
205
- }
206
- }
207
193
  }
package/src/useKeyHold.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { useStore } from '@tanstack/preact-store'
1
+ import { useSelector } from '@tanstack/preact-store'
2
2
  import { getKeyStateTracker } from '@tanstack/hotkeys'
3
3
  import type { IndividualKey } from '@tanstack/hotkeys'
4
4
 
5
5
  /**
6
6
  * Preact hook that returns whether a specific key is currently being held.
7
7
  *
8
- * This hook uses `useStore` from `@tanstack/preact-store` to subscribe
8
+ * This hook uses `useSelector` from `@tanstack/preact-store` to subscribe
9
9
  * to the global KeyStateTracker and uses a selector to determine if
10
10
  * the specified key is held.
11
11
  *
@@ -46,7 +46,7 @@ export function useKeyHold(key: IndividualKey): boolean {
46
46
  const tracker = getKeyStateTracker()
47
47
  const normalizedKey = key.toLowerCase()
48
48
 
49
- return useStore(tracker.store, (state) =>
49
+ return useSelector(tracker.store, (state) =>
50
50
  state.heldKeys.some((heldKey) => heldKey.toLowerCase() === normalizedKey),
51
51
  )
52
52
  }