Modifiers (shift, alt, meta) are now checked in both directions: when not required, the physical key must NOT be pressed either. Before: Cmd+K shortcut would fire on Cmd+Shift+K or Cmd+Alt+K. After: exact modifier combination is enforced. 4 regression tests added.
60 lines
1.8 KiB
TypeScript
60 lines
1.8 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) {
|
|
const inInput = isInputElement(e.target);
|
|
|
|
for (const shortcut of shortcuts) {
|
|
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]);
|
|
}
|