- 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
47 lines
1.4 KiB
TypeScript
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);
|
|
}, []);
|
|
}
|