@tanstack/react-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 +19 -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 +2 -2
  31. package/dist/useHotkeyRegistrations.cjs.map +1 -1
  32. package/dist/useHotkeyRegistrations.js +3 -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 +7 -7
  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 +3 -3
  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 -19
  68. package/src/useKeyHold.ts +3 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-hotkeys",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "React adapter for TanStack Hotkeys",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -39,18 +39,18 @@
39
39
  "src"
40
40
  ],
41
41
  "dependencies": {
42
- "@tanstack/react-store": "^0.9.3",
43
- "@tanstack/hotkeys": "0.7.1"
42
+ "@tanstack/react-store": "^0.11.0",
43
+ "@tanstack/hotkeys": "0.9.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@eslint-react/eslint-plugin": "^3.0.0",
46
+ "@eslint-react/eslint-plugin": "^4.2.3",
47
47
  "@testing-library/react": "^16.3.2",
48
48
  "@types/react": "^19.2.14",
49
49
  "@vitejs/plugin-react": "^6.0.1",
50
50
  "eslint-plugin-react-compiler": "19.1.0-rc.2",
51
- "eslint-plugin-react-hooks": "^7.0.1",
52
- "react": "^19.2.4",
53
- "react-dom": "^19.2.4"
51
+ "eslint-plugin-react-hooks": "^7.1.1",
52
+ "react": "^19.2.5",
53
+ "react-dom": "^19.2.5"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "react": ">=16.8",
package/src/index.ts CHANGED
@@ -15,3 +15,4 @@ export * from './useHotkeySequences'
15
15
  export * from './useHotkeyRecorder'
16
16
  export * from './useHotkeyRegistrations'
17
17
  export * from './useHotkeySequenceRecorder'
18
+ export * from './useHotkeyHint'
@@ -1,4 +1,4 @@
1
- import { useStore } from '@tanstack/react-store'
1
+ import { useSelector } from '@tanstack/react-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/react-store'
1
+ import { useSelector } from '@tanstack/react-store'
2
2
  import { getKeyStateTracker } from '@tanstack/hotkeys'
3
3
 
4
4
  /**
5
5
  * React hook that returns an array of currently held keyboard keys.
6
6
  *
7
- * This hook uses `useStore` from `@tanstack/react-store` to subscribe
7
+ * This hook uses `useSelector` from `@tanstack/react-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
@@ -168,7 +168,9 @@ export function useHotkey(
168
168
  // Update tracking refs
169
169
  prevTargetRef.current = resolvedTarget
170
170
  prevHotkeyRef.current = hotkeyString
171
+ })
171
172
 
173
+ useEffect(() => {
172
174
  // Cleanup on unmount
173
175
  return () => {
174
176
  if (registrationRef.current?.isActive) {
@@ -176,12 +178,13 @@ export function useHotkey(
176
178
  registrationRef.current = null
177
179
  }
178
180
  }
179
- }, [hotkeyString])
180
-
181
- // Sync callback and options on EVERY render (outside useEffect)
182
- // This avoids stale closures - the callback always has access to latest state
183
- if (registrationRef.current?.isActive) {
184
- registrationRef.current.callback = callback
185
- registrationRef.current.setOptions(optionsWithoutTarget)
186
- }
181
+ }, [])
182
+
183
+ // Publish option changes after commit, never during render.
184
+ useEffect(() => {
185
+ if (registrationRef.current?.isActive) {
186
+ registrationRef.current.callback = callback
187
+ registrationRef.current.setOptions(optionsWithoutTarget)
188
+ }
189
+ })
187
190
  }
@@ -0,0 +1,19 @@
1
+ import { useSelector } from '@tanstack/react-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 'react'
2
- import { useStore } from '@tanstack/react-store'
2
+ import { useSelector } from '@tanstack/react-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,4 @@
1
- import { useStore } from '@tanstack/react-store'
1
+ import { useSelector } from '@tanstack/react-store'
2
2
  import {
3
3
  getHotkeyManager,
4
4
  getSequenceManager,
@@ -55,11 +55,11 @@ export function useHotkeyRegistrations(): HotkeyRegistrationsResult {
55
55
  const hotkeyManager = getHotkeyManager()
56
56
  const sequenceManager = getSequenceManager()
57
57
 
58
- const hotkeys = useStore(hotkeyManager.registrations, (state) =>
58
+ const hotkeys = useSelector(hotkeyManager.registrations, (state) =>
59
59
  Array.from(state.values()).map(toHotkeyRegistrationView),
60
60
  )
61
61
 
62
- const sequences = useStore(sequenceManager.registrations, (state) =>
62
+ const sequences = useSelector(sequenceManager.registrations, (state) =>
63
63
  Array.from(state.values()),
64
64
  )
65
65
 
@@ -166,7 +166,9 @@ export function useHotkeySequence(
166
166
  // Update tracking refs
167
167
  prevTargetRef.current = resolvedTarget
168
168
  prevSequenceRef.current = hotkeySequenceString
169
+ })
169
170
 
171
+ useEffect(() => {
170
172
  // Cleanup on unmount
171
173
  return () => {
172
174
  if (registrationRef.current?.isActive) {
@@ -174,14 +176,16 @@ export function useHotkeySequence(
174
176
  registrationRef.current = null
175
177
  }
176
178
  }
177
- }, [hotkeySequenceString])
178
-
179
- // Sync callback and options on EVERY render (outside useEffect)
180
- if (registrationRef.current?.isActive) {
181
- registrationRef.current.callback = (
182
- event: KeyboardEvent,
183
- context: HotkeyCallbackContext,
184
- ) => callbackRef.current(event, context)
185
- registrationRef.current.setOptions(optionsWithoutTarget)
186
- }
179
+ }, [])
180
+
181
+ // Publish option changes after commit, never during render.
182
+ useEffect(() => {
183
+ if (registrationRef.current?.isActive) {
184
+ registrationRef.current.callback = (
185
+ event: KeyboardEvent,
186
+ context: HotkeyCallbackContext,
187
+ ) => callbackRef.current(event, context)
188
+ registrationRef.current.setOptions(optionsWithoutTarget)
189
+ }
190
+ })
187
191
  }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef } from 'react'
2
- import { useStore } from '@tanstack/react-store'
2
+ import { useSelector } from '@tanstack/react-store'
3
3
  import { HotkeySequenceRecorder } from '@tanstack/hotkeys'
4
4
  import { useDefaultHotkeysOptions } from './HotkeysProvider'
5
5
  import type {
@@ -42,12 +42,12 @@ export function useHotkeySequenceRecorder(
42
42
 
43
43
  recorderRef.current.setOptions(mergedOptions)
44
44
 
45
- const isRecording = useStore(
45
+ const isRecording = useSelector(
46
46
  recorderRef.current.store,
47
47
  (state) => state.isRecording,
48
48
  )
49
- const steps = useStore(recorderRef.current.store, (state) => state.steps)
50
- const recordedSequence = useStore(
49
+ const steps = useSelector(recorderRef.current.store, (state) => state.steps)
50
+ const recordedSequence = useSelector(
51
51
  recorderRef.current.store,
52
52
  (state) => state.recordedSequence,
53
53
  )
@@ -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,23 +190,4 @@ export function useHotkeys(
186
190
  registrationsRef.current = new Map()
187
191
  }
188
192
  }, [])
189
-
190
- // Sync callbacks and options on EVERY render (outside useEffect)
191
- for (let i = 0; i < hotkeys.length; i++) {
192
- const def = hotkeys[i]!
193
- const hotkeyStr = hotkeyStrings[i]!
194
- const registrationKey = `${i}:${hotkeyStr}`
195
- const handle = registrationsRef.current.get(registrationKey)?.handle
196
-
197
- if (handle?.isActive) {
198
- handle.callback = def.callback
199
- const mergedOptions = {
200
- ...defaultOptions,
201
- ...commonOptions,
202
- ...def.options,
203
- } as UseHotkeyOptions
204
- const { target: _target, ...optionsWithoutTarget } = mergedOptions
205
- handle.setOptions(optionsWithoutTarget)
206
- }
207
- }
208
193
  }
package/src/useKeyHold.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { useStore } from '@tanstack/react-store'
1
+ import { useSelector } from '@tanstack/react-store'
2
2
  import { getKeyStateTracker } from '@tanstack/hotkeys'
3
3
  import type { IndividualKey } from '@tanstack/hotkeys'
4
4
 
5
5
  /**
6
6
  * React hook that returns whether a specific key is currently being held.
7
7
  *
8
- * This hook uses `useStore` from `@tanstack/react-store` to subscribe
8
+ * This hook uses `useSelector` from `@tanstack/react-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
  }