Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/all-poets-write.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/vue-hotkeys': minor
'@tanstack/vue-hotkeys-devtools': minor
---

- Initial Vue adapter release
13 changes: 13 additions & 0 deletions examples/vue/useHeldKeys/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @ts-check

import rootConfig from '../../../eslint.config.js'

/** @type {import('eslint').Linter.Config[]} */
const config = [
...rootConfig,
{
files: ['**/*.{ts,tsx,vue}'],
},
]

export default config
12 changes: 12 additions & 0 deletions examples/vue/useHeldKeys/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>useHeldKeys - TanStack Hotkeys Vue Example</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/index.ts"></script>
</body>
</html>
19 changes: 19 additions & 0 deletions examples/vue/useHeldKeys/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@tanstack/hotkeys-example-vue-use-held-keys",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port=3076",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/vue-hotkeys": "^0.3.0",
"vue": "^3.5.14"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"typescript": "5.9.3",
"vite": "^7.3.1"
}
}
39 changes: 39 additions & 0 deletions examples/vue/useHeldKeys/src/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { formatKeyForDebuggingDisplay, useHeldKeyCodes, useHeldKeys } from '@tanstack/vue-hotkeys'
const heldKeys = useHeldKeys()
const heldCodes = useHeldKeyCodes()
</script>

<template>
<div style="padding: 2rem; font-family: sans-serif; max-width: 800px; margin: 0 auto;">
<h1>useHeldKeys</h1>
<p>Returns an array of all currently pressed keys.</p>

<div style="margin: 2rem 0; padding: 2rem; background: #f5f5f5; border-radius: 8px; min-height: 100px; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
<template v-if="heldKeys.length > 0">
<kbd
v-for="(key, index) in heldKeys"
:key="key"
style="background: #333; color: white; padding: 0.5rem 1rem; border-radius: 4px; font-family: monospace;"
>
{{ formatKeyForDebuggingDisplay(key) }}
<small v-if="heldCodes[key]" style="opacity: 0.6; font-size: 0.8em;">
{{ formatKeyForDebuggingDisplay(heldCodes[key], { source: 'code' }) }}
</small>
</kbd>
<span v-if="index < heldKeys.length - 1">+</span>
</template>
<span v-else style="opacity: 0.5;">Press any keys...</span>
</div>

<div>Keys held: <strong>{{ heldKeys.length }}</strong></div>

<h2 style="margin-top: 2rem;">Usage</h2>
<pre style="background: #f5f5f5; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>import { useHeldKeys } from '@tanstack/vue-hotkeys'

const heldKeys = useHeldKeys()

// heldKeys is a reactive ref containing array of key names</code></pre>
</div>
</template>
4 changes: 4 additions & 0 deletions examples/vue/useHeldKeys/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
8 changes: 8 additions & 0 deletions examples/vue/useHeldKeys/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"jsx": "preserve"
},
"include": ["src"],
"exclude": ["eslint.config.js"]
}
6 changes: 6 additions & 0 deletions examples/vue/useHeldKeys/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
plugins: [vue()],
})
13 changes: 13 additions & 0 deletions examples/vue/useHotkey/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @ts-check

import rootConfig from '../../../eslint.config.js'

/** @type {import('eslint').Linter.Config[]} */
const config = [
...rootConfig,
{
files: ['**/*.{ts,tsx,vue}'],
},
]

export default config
14 changes: 14 additions & 0 deletions examples/vue/useHotkey/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>useHotkey - TanStack Hotkeys Vue Example</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="app"></div>
<script type="module" src="/src/index.ts"></script>
</body>
</html>
23 changes: 23 additions & 0 deletions examples/vue/useHotkey/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@tanstack/hotkeys-example-vue-use-hotkey",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port=3075",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test:types": "tsc"
},
"dependencies": {
"@tanstack/vue-hotkeys": "^0.3.0",
"vue": "^3.5.14"
},
"devDependencies": {
"@tanstack/vue-hotkeys-devtools": "^0.3.0",
"@vitejs/plugin-vue": "^5.2.3",
"typescript": "5.9.3",
"vite": "^7.3.1"
}
}
212 changes: 212 additions & 0 deletions examples/vue/useHotkey/src/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<script setup lang="ts">
import { ref } from 'vue'
import { formatForDisplay, useHotkey } from '@tanstack/vue-hotkeys'
import type {Hotkey} from '@tanstack/vue-hotkeys';

const lastHotkey = ref<Hotkey | null>(null)
const saveCount = ref(0)
const incrementCount = ref(0)
const enabled = ref(true)
const activeTab = ref(1)

// Scoped shortcuts state
const modalOpen = ref(false)
const editorContent = ref('')
const sidebarShortcutCount = ref(0)
const modalShortcutCount = ref(0)
const editorShortcutCount = ref(0)

// Refs for scoped shortcuts
const sidebarRef = ref<HTMLDivElement | null>(null)
const modalRef = ref<HTMLDivElement | null>(null)
const editorRef = ref<HTMLTextAreaElement | null>(null)

// ============================================================================
// Basic Hotkeys
// ============================================================================

// Basic hotkey with callback context
useHotkey('Mod+S', (_event, { hotkey, parsedHotkey }) => {
lastHotkey.value = hotkey
saveCount.value++
console.log('Hotkey triggered:', hotkey)
console.log('Parsed hotkey:', parsedHotkey)
})

// requireReset prevents repeated triggering while holding keys
useHotkey(
'Mod+K',
(_event, { hotkey }) => {
lastHotkey.value = hotkey
incrementCount.value++
},
{ requireReset: true },
)

// Conditional hotkey (enabled/disabled)
useHotkey(
'Mod+E',
(_event, { hotkey }) => {
lastHotkey.value = hotkey
alert('This hotkey can be toggled!')
},
{ enabled: enabled.value },
)

// ============================================================================
// Number Key Combinations (Tab/Section Switching)
// ============================================================================

useHotkey('Mod+1', () => {
lastHotkey.value = 'Mod+1'
activeTab.value = 1
})

useHotkey('Mod+2', () => {
lastHotkey.value = 'Mod+2'
activeTab.value = 2
})

useHotkey('Mod+3', () => {
lastHotkey.value = 'Mod+3'
activeTab.value = 3
})

// ============================================================================
// Scoped Hotkeys
// ============================================================================

// Sidebar-scoped hotkey
useHotkey(
'Mod+B',
() => {
sidebarShortcutCount.value++
},
{ target: sidebarRef },
)

// Modal-scoped hotkey
useHotkey(
'Escape',
() => {
modalShortcutCount.value++
modalOpen.value = false
},
{ target: modalRef, enabled: modalOpen },
)

// Editor-scoped hotkey
useHotkey(
'Mod+/',
(event) => {
event.preventDefault()
editorShortcutCount.value++
const start = editorRef.value?.selectionStart || 0
const end = editorRef.value?.selectionEnd || 0
const text = editorContent.value
const beforeSelection = text.substring(0, start)
const selection = text.substring(start, end)
const afterSelection = text.substring(end)

editorContent.value = beforeSelection + '// ' + selection + afterSelection
},
{ target: editorRef },
)
</script>

<template>
<div class="app">
<header>
<h1>useHotkey</h1>
<p>
Register keyboard shortcuts with the useHotkey composable. Supports scoped
shortcuts, conditional enabling, and cross-platform Mod key.
</p>
</header>

<main>
<section class="demo-section">
<h2>Last Hotkey Pressed</h2>
<div class="hotkey-display">
{{ lastHotkey ? formatForDisplay(lastHotkey) : 'None yet' }}
</div>
</section>

<section class="demo-section">
<h2>Basic Hotkeys</h2>
<div class="hotkey-list">
<div class="hotkey-item">
<kbd>{{ formatForDisplay('Mod+S') }}</kbd>
<span>Save (triggered {{ saveCount }} times)</span>
</div>
<div class="hotkey-item">
<kbd>{{ formatForDisplay('Mod+K') }}</kbd>
<span>Increment (triggered {{ incrementCount }} times)</span>
</div>
<div class="hotkey-item">
<kbd>{{ formatForDisplay('Mod+E') }}</kbd>
<span>Alert ({{ enabled ? 'enabled' : 'disabled' }})</span>
<button @click="enabled = !enabled">
{{ enabled ? 'Disable' : 'Enable' }}
</button>
</div>
</div>
</section>

<section class="demo-section">
<h2>Tab Switching</h2>
<div class="tabs">
<button
v-for="tab in [1, 2, 3]"
:key="tab"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
>
Tab {{ tab }} <kbd>{{ formatForDisplay(`Mod+${tab}`) }}</kbd>
</button>
</div>
<div class="tab-content">
<div v-if="activeTab === 1">Content for Tab 1</div>
<div v-else-if="activeTab === 2">Content for Tab 2</div>
<div v-else-if="activeTab === 3">Content for Tab 3</div>
</div>
</section>

<section class="demo-section">
<h2>Scoped Hotkeys</h2>

<div class="scoped-demo">
<div ref="sidebarRef" tabindex="0" class="scoped-box sidebar">
<h3>Sidebar (focus me)</h3>
<p>Press <kbd>{{ formatForDisplay('Mod+B') }}</kbd> while focused</p>
<p>Triggered {{ sidebarShortcutCount }} times</p>
</div>

<div class="scoped-box">
<h3>Modal</h3>
<button @click="modalOpen = true">Open Modal</button>
<div v-if="modalOpen" class="modal-overlay">
<div ref="modalRef" tabindex="0" class="modal">
<p>Press <kbd>Escape</kbd> to close</p>
<p>Triggered {{ modalShortcutCount }} times</p>
<button @click="modalOpen = false">Close</button>
</div>
</div>
</div>
</div>

<div class="editor-demo">
<h3>Code Editor</h3>
<p>Focus and press <kbd>{{ formatForDisplay('Mod+/') }}</kbd> to comment</p>
<textarea
ref="editorRef"
v-model="editorContent"
class="code-editor"
placeholder="Type some code here..."
></textarea>
<p class="editor-stats">Comment shortcut used {{ editorShortcutCount }} times</p>
</div>
</section>
</main>
</div>
</template>
Loading