Some keyboard events (dead keys, modifier-only presses) have e.key as undefined. Similarly, malformed shortcut definitions may lack a key. Added early-return guards to prevent TypeError. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
62 lines
1.9 KiB
TypeScript
62 lines
1.9 KiB
TypeScript
import { useEffect } from 'react';
|
|
|
|
export interface ShortcutDef {
|
|
/** Key to match (e.g. 'k', 'n', 'Escape', '/') */
|
|
key: string;
|
|
/** Require Cmd (Mac) or Ctrl (Windows/Linux) — default: false */
|
|
meta?: boolean;
|
|
/** Require Shift — default: false */
|
|
shift?: boolean;
|
|
/** Require Alt/Option — default: false */
|
|
alt?: boolean;
|
|
/** Handler to invoke when shortcut fires */
|
|
handler: () => void;
|
|
/** Allow firing when focus is in input/textarea — default: false */
|
|
allowInInput?: boolean;
|
|
/** Human-readable description for help overlay / accessibility */
|
|
description?: string;
|
|
}
|
|
|
|
function isInputElement(el: EventTarget | null): boolean {
|
|
if (!el || !(el instanceof HTMLElement)) {
|
|
return false;
|
|
}
|
|
const tag = el.tagName;
|
|
return tag === 'INPUT' || tag === 'TEXTAREA' || el.isContentEditable;
|
|
}
|
|
|
|
export function useKeyboardShortcuts(shortcuts: ShortcutDef[]): void {
|
|
useEffect(() => {
|
|
if (shortcuts.length === 0) {
|
|
return;
|
|
}
|
|
|
|
function handleKeyDown(e: KeyboardEvent) {
|
|
if (!e.key) return;
|
|
const inInput = isInputElement(e.target);
|
|
|
|
for (const shortcut of shortcuts) {
|
|
if (!shortcut.key) continue;
|
|
const metaMatch = shortcut.meta ? e.metaKey || e.ctrlKey : !(e.metaKey || e.ctrlKey);
|
|
const shiftMatch = shortcut.shift ? e.shiftKey : !e.shiftKey;
|
|
const altMatch = shortcut.alt ? e.altKey : !e.altKey;
|
|
|
|
// Normalize key comparison (case-insensitive for letters)
|
|
const keyMatch = e.key.toLowerCase() === shortcut.key.toLowerCase();
|
|
|
|
if (keyMatch && metaMatch && shiftMatch && altMatch) {
|
|
if (inInput && !shortcut.allowInInput) {
|
|
continue;
|
|
}
|
|
e.preventDefault();
|
|
shortcut.handler();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [shortcuts]);
|
|
}
|