fix: keyboard shortcuts now work after labels load (lazy keymap evaluation)
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).
This commit is contained in:
parent
ba25ee47a5
commit
d82db402a3
3 changed files with 19 additions and 4 deletions
|
|
@ -89,4 +89,18 @@ describe('useLabelKeyboard', () => {
|
||||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: '1' }))
|
window.dispatchEvent(new KeyboardEvent('keydown', { key: '1' }))
|
||||||
expect(onLabel).not.toHaveBeenCalled()
|
expect(onLabel).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('evaluates labels getter on each keypress', () => {
|
||||||
|
const labelList: { name: string; key: string; emoji: string; color: string }[] = []
|
||||||
|
const onLabel = vi.fn()
|
||||||
|
const { cleanup } = useLabelKeyboard({ labels: () => labelList, onLabel, onSkip: vi.fn(), onDiscard: vi.fn(), onUndo: vi.fn(), onHelp: vi.fn() })
|
||||||
|
cleanups.push(cleanup)
|
||||||
|
// Before labels loaded — pressing '1' does nothing
|
||||||
|
window.dispatchEvent(new KeyboardEvent('keydown', { key: '1', bubbles: true }))
|
||||||
|
expect(onLabel).not.toHaveBeenCalled()
|
||||||
|
// Add a label (simulating async load)
|
||||||
|
labelList.push({ name: 'interview_scheduled', key: '1', emoji: '🗓️', color: '#4CAF50' })
|
||||||
|
window.dispatchEvent(new KeyboardEvent('keydown', { key: '1', bubbles: true }))
|
||||||
|
expect(onLabel).toHaveBeenCalledWith('interview_scheduled')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { onUnmounted, getCurrentInstance } from 'vue'
|
||||||
interface Label { name: string; key: string; emoji: string; color: string }
|
interface Label { name: string; key: string; emoji: string; color: string }
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
labels: Label[]
|
labels: Label[] | (() => Label[])
|
||||||
onLabel: (name: string) => void
|
onLabel: (name: string) => void
|
||||||
onSkip: () => void
|
onSkip: () => void
|
||||||
onDiscard: () => void
|
onDiscard: () => void
|
||||||
|
|
@ -12,12 +12,13 @@ interface Options {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLabelKeyboard(opts: Options) {
|
export function useLabelKeyboard(opts: Options) {
|
||||||
const keyMap = new Map(opts.labels.map(l => [l.key.toLowerCase(), l.name]))
|
|
||||||
|
|
||||||
function handler(e: KeyboardEvent) {
|
function handler(e: KeyboardEvent) {
|
||||||
if (e.target instanceof HTMLInputElement) return
|
if (e.target instanceof HTMLInputElement) return
|
||||||
if (e.target instanceof HTMLTextAreaElement) return
|
if (e.target instanceof HTMLTextAreaElement) return
|
||||||
const k = e.key.toLowerCase()
|
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 (keyMap.has(k)) { opts.onLabel(keyMap.get(k)!); return }
|
||||||
if (k === 'h') { opts.onLabel('hired'); return }
|
if (k === 'h') { opts.onLabel('hired'); return }
|
||||||
if (k === 's') { opts.onSkip(); return }
|
if (k === 's') { opts.onSkip(); return }
|
||||||
|
|
|
||||||
|
|
@ -254,7 +254,7 @@ async function handleUndo() {
|
||||||
}
|
}
|
||||||
|
|
||||||
useLabelKeyboard({
|
useLabelKeyboard({
|
||||||
labels: [], // will be updated after labels load — keyboard not active until queue loads
|
labels: () => labels.value, // getter — evaluated on each keypress
|
||||||
onLabel: handleLabel,
|
onLabel: handleLabel,
|
||||||
onSkip: handleSkip,
|
onSkip: handleSkip,
|
||||||
onDiscard: handleDiscard,
|
onDiscard: handleDiscard,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue