learning_ai_common_plat/packages/use-keyboard-shortcuts/src/use-keyboard-shortcuts.ts
saravanakumardb1 6f4957d821 fix(use-keyboard-shortcuts): enforce symmetric modifier matching
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.
2026-03-29 12:45:32 -07:00

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]);
}