import React from 'react'
import ReactDOM from 'react-dom/client'
import {
formatForDisplay,
useHotkey,
useHeldKeys,
useHotkeyRecorder,
type Hotkey,
} from '@tanstack/react-hotkeys'
import { HotkeysProvider } from '@tanstack/react-hotkeys'
import { hotkeysDevtoolsPlugin } from '@tanstack/react-hotkeys-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
import './index.css'
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',
},
}
function App() {
// State for shortcuts
const [shortcuts, setShortcuts] = React.useState<Record<string, Hotkey | ''>>(
() => {
const defaults: Record<string, Hotkey> = {}
for (const [id, action] of Object.entries(DEFAULT_SHORTCUT_ACTIONS)) {
defaults[id] = action.defaultHotkey
}
return defaults
},
)
// Simple counters for each action
const [saveCount, setSaveCount] = React.useState(0)
const [openCount, setOpenCount] = React.useState(0)
const [newCount, setNewCount] = React.useState(0)
const [closeCount, setCloseCount] = React.useState(0)
const [undoCount, setUndoCount] = React.useState(0)
const [redoCount, setRedoCount] = React.useState(0)
// Track which action is being recorded
const [recordingActionId, setRecordingActionId] = React.useState<
string | null
>(null)
// Use the library's useHotkeyRecorder hook
const recorder = useHotkeyRecorder({
onRecord: (hotkey: Hotkey) => {
if (recordingActionId) {
setShortcuts((prev) => ({
...prev,
[recordingActionId]: hotkey || ('' as Hotkey | ''),
}))
setRecordingActionId(null)
}
},
onCancel: () => {
setRecordingActionId(null)
},
onClear: () => {
if (recordingActionId) {
setShortcuts((prev) => ({
...prev,
[recordingActionId]: '' as Hotkey | '',
}))
setRecordingActionId(null)
}
},
})
// Register shortcuts using useHotkey
const isRecording = recorder.isRecording
// Get actual hotkey values (use defaults if empty)
const saveHotkey: Hotkey =
shortcuts.save || DEFAULT_SHORTCUT_ACTIONS.save.defaultHotkey
const openHotkey: Hotkey =
shortcuts.open || DEFAULT_SHORTCUT_ACTIONS.open.defaultHotkey
const newHotkey: Hotkey =
shortcuts.new || DEFAULT_SHORTCUT_ACTIONS.new.defaultHotkey
const closeHotkey: Hotkey =
shortcuts.close || DEFAULT_SHORTCUT_ACTIONS.close.defaultHotkey
const undoHotkey: Hotkey =
shortcuts.undo || DEFAULT_SHORTCUT_ACTIONS.undo.defaultHotkey
const redoHotkey: Hotkey =
shortcuts.redo || DEFAULT_SHORTCUT_ACTIONS.redo.defaultHotkey
// Register each shortcut - only enable if shortcut is actually set (not empty)
useHotkey(
saveHotkey,
() => {
console.log('Save triggered:', saveHotkey)
setSaveCount((c) => c + 1)
},
{
enabled: !isRecording && shortcuts.save !== '',
},
)
useHotkey(
openHotkey,
() => {
console.log('Open triggered:', openHotkey)
setOpenCount((c) => c + 1)
},
{
enabled: !isRecording && shortcuts.open !== '',
},
)
useHotkey(
newHotkey,
() => {
console.log('New triggered:', newHotkey)
setNewCount((c) => c + 1)
},
{
enabled: !isRecording && shortcuts.new !== '',
},
)
useHotkey(
closeHotkey,
() => {
console.log('Close triggered:', closeHotkey)
setCloseCount((c) => c + 1)
},
{
enabled: !isRecording && shortcuts.close !== '',
},
)
useHotkey(
undoHotkey,
() => {
console.log('Undo triggered:', undoHotkey)
setUndoCount((c) => c + 1)
},
{
enabled: !isRecording && shortcuts.undo !== '',
},
)
useHotkey(
redoHotkey,
() => {
console.log('Redo triggered:', redoHotkey)
setRedoCount((c) => c + 1)
},
{
enabled: !isRecording && shortcuts.redo !== '',
},
)
const handleEdit = (actionId: string) => {
setRecordingActionId(actionId)
recorder.startRecording()
}
const handleCancel = () => {
recorder.cancelRecording()
setRecordingActionId(null)
}
return (
<div className="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 className="demo-section">
<h2>Shortcuts</h2>
<div className="shortcuts-list">
{Object.entries(DEFAULT_SHORTCUT_ACTIONS).map(
([actionId, action]) => (
<ShortcutListItem
key={actionId}
actionName={action.name}
hotkey={shortcuts[actionId] || ''}
isRecording={
recorder.isRecording && recordingActionId === actionId
}
onEdit={() => handleEdit(actionId)}
onCancel={handleCancel}
/>
),
)}
</div>
</section>
<section className="demo-section">
<h2>Demo Actions</h2>
<p>Try your shortcuts! Actions will trigger when you press them.</p>
<div className="demo-stats">
<div className="stat-item">
<div className="stat-label">Save</div>
<div className="stat-value">{saveCount}</div>
<kbd>{formatForDisplay(shortcuts.save || 'Mod+K')}</kbd>
</div>
<div className="stat-item">
<div className="stat-label">Open</div>
<div className="stat-value">{openCount}</div>
<kbd>{formatForDisplay(shortcuts.open || 'Mod+E')}</kbd>
</div>
<div className="stat-item">
<div className="stat-label">New</div>
<div className="stat-value">{newCount}</div>
<kbd>{formatForDisplay(shortcuts.new || 'Mod+G')}</kbd>
</div>
<div className="stat-item">
<div className="stat-label">Close</div>
<div className="stat-value">{closeCount}</div>
<kbd>{formatForDisplay(shortcuts.close || 'Mod+Shift+K')}</kbd>
</div>
<div className="stat-item">
<div className="stat-label">Undo</div>
<div className="stat-value">{undoCount}</div>
<kbd>{formatForDisplay(shortcuts.undo || 'Mod+Shift+E')}</kbd>
</div>
<div className="stat-item">
<div className="stat-label">Redo</div>
<div className="stat-value">{redoCount}</div>
<kbd>{formatForDisplay(shortcuts.redo || 'Mod+Shift+G')}</kbd>
</div>
</div>
</section>
{recorder.isRecording && (
<div className="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 className="demo-section">
<h2>Usage</h2>
<pre className="code-block">{`import { useHotkey, formatForDisplay } from '@tanstack/react-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>
)
}`}</pre>
</section>
</main>
<TanStackDevtools plugins={[hotkeysDevtoolsPlugin()]} />
</div>
)
}
interface ShortcutListItemProps {
actionName: string
hotkey: string
isRecording: boolean
onEdit: () => void
onCancel: () => void
}
function ShortcutListItem({
actionName,
hotkey,
isRecording,
onEdit,
onCancel,
}: ShortcutListItemProps) {
const heldKeys = useHeldKeys()
return (
<div className={`shortcut-item ${isRecording ? 'recording' : ''}`}>
<div className="shortcut-item-content">
<div className="shortcut-action">{actionName}</div>
<div className="shortcut-hotkey">
{isRecording ? (
<div className="recording-indicator">
{heldKeys.length > 0 ? (
<div className="held-hotkeys">
{heldKeys.map((key, index) => (
<React.Fragment key={key}>
{index > 0 && <span className="plus">+</span>}
<kbd>{key}</kbd>
</React.Fragment>
))}
</div>
) : (
<span className="recording-text">
Press any key combination...
</span>
)}
</div>
) : hotkey ? (
<kbd>{formatForDisplay(hotkey as Hotkey)}</kbd>
) : (
<span className="no-shortcut">No shortcut</span>
)}
</div>
</div>
<div className="shortcut-actions">
{isRecording ? (
<button onClick={onCancel} className="cancel-button">
Cancel
</button>
) : (
<button onClick={onEdit} className="edit-button">
Edit
</button>
)}
</div>
</div>
)
}
ReactDOM.createRoot(document.getElementById('root')!).render(
// optionally, provide default options to an optional HotkeysProvider
<HotkeysProvider
// defaultOptions={{
// hotkey: {
// preventDefault: true,
// },
// }}
>
<App />
</HotkeysProvider>,
)