learning_ai_notes/web/src/lib/use-keyboard-shortcuts.ts
saravanakumardb1 420945e081 fix(web): stabilize useKeyboardShortcuts with ref-based callback
- Use useRef to hold shortcuts array, read from ref inside event handler
- Event listener registered once on mount (empty deps), avoids re-subscription
  when callers forget to memoize the shortcuts array
- Prevents subtle memory leak from rapid add/remove of keydown listeners
2026-03-10 19:54:50 -07:00

47 lines
1.4 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
export interface KeyboardShortcut {
key: string;
meta?: boolean;
shift?: boolean;
alt?: boolean;
handler: () => void;
description: string;
}
export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
const shortcutsRef = useRef(shortcuts);
shortcutsRef.current = shortcuts;
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
const target = event.target as HTMLElement;
const isInputFocused =
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.tagName === "SELECT" ||
target.isContentEditable;
for (const shortcut of shortcutsRef.current) {
const metaMatch = shortcut.meta ? event.metaKey || event.ctrlKey : !event.metaKey && !event.ctrlKey;
const shiftMatch = shortcut.shift ? event.shiftKey : !event.shiftKey;
const altMatch = shortcut.alt ? event.altKey : !event.altKey;
const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase();
if (keyMatch && metaMatch && shiftMatch && altMatch) {
if (shortcut.key === "Escape" || !isInputFocused) {
event.preventDefault();
shortcut.handler();
return;
}
}
}
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
}