@tanstack/vue-hotkeys 0.0.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.
Files changed (64) hide show
  1. package/README.md +122 -0
  2. package/dist/HotkeysProvider.cjs +31 -0
  3. package/dist/HotkeysProvider.cjs.map +1 -0
  4. package/dist/HotkeysProvider.d.cts +34 -0
  5. package/dist/HotkeysProvider.d.ts +34 -0
  6. package/dist/HotkeysProvider.js +31 -0
  7. package/dist/HotkeysProvider.js.map +1 -0
  8. package/dist/HotkeysProviderContext.cjs +20 -0
  9. package/dist/HotkeysProviderContext.cjs.map +1 -0
  10. package/dist/HotkeysProviderContext.d.cts +19 -0
  11. package/dist/HotkeysProviderContext.d.ts +19 -0
  12. package/dist/HotkeysProviderContext.js +18 -0
  13. package/dist/HotkeysProviderContext.js.map +1 -0
  14. package/dist/index.cjs +27 -0
  15. package/dist/index.d.cts +10 -0
  16. package/dist/index.d.ts +10 -0
  17. package/dist/index.js +12 -0
  18. package/dist/useHeldKeyCodes.cjs +37 -0
  19. package/dist/useHeldKeyCodes.cjs.map +1 -0
  20. package/dist/useHeldKeyCodes.d.cts +33 -0
  21. package/dist/useHeldKeyCodes.d.ts +33 -0
  22. package/dist/useHeldKeyCodes.js +37 -0
  23. package/dist/useHeldKeyCodes.js.map +1 -0
  24. package/dist/useHeldKeys.cjs +35 -0
  25. package/dist/useHeldKeys.cjs.map +1 -0
  26. package/dist/useHeldKeys.d.cts +31 -0
  27. package/dist/useHeldKeys.d.ts +31 -0
  28. package/dist/useHeldKeys.js +35 -0
  29. package/dist/useHeldKeys.js.map +1 -0
  30. package/dist/useHotkey.cjs +124 -0
  31. package/dist/useHotkey.cjs.map +1 -0
  32. package/dist/useHotkey.d.cts +86 -0
  33. package/dist/useHotkey.d.ts +86 -0
  34. package/dist/useHotkey.js +124 -0
  35. package/dist/useHotkey.js.map +1 -0
  36. package/dist/useHotkeyRecorder.cjs +80 -0
  37. package/dist/useHotkeyRecorder.cjs.map +1 -0
  38. package/dist/useHotkeyRecorder.d.cts +61 -0
  39. package/dist/useHotkeyRecorder.d.ts +61 -0
  40. package/dist/useHotkeyRecorder.js +80 -0
  41. package/dist/useHotkeyRecorder.js.map +1 -0
  42. package/dist/useHotkeySequence.cjs +95 -0
  43. package/dist/useHotkeySequence.cjs.map +1 -0
  44. package/dist/useHotkeySequence.d.cts +58 -0
  45. package/dist/useHotkeySequence.d.ts +58 -0
  46. package/dist/useHotkeySequence.js +95 -0
  47. package/dist/useHotkeySequence.js.map +1 -0
  48. package/dist/useKeyHold.cjs +60 -0
  49. package/dist/useKeyHold.cjs.map +1 -0
  50. package/dist/useKeyHold.d.cts +52 -0
  51. package/dist/useKeyHold.d.ts +52 -0
  52. package/dist/useKeyHold.js +60 -0
  53. package/dist/useKeyHold.js.map +1 -0
  54. package/package.json +66 -0
  55. package/src/HotkeysProvider.tsx +29 -0
  56. package/src/HotkeysProviderContext.ts +37 -0
  57. package/src/index.ts +14 -0
  58. package/src/useHeldKeyCodes.ts +34 -0
  59. package/src/useHeldKeys.ts +32 -0
  60. package/src/useHotkey.ts +197 -0
  61. package/src/useHotkeyRecorder.ts +112 -0
  62. package/src/useHotkeySequence.ts +158 -0
  63. package/src/useKeyHold.ts +65 -0
  64. package/src/vue.d.ts +6 -0
@@ -0,0 +1,52 @@
1
+ import { HeldKey } from "@tanstack/hotkeys";
2
+ import { MaybeRefOrGetter, Ref } from "vue";
3
+
4
+ //#region src/useKeyHold.d.ts
5
+ /**
6
+ * Vue composable that returns a reactive ref indicating whether a specific key is currently being held.
7
+ *
8
+ * This composable uses `useStore` from `@tanstack/vue-store` to subscribe
9
+ * to the global KeyStateTracker and uses a selector to determine if
10
+ * the specified key is held.
11
+ *
12
+ * @param key - The key to check (e.g., 'Shift', 'Control', 'A')
13
+ * @returns Reactive ref that is true if the key is currently held down
14
+ *
15
+ * @example
16
+ * ```vue
17
+ * <script setup>
18
+ * import { useKeyHold } from '@tanstack/vue-hotkeys'
19
+ *
20
+ * const isShiftHeld = useKeyHold('Shift')
21
+ * </script>
22
+ *
23
+ * <template>
24
+ * <div :style="{ opacity: isShiftHeld ? 1 : 0.5 }">
25
+ * {{ isShiftHeld ? 'Shift is pressed!' : 'Press Shift' }}
26
+ * </div>
27
+ * </template>
28
+ * ```
29
+ *
30
+ * @example
31
+ * ```vue
32
+ * <script setup>
33
+ * import { useKeyHold } from '@tanstack/vue-hotkeys'
34
+ *
35
+ * const ctrl = useKeyHold('Control')
36
+ * const shift = useKeyHold('Shift')
37
+ * const alt = useKeyHold('Alt')
38
+ * </script>
39
+ *
40
+ * <template>
41
+ * <div>
42
+ * <span :style="{ opacity: ctrl ? 1 : 0.3 }">Ctrl</span>
43
+ * <span :style="{ opacity: shift ? 1 : 0.3 }">Shift</span>
44
+ * <span :style="{ opacity: alt ? 1 : 0.3 }">Alt</span>
45
+ * </div>
46
+ * </template>
47
+ * ```
48
+ */
49
+ declare function useKeyHold(key: MaybeRefOrGetter<HeldKey>): Ref<boolean>;
50
+ //#endregion
51
+ export { useKeyHold };
52
+ //# sourceMappingURL=useKeyHold.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { getKeyStateTracker } from "@tanstack/hotkeys";
2
+ import { unref } from "vue";
3
+ import { useStore } from "@tanstack/vue-store";
4
+
5
+ //#region src/useKeyHold.ts
6
+ /**
7
+ * Vue composable that returns a reactive ref indicating whether a specific key is currently being held.
8
+ *
9
+ * This composable uses `useStore` from `@tanstack/vue-store` to subscribe
10
+ * to the global KeyStateTracker and uses a selector to determine if
11
+ * the specified key is held.
12
+ *
13
+ * @param key - The key to check (e.g., 'Shift', 'Control', 'A')
14
+ * @returns Reactive ref that is true if the key is currently held down
15
+ *
16
+ * @example
17
+ * ```vue
18
+ * <script setup>
19
+ * import { useKeyHold } from '@tanstack/vue-hotkeys'
20
+ *
21
+ * const isShiftHeld = useKeyHold('Shift')
22
+ * <\/script>
23
+ *
24
+ * <template>
25
+ * <div :style="{ opacity: isShiftHeld ? 1 : 0.5 }">
26
+ * {{ isShiftHeld ? 'Shift is pressed!' : 'Press Shift' }}
27
+ * </div>
28
+ * </template>
29
+ * ```
30
+ *
31
+ * @example
32
+ * ```vue
33
+ * <script setup>
34
+ * import { useKeyHold } from '@tanstack/vue-hotkeys'
35
+ *
36
+ * const ctrl = useKeyHold('Control')
37
+ * const shift = useKeyHold('Shift')
38
+ * const alt = useKeyHold('Alt')
39
+ * <\/script>
40
+ *
41
+ * <template>
42
+ * <div>
43
+ * <span :style="{ opacity: ctrl ? 1 : 0.3 }">Ctrl</span>
44
+ * <span :style="{ opacity: shift ? 1 : 0.3 }">Shift</span>
45
+ * <span :style="{ opacity: alt ? 1 : 0.3 }">Alt</span>
46
+ * </div>
47
+ * </template>
48
+ * ```
49
+ */
50
+ function useKeyHold(key) {
51
+ return useStore(getKeyStateTracker().store, (state) => {
52
+ const keyValue = unref(key);
53
+ const normalizedKey = (typeof keyValue === "string" ? keyValue : String(keyValue)).toLowerCase();
54
+ return state.heldKeys.some((heldKey) => heldKey.toLowerCase() === normalizedKey);
55
+ });
56
+ }
57
+
58
+ //#endregion
59
+ export { useKeyHold };
60
+ //# sourceMappingURL=useKeyHold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useKeyHold.js","names":[],"sources":["../src/useKeyHold.ts"],"sourcesContent":["import { useStore } from '@tanstack/vue-store'\nimport { getKeyStateTracker } from '@tanstack/hotkeys'\nimport { unref } from 'vue'\nimport type { MaybeRefOrGetter, Ref } from 'vue'\nimport type { HeldKey } from '@tanstack/hotkeys'\n\n/**\n * Vue composable that returns a reactive ref indicating whether a specific key is currently being held.\n *\n * This composable uses `useStore` from `@tanstack/vue-store` to subscribe\n * to the global KeyStateTracker and uses a selector to determine if\n * the specified key is held.\n *\n * @param key - The key to check (e.g., 'Shift', 'Control', 'A')\n * @returns Reactive ref that is true if the key is currently held down\n *\n * @example\n * ```vue\n * <script setup>\n * import { useKeyHold } from '@tanstack/vue-hotkeys'\n *\n * const isShiftHeld = useKeyHold('Shift')\n * </script>\n *\n * <template>\n * <div :style=\"{ opacity: isShiftHeld ? 1 : 0.5 }\">\n * {{ isShiftHeld ? 'Shift is pressed!' : 'Press Shift' }}\n * </div>\n * </template>\n * ```\n *\n * @example\n * ```vue\n * <script setup>\n * import { useKeyHold } from '@tanstack/vue-hotkeys'\n *\n * const ctrl = useKeyHold('Control')\n * const shift = useKeyHold('Shift')\n * const alt = useKeyHold('Alt')\n * </script>\n *\n * <template>\n * <div>\n * <span :style=\"{ opacity: ctrl ? 1 : 0.3 }\">Ctrl</span>\n * <span :style=\"{ opacity: shift ? 1 : 0.3 }\">Shift</span>\n * <span :style=\"{ opacity: alt ? 1 : 0.3 }\">Alt</span>\n * </div>\n * </template>\n * ```\n */\nexport function useKeyHold(key: MaybeRefOrGetter<HeldKey>): Ref<boolean> {\n const tracker = getKeyStateTracker()\n\n const isHeld = useStore(tracker.store, (state) => {\n const keyValue = unref(key)\n const normalizedKey = (\n typeof keyValue === 'string' ? keyValue : String(keyValue)\n ).toLowerCase()\n return state.heldKeys.some(\n (heldKey) => heldKey.toLowerCase() === normalizedKey,\n )\n })\n\n return isHeld as Ref<boolean>\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,SAAgB,WAAW,KAA8C;AAavE,QAVe,SAFC,oBAAoB,CAEJ,QAAQ,UAAU;EAChD,MAAM,WAAW,MAAM,IAAI;EAC3B,MAAM,iBACJ,OAAO,aAAa,WAAW,WAAW,OAAO,SAAS,EAC1D,aAAa;AACf,SAAO,MAAM,SAAS,MACnB,YAAY,QAAQ,aAAa,KAAK,cACxC;GACD"}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@tanstack/vue-hotkeys",
3
+ "version": "0.0.1",
4
+ "description": "Vue adapter for TanStack Hotkeys",
5
+ "author": "Tanner Linsley",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/TanStack/hotkeys.git",
10
+ "directory": "packages/vue-hotkeys"
11
+ },
12
+ "homepage": "https://tanstack.com/hotkeys",
13
+ "funding": {
14
+ "type": "github",
15
+ "url": "https://github.com/sponsors/tannerlinsley"
16
+ },
17
+ "keywords": [
18
+ "vue",
19
+ "vuejs",
20
+ "tanstack",
21
+ "hotkeys",
22
+ "keyboard",
23
+ "shortcuts"
24
+ ],
25
+ "scripts": {
26
+ "clean": "premove ./build ./dist",
27
+ "lint": "eslint ./src",
28
+ "lint:fix": "eslint ./src --fix",
29
+ "test:eslint": "eslint ./src",
30
+ "test:lib": "vitest --passWithNoTests",
31
+ "test:lib:dev": "pnpm test:lib --watch",
32
+ "test:types": "tsc",
33
+ "build": "tsdown"
34
+ },
35
+ "type": "module",
36
+ "main": "./dist/index.cjs",
37
+ "module": "./dist/index.js",
38
+ "types": "./dist/index.d.cts",
39
+ "exports": {
40
+ ".": {
41
+ "import": "./dist/index.js",
42
+ "require": "./dist/index.cjs"
43
+ },
44
+ "./package.json": "./package.json"
45
+ },
46
+ "sideEffects": false,
47
+ "engines": {
48
+ "node": ">=18"
49
+ },
50
+ "files": [
51
+ "dist",
52
+ "src"
53
+ ],
54
+ "dependencies": {
55
+ "@tanstack/hotkeys": "workspace:*",
56
+ "@tanstack/vue-store": "^0.9.2"
57
+ },
58
+ "devDependencies": {
59
+ "@vitejs/plugin-vue": "^6.0.4",
60
+ "@vue/test-utils": "^2.4.6",
61
+ "vue": "^3.5.29"
62
+ },
63
+ "peerDependencies": {
64
+ "vue": ">=3.0.0"
65
+ }
66
+ }
@@ -0,0 +1,29 @@
1
+ import { defineComponent } from 'vue'
2
+ import { provideHotkeysContext } from './HotkeysProviderContext'
3
+ import type { HotkeysProviderOptions } from './HotkeysProviderContext'
4
+
5
+ /**
6
+ * Vue component that provides default options for hotkeys context.
7
+ *
8
+ * @example
9
+ * ```vue
10
+ * <template>
11
+ * <HotkeysProvider :defaultOptions="{ hotkey: { enabled: true } }">
12
+ * <App />
13
+ * </HotkeysProvider>
14
+ * </template>
15
+ * ```
16
+ */
17
+ export const HotkeysProvider = defineComponent({
18
+ name: 'HotkeysProvider',
19
+ props: {
20
+ defaultOptions: {
21
+ type: Object as () => HotkeysProviderOptions,
22
+ default: undefined,
23
+ },
24
+ },
25
+ setup(props, { slots }) {
26
+ provideHotkeysContext(props.defaultOptions)
27
+ return () => slots.default?.()
28
+ },
29
+ })
@@ -0,0 +1,37 @@
1
+ import { inject, provide } from 'vue'
2
+ import type { InjectionKey } from 'vue'
3
+ import type { HotkeyRecorderOptions } from '@tanstack/hotkeys'
4
+ import type { UseHotkeyOptions } from './useHotkey'
5
+ import type { UseHotkeySequenceOptions } from './useHotkeySequence'
6
+
7
+ export interface HotkeysProviderOptions {
8
+ hotkey?: Partial<UseHotkeyOptions>
9
+ hotkeyRecorder?: Partial<HotkeyRecorderOptions>
10
+ hotkeySequence?: Partial<UseHotkeySequenceOptions>
11
+ }
12
+
13
+ interface HotkeysContextValue {
14
+ defaultOptions: HotkeysProviderOptions
15
+ }
16
+
17
+ const HotkeysContext: InjectionKey<HotkeysContextValue> =
18
+ Symbol('HotkeysContext')
19
+
20
+ const DEFAULT_OPTIONS: HotkeysProviderOptions = {}
21
+
22
+ export function provideHotkeysContext(defaultOptions?: HotkeysProviderOptions) {
23
+ const contextValue: HotkeysContextValue = {
24
+ defaultOptions: defaultOptions ?? DEFAULT_OPTIONS,
25
+ }
26
+
27
+ provide(HotkeysContext, contextValue)
28
+ }
29
+
30
+ export function useHotkeysContext() {
31
+ return inject(HotkeysContext, null)
32
+ }
33
+
34
+ export function useDefaultHotkeysOptions() {
35
+ const context = inject(HotkeysContext, null)
36
+ return context?.defaultOptions ?? {}
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ // Re-export everything from the core package
2
+ export * from '@tanstack/hotkeys'
3
+
4
+ // Provider
5
+ export * from './HotkeysProvider'
6
+ export * from './HotkeysProviderContext'
7
+
8
+ // Vue-specific composables
9
+ export * from './useHotkey'
10
+ export * from './useHeldKeys'
11
+ export * from './useHeldKeyCodes'
12
+ export * from './useKeyHold'
13
+ export * from './useHotkeySequence'
14
+ export * from './useHotkeyRecorder'
@@ -0,0 +1,34 @@
1
+ import { useStore } from '@tanstack/vue-store'
2
+ import { getKeyStateTracker } from '@tanstack/hotkeys'
3
+ import type { Ref } from 'vue'
4
+
5
+ /**
6
+ * Vue composable that returns a reactive ref mapping currently held key names to their physical `event.code` values.
7
+ *
8
+ * This is useful for debugging which physical key was pressed (e.g. distinguishing
9
+ * left vs right Shift via "ShiftLeft" / "ShiftRight").
10
+ *
11
+ * @returns Reactive ref containing record mapping normalized key names to their `event.code` values
12
+ *
13
+ * @example
14
+ * ```vue
15
+ * <script setup>
16
+ * import { useHeldKeys, useHeldKeyCodes } from '@tanstack/vue-hotkeys'
17
+ *
18
+ * const heldKeys = useHeldKeys()
19
+ * const heldCodes = useHeldKeyCodes()
20
+ * </script>
21
+ *
22
+ * <template>
23
+ * <div>
24
+ * <kbd v-for="key in heldKeys" :key="key">
25
+ * {{ key }} <small>{{ heldCodes[key] }}</small>
26
+ * </kbd>
27
+ * </div>
28
+ * </template>
29
+ * ```
30
+ */
31
+ export function useHeldKeyCodes(): Ref<Record<string, string>> {
32
+ const tracker = getKeyStateTracker()
33
+ return useStore(tracker.store, (state) => state.heldCodes)
34
+ }
@@ -0,0 +1,32 @@
1
+ import { useStore } from '@tanstack/vue-store'
2
+ import { getKeyStateTracker } from '@tanstack/hotkeys'
3
+ import type { Ref } from 'vue'
4
+
5
+ /**
6
+ * Vue composable that returns a reactive ref of currently held keyboard keys.
7
+ *
8
+ * This composable uses `useStore` from `@tanstack/vue-store` to subscribe
9
+ * to the global KeyStateTracker and updates whenever keys are pressed
10
+ * or released.
11
+ *
12
+ * @returns Reactive ref containing array of currently held key names
13
+ *
14
+ * @example
15
+ * ```vue
16
+ * <script setup>
17
+ * import { useHeldKeys } from '@tanstack/vue-hotkeys'
18
+ *
19
+ * const heldKeys = useHeldKeys()
20
+ * </script>
21
+ *
22
+ * <template>
23
+ * <div>
24
+ * Currently pressed: {{ heldKeys.join(' + ') || 'None' }}
25
+ * </div>
26
+ * </template>
27
+ * ```
28
+ */
29
+ export function useHeldKeys(): Ref<Array<string>> {
30
+ const tracker = getKeyStateTracker()
31
+ return useStore(tracker.store, (state) => state.heldKeys)
32
+ }
@@ -0,0 +1,197 @@
1
+ import { onUnmounted, unref, watch } from 'vue'
2
+ import {
3
+ detectPlatform,
4
+ formatHotkey,
5
+ getHotkeyManager,
6
+ rawHotkeyToParsedHotkey,
7
+ } from '@tanstack/hotkeys'
8
+ import { useDefaultHotkeysOptions } from './HotkeysProviderContext'
9
+ import type {
10
+ Hotkey,
11
+ HotkeyCallback,
12
+ HotkeyOptions,
13
+ HotkeyRegistrationHandle,
14
+ RegisterableHotkey,
15
+ } from '@tanstack/hotkeys'
16
+ import type { MaybeRefOrGetter } from 'vue'
17
+
18
+ export interface UseHotkeyOptions extends Omit<
19
+ HotkeyOptions,
20
+ 'enabled' | 'target'
21
+ > {
22
+ /**
23
+ * Whether the hotkey is active.
24
+ * Can be a Ref, a getter function, or a boolean value.
25
+ * Defaults to true.
26
+ */
27
+ enabled?: MaybeRefOrGetter<boolean>
28
+ /**
29
+ * The DOM element to attach the event listener to.
30
+ * Can be a Ref, a getter function, direct DOM element, or null.
31
+ * Defaults to document.
32
+ */
33
+ target?: MaybeRefOrGetter<HTMLElement | Document | Window | null>
34
+ }
35
+
36
+ /**
37
+ * Vue composable for registering a keyboard hotkey.
38
+ *
39
+ * Uses the singleton HotkeyManager for efficient event handling.
40
+ * The callback receives both the keyboard event and a context object
41
+ * containing the hotkey string and parsed hotkey.
42
+ *
43
+ * This composable automatically tracks reactive dependencies and updates
44
+ * the registration when options or the callback change.
45
+ *
46
+ * @param hotkey - The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)
47
+ * @param callback - The function to call when the hotkey is pressed
48
+ * @param options - Options for the hotkey behavior
49
+ *
50
+ * @example
51
+ * ```vue
52
+ * <script setup>
53
+ * import { ref } from 'vue'
54
+ * import { useHotkey } from '@tanstack/vue-hotkeys'
55
+ *
56
+ * const count = ref(0)
57
+ *
58
+ * // Callback always has access to latest count value
59
+ * useHotkey('Mod+S', (event, { hotkey }) => {
60
+ * console.log(`Save triggered, count is ${count.value}`)
61
+ * handleSave()
62
+ * })
63
+ * </script>
64
+ * ```
65
+ *
66
+ * @example
67
+ * ```vue
68
+ * <script setup>
69
+ * import { ref } from 'vue'
70
+ * import { useHotkey } from '@tanstack/vue-hotkeys'
71
+ *
72
+ * const isOpen = ref(false)
73
+ *
74
+ * // enabled option is reactive
75
+ * useHotkey('Escape', () => {
76
+ * isOpen.value = false
77
+ * }, { enabled: isOpen })
78
+ * </script>
79
+ * ```
80
+ *
81
+ * @example
82
+ * ```vue
83
+ * <script setup>
84
+ * import { ref } from 'vue'
85
+ * import { useHotkey } from '@tanstack/vue-hotkeys'
86
+ *
87
+ * const editorRef = ref<HTMLDivElement | null>(null)
88
+ *
89
+ * // Scoped to a specific element
90
+ * useHotkey('Mod+S', () => {
91
+ * save()
92
+ * }, { target: editorRef })
93
+ * </script>
94
+ *
95
+ * <template>
96
+ * <div ref="editorRef">...</div>
97
+ * </template>
98
+ * ```
99
+ */
100
+ export function useHotkey(
101
+ hotkey: MaybeRefOrGetter<RegisterableHotkey>,
102
+ callback: HotkeyCallback,
103
+ options: MaybeRefOrGetter<UseHotkeyOptions> = {},
104
+ ): void {
105
+ const defaultOptions = useDefaultHotkeysOptions()
106
+ const manager = getHotkeyManager()
107
+
108
+ let registration: HotkeyRegistrationHandle | null = null
109
+
110
+ // Watch for changes to reactive dependencies
111
+ const stopWatcher = watch(
112
+ () => {
113
+ const resolvedHotkey = resolveMaybeRefOrGetter(hotkey)
114
+ const resolvedOptions = resolveMaybeRefOrGetter(options)
115
+ const mergedOptions = {
116
+ ...defaultOptions.hotkey,
117
+ ...resolvedOptions,
118
+ } as UseHotkeyOptions
119
+ const resolvedEnabled =
120
+ mergedOptions.enabled === undefined
121
+ ? undefined
122
+ : resolveMaybeRefOrGetter(mergedOptions.enabled)
123
+ const resolvedTarget =
124
+ mergedOptions.target === undefined
125
+ ? undefined
126
+ : resolveMaybeRefOrGetter(mergedOptions.target)
127
+
128
+ return {
129
+ resolvedHotkey,
130
+ mergedOptions,
131
+ resolvedEnabled,
132
+ resolvedTarget,
133
+ }
134
+ },
135
+ ({ resolvedHotkey, mergedOptions, resolvedEnabled, resolvedTarget }) => {
136
+ // Normalize to hotkey string
137
+ const platform = mergedOptions.platform ?? detectPlatform()
138
+ const hotkeyString: Hotkey =
139
+ typeof resolvedHotkey === 'string'
140
+ ? resolvedHotkey
141
+ : (formatHotkey(
142
+ rawHotkeyToParsedHotkey(resolvedHotkey as any, platform),
143
+ ) as Hotkey)
144
+
145
+ // Resolve target
146
+ const finalTarget =
147
+ resolvedTarget ?? (typeof document !== 'undefined' ? document : null)
148
+
149
+ if (!finalTarget) {
150
+ return
151
+ }
152
+
153
+ // Unregister previous registration if it exists
154
+ if (registration?.isActive) {
155
+ registration.unregister()
156
+ registration = null
157
+ }
158
+
159
+ // Extract options without target (target is handled separately)
160
+ const {
161
+ target: _target,
162
+ enabled: _enabled,
163
+ ...restOptions
164
+ } = mergedOptions
165
+ const optionsWithoutTarget = {
166
+ ...restOptions,
167
+ ...(resolvedEnabled === undefined ? {} : { enabled: resolvedEnabled }),
168
+ }
169
+
170
+ // Register the hotkey
171
+ registration = manager.register(hotkeyString, callback, {
172
+ ...optionsWithoutTarget,
173
+ target: finalTarget,
174
+ })
175
+
176
+ // Update callback and options
177
+ if (registration.isActive) {
178
+ registration.callback = callback
179
+ registration.setOptions(optionsWithoutTarget)
180
+ }
181
+ },
182
+ { immediate: true },
183
+ )
184
+
185
+ // Cleanup on unmount
186
+ onUnmounted(() => {
187
+ stopWatcher()
188
+ if (registration?.isActive) {
189
+ registration.unregister()
190
+ registration = null
191
+ }
192
+ })
193
+ }
194
+
195
+ function resolveMaybeRefOrGetter<T>(value: MaybeRefOrGetter<T>): T {
196
+ return typeof value === 'function' ? (value as () => T)() : unref(value)
197
+ }
@@ -0,0 +1,112 @@
1
+ import { onUnmounted, unref, watch } from 'vue'
2
+ import { useStore } from '@tanstack/vue-store'
3
+ import { HotkeyRecorder } from '@tanstack/hotkeys'
4
+ import { useDefaultHotkeysOptions } from './HotkeysProviderContext'
5
+ import type { MaybeRefOrGetter, Ref } from 'vue'
6
+ import type { Hotkey, HotkeyRecorderOptions } from '@tanstack/hotkeys'
7
+
8
+ export interface VueHotkeyRecorder {
9
+ /** Whether recording is currently active */
10
+ isRecording: Ref<boolean>
11
+ /** The currently recorded hotkey (for live preview) */
12
+ recordedHotkey: Ref<Hotkey | null>
13
+ /** Start recording a new hotkey */
14
+ startRecording: () => void
15
+ /** Stop recording (same as cancel) */
16
+ stopRecording: () => void
17
+ /** Cancel recording without saving */
18
+ cancelRecording: () => void
19
+ }
20
+
21
+ /**
22
+ * Vue composable for recording keyboard shortcuts.
23
+ *
24
+ * This composable provides a thin wrapper around the framework-agnostic `HotkeyRecorder`
25
+ * class, managing all the complexity of capturing keyboard events, converting them
26
+ * to hotkey strings, and handling edge cases like Escape to cancel or Backspace/Delete
27
+ * to clear.
28
+ *
29
+ * @param options - Configuration options for the recorder
30
+ * @returns An object with recording state and control functions
31
+ *
32
+ * @example
33
+ * ```vue
34
+ * <script setup>
35
+ * import { ref } from 'vue'
36
+ * import { useHotkeyRecorder } from '@tanstack/vue-hotkeys'
37
+ *
38
+ * const shortcut = ref<Hotkey>('Mod+S')
39
+ *
40
+ * const recorder = useHotkeyRecorder({
41
+ * onRecord: (hotkey) => {
42
+ * shortcut.value = hotkey
43
+ * },
44
+ * onCancel: () => {
45
+ * console.log('Recording cancelled')
46
+ * },
47
+ * })
48
+ * </script>
49
+ *
50
+ * <template>
51
+ * <div>
52
+ * <button @click="recorder.startRecording()">
53
+ * {{ recorder.isRecording ? 'Recording...' : 'Edit Shortcut' }}
54
+ * </button>
55
+ * <div v-if="recorder.recordedHotkey">
56
+ * Recording: {{ recorder.recordedHotkey }}
57
+ * </div>
58
+ * </div>
59
+ * </template>
60
+ * ```
61
+ */
62
+ export function useHotkeyRecorder(
63
+ options: MaybeRefOrGetter<HotkeyRecorderOptions>,
64
+ ): VueHotkeyRecorder {
65
+ const defaultOptions = useDefaultHotkeysOptions()
66
+ const recorder = new HotkeyRecorder(
67
+ resolveRecorderOptions(options, defaultOptions),
68
+ )
69
+
70
+ // Subscribe to recorder state using useStore
71
+ const isRecording = useStore(recorder.store, (state) => state.isRecording)
72
+ const recordedHotkey = useStore(
73
+ recorder.store,
74
+ (state) => state.recordedHotkey,
75
+ )
76
+
77
+ const stopWatcher = watch(
78
+ () => resolveRecorderOptions(options, defaultOptions),
79
+ (resolvedOptions) => {
80
+ recorder.setOptions(resolvedOptions)
81
+ },
82
+ { immediate: true },
83
+ )
84
+
85
+ // Cleanup on unmount
86
+ onUnmounted(() => {
87
+ stopWatcher()
88
+ recorder.destroy()
89
+ })
90
+
91
+ return {
92
+ isRecording: isRecording as Ref<boolean>,
93
+ recordedHotkey: recordedHotkey as Ref<Hotkey | null>,
94
+ startRecording: () => recorder.start(),
95
+ stopRecording: () => recorder.stop(),
96
+ cancelRecording: () => recorder.cancel(),
97
+ }
98
+ }
99
+
100
+ function resolveRecorderOptions(
101
+ options: MaybeRefOrGetter<HotkeyRecorderOptions>,
102
+ defaultOptions: ReturnType<typeof useDefaultHotkeysOptions>,
103
+ ): HotkeyRecorderOptions {
104
+ return {
105
+ ...defaultOptions.hotkeyRecorder,
106
+ ...resolveMaybeRefOrGetter(options),
107
+ } as HotkeyRecorderOptions
108
+ }
109
+
110
+ function resolveMaybeRefOrGetter<T>(value: MaybeRefOrGetter<T>): T {
111
+ return typeof value === 'function' ? (value as () => T)() : unref(value)
112
+ }