Vue Example: UseHotkeyRecorder

vue
<script setup lang="ts">
import { TanStackDevtools } from '@tanstack/vue-devtools'
import {
  HotkeysProvider,
  formatForDisplay,
  useHotkey,
  useHotkeyRecorder,
  type Hotkey,
} from '@tanstack/vue-hotkeys'
import { HotkeysDevtoolsPanel } from '@tanstack/vue-hotkeys-devtools'
import { ref } from 'vue'
import ShortcutListItem from './ShortcutListItem.vue'

interface ShortcutActions {
  [key: string]: {
    name: string
    defaultHotkey: Hotkey
  }
}

const DEFAULT_SHORTCUT_ACTIONS: ShortcutActions = {
  save: {
    name: 'Save',
    defaultHotkey: 'Mod+K',
  },
  open: {
    name: 'Open',
    defaultHotkey: 'Mod+E',
  },
  new: {
    name: 'New',
    defaultHotkey: 'Mod+G',
  },
  close: {
    name: 'Close',
    defaultHotkey: 'Mod+Shift+K',
  },
  undo: {
    name: 'Undo',
    defaultHotkey: 'Mod+Shift+E',
  },
  redo: {
    name: 'Redo',
    defaultHotkey: 'Mod+Shift+G',
  },
}

const shortcuts = ref<Record<string, Hotkey | ''>>(
  Object.fromEntries(
    Object.entries(DEFAULT_SHORTCUT_ACTIONS).map(([id, action]) => [
      id,
      action.defaultHotkey,
    ]),
  ) as Record<string, Hotkey | ''>,
)

const saveCount = ref(0)
const openCount = ref(0)
const newCount = ref(0)
const closeCount = ref(0)
const undoCount = ref(0)
const redoCount = ref(0)
const recordingActionId = ref<string | null>(null)
const plugins = [{ name: 'TanStack Hotkeys', component: HotkeysDevtoolsPanel }]

const recorder = useHotkeyRecorder({
  onRecord: (hotkey: Hotkey) => {
    if (recordingActionId.value) {
      shortcuts.value = {
        ...shortcuts.value,
        [recordingActionId.value]: hotkey || ('' as Hotkey | ''),
      }
      recordingActionId.value = null
    }
  },
  onCancel: () => {
    recordingActionId.value = null
  },
  onClear: () => {
    if (recordingActionId.value) {
      shortcuts.value = {
        ...shortcuts.value,
        [recordingActionId.value]: '' as Hotkey | '',
      }
      recordingActionId.value = null
    }
  },
})

useHotkey(
  () =>
    (shortcuts.value.save ||
      DEFAULT_SHORTCUT_ACTIONS.save.defaultHotkey) as Hotkey,
  () => {
    console.log('Save triggered:', shortcuts.value.save)
    saveCount.value++
  },
  {
    enabled: () => !recorder.isRecording.value && shortcuts.value.save !== '',
  },
)

useHotkey(
  () =>
    (shortcuts.value.open ||
      DEFAULT_SHORTCUT_ACTIONS.open.defaultHotkey) as Hotkey,
  () => {
    console.log('Open triggered:', shortcuts.value.open)
    openCount.value++
  },
  {
    enabled: () => !recorder.isRecording.value && shortcuts.value.open !== '',
  },
)

useHotkey(
  () =>
    (shortcuts.value.new ||
      DEFAULT_SHORTCUT_ACTIONS.new.defaultHotkey) as Hotkey,
  () => {
    console.log('New triggered:', shortcuts.value.new)
    newCount.value++
  },
  {
    enabled: () => !recorder.isRecording.value && shortcuts.value.new !== '',
  },
)

useHotkey(
  () =>
    (shortcuts.value.close ||
      DEFAULT_SHORTCUT_ACTIONS.close.defaultHotkey) as Hotkey,
  () => {
    console.log('Close triggered:', shortcuts.value.close)
    closeCount.value++
  },
  {
    enabled: () => !recorder.isRecording.value && shortcuts.value.close !== '',
  },
)

useHotkey(
  () =>
    (shortcuts.value.undo ||
      DEFAULT_SHORTCUT_ACTIONS.undo.defaultHotkey) as Hotkey,
  () => {
    console.log('Undo triggered:', shortcuts.value.undo)
    undoCount.value++
  },
  {
    enabled: () => !recorder.isRecording.value && shortcuts.value.undo !== '',
  },
)

useHotkey(
  () =>
    (shortcuts.value.redo ||
      DEFAULT_SHORTCUT_ACTIONS.redo.defaultHotkey) as Hotkey,
  () => {
    console.log('Redo triggered:', shortcuts.value.redo)
    redoCount.value++
  },
  {
    enabled: () => !recorder.isRecording.value && shortcuts.value.redo !== '',
  },
)

const handleEdit = (actionId: string) => {
  recordingActionId.value = actionId
  recorder.startRecording()
}

const handleCancel = () => {
  recorder.cancelRecording()
  recordingActionId.value = null
}

const usageCode = `import { useHotkey, formatForDisplay } from '@tanstack/vue-hotkeys'

function App() {
  const [shortcuts, setShortcuts] = useState({
    save: 'Mod+K',
    open: 'Mod+E',
  })

  // Register shortcuts dynamically
  useHotkey(
    shortcuts.save,
    () => handleSave(),
    { enabled: !isRecording }
  )

  return (
    <div>
      <kbd>{formatForDisplay(shortcuts.save)}</kbd>
    </div>
  )
}`
</script>

<template>
  <HotkeysProvider>
    <div class="app">
      <header>
        <h1>Keyboard Shortcuts Settings</h1>
        <p>
          Customize your keyboard shortcuts. Click "Edit" to record a new
          shortcut, or press Escape to cancel.
        </p>
      </header>

      <main>
        <section class="demo-section">
          <h2>Shortcuts</h2>
          <div class="shortcuts-list">
            <ShortcutListItem
              v-for="(action, actionId) in DEFAULT_SHORTCUT_ACTIONS"
              :key="actionId"
              :action-name="action.name"
              :hotkey="shortcuts[actionId] || ''"
              :is-recording="
                recorder.isRecording && recordingActionId === actionId
              "
              @edit="handleEdit(actionId)"
              @cancel="handleCancel"
            />
          </div>
        </section>

        <section class="demo-section">
          <h2>Demo Actions</h2>
          <p>Try your shortcuts! Actions will trigger when you press them.</p>
          <div class="demo-stats">
            <div class="stat-item">
              <div class="stat-label">Save</div>
              <div class="stat-value">{{ saveCount }}</div>
              <kbd>{{ formatForDisplay(shortcuts.save || 'Mod+K') }}</kbd>
            </div>
            <div class="stat-item">
              <div class="stat-label">Open</div>
              <div class="stat-value">{{ openCount }}</div>
              <kbd>{{ formatForDisplay(shortcuts.open || 'Mod+E') }}</kbd>
            </div>
            <div class="stat-item">
              <div class="stat-label">New</div>
              <div class="stat-value">{{ newCount }}</div>
              <kbd>{{ formatForDisplay(shortcuts.new || 'Mod+G') }}</kbd>
            </div>
            <div class="stat-item">
              <div class="stat-label">Close</div>
              <div class="stat-value">{{ closeCount }}</div>
              <kbd>{{
                formatForDisplay(shortcuts.close || 'Mod+Shift+K')
              }}</kbd>
            </div>
            <div class="stat-item">
              <div class="stat-label">Undo</div>
              <div class="stat-value">{{ undoCount }}</div>
              <kbd>{{ formatForDisplay(shortcuts.undo || 'Mod+Shift+E') }}</kbd>
            </div>
            <div class="stat-item">
              <div class="stat-label">Redo</div>
              <div class="stat-value">{{ redoCount }}</div>
              <kbd>{{ formatForDisplay(shortcuts.redo || 'Mod+Shift+G') }}</kbd>
            </div>
          </div>
        </section>

        <div v-if="recorder.isRecording" class="info-box recording-notice">
          <strong>Recording shortcut...</strong> Press any key combination or
          Escape to cancel. Press Backspace/Delete to clear the shortcut.
        </div>

        <section class="demo-section">
          <h2>Usage</h2>
          <pre class="code-block">{{ usageCode }}</pre>
        </section>
      </main>

      <TanStackDevtools :plugins="plugins" />
    </div>
  </HotkeysProvider>
</template>