useLabelKeyboard now accepts labels as Label[] | (() => Label[]). The keymap is rebuilt on every keypress from the getter result instead of being captured once at construction time — so keys 1–9 now fire correctly after the async /api/config/labels fetch completes. LabelView passes () => labels.value so the reactive ref is read lazily. New test: 'evaluates labels getter on each keypress' covers the async-load scenario (empty list → no match; push a label → key fires).
41 lines
1.5 KiB
TypeScript
41 lines
1.5 KiB
TypeScript
import { onUnmounted, getCurrentInstance } from 'vue'
|
|
|
|
interface Label { name: string; key: string; emoji: string; color: string }
|
|
|
|
interface Options {
|
|
labels: Label[] | (() => Label[])
|
|
onLabel: (name: string) => void
|
|
onSkip: () => void
|
|
onDiscard: () => void
|
|
onUndo: () => void
|
|
onHelp: () => void
|
|
}
|
|
|
|
export function useLabelKeyboard(opts: Options) {
|
|
function handler(e: KeyboardEvent) {
|
|
if (e.target instanceof HTMLInputElement) return
|
|
if (e.target instanceof HTMLTextAreaElement) return
|
|
const k = e.key.toLowerCase()
|
|
// Evaluate labels lazily so reactive updates work
|
|
const labelList = typeof opts.labels === 'function' ? opts.labels() : opts.labels
|
|
const keyMap = new Map(labelList.map(l => [l.key.toLowerCase(), l.name]))
|
|
if (keyMap.has(k)) { opts.onLabel(keyMap.get(k)!); return }
|
|
if (k === 'h') { opts.onLabel('hired'); return }
|
|
if (k === 's') { opts.onSkip(); return }
|
|
if (k === 'd') { opts.onDiscard(); return }
|
|
if (k === 'u') { opts.onUndo(); return }
|
|
if (k === '?') { opts.onHelp(); return }
|
|
}
|
|
|
|
// Add listener immediately (composable is called in setup, not in onMounted)
|
|
window.addEventListener('keydown', handler)
|
|
|
|
const cleanup = () => window.removeEventListener('keydown', handler)
|
|
|
|
// In component context: auto-cleanup on unmount
|
|
if (getCurrentInstance()) {
|
|
onUnmounted(cleanup)
|
|
}
|
|
|
|
return { cleanup }
|
|
}
|