MicProtocol / apps /web /src /hooks /useKeyboardShortcuts.ts
Benaah's picture
Upload folder using huggingface_hub
fb12607 verified
Raw History Blame Contribute Delete
3.56 kB
"use client";
import { useEffect, useCallback, useRef, useMemo } from "react";
export interface KeyboardShortcut {
key: string;
description: string;
action: () => void;
category: string;
meta?: boolean; // Cmd/Ctrl
shift?: boolean;
alt?: boolean;
ctrl?: boolean;
// When to disable this shortcut (e.g., when typing in input)
disabledWhen?: () => boolean;
}
export interface ShortcutCategory {
name: string;
shortcuts: KeyboardShortcut[];
}
interface UseKeyboardShortcutsOptions {
shortcuts: KeyboardShortcut[];
enabled?: boolean;
}
export function useKeyboardShortcuts({ shortcuts, enabled = true }: UseKeyboardShortcutsOptions) {
const shortcutsRef = useRef(shortcuts);
useEffect(() => {
shortcutsRef.current = shortcuts;
}, [shortcuts]);
const isInputFocused = useCallback(() => {
const active = document.activeElement;
if (!active) return false;
const tag = active.tagName.toLowerCase();
return (
tag === "input" ||
tag === "textarea" ||
tag === "select" ||
(active as HTMLElement).isContentEditable ||
(active as HTMLElement).closest('[data-radix-popper-content-wrapper], [role="dialog"], [role="menu"]') !== null
);
}, []);
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (!enabled) return;
// Allow shortcuts with meta/ctrl even in inputs for some cases
const inInput = isInputFocused();
for (const shortcut of shortcutsRef.current) {
const { key, meta, shift, alt, ctrl, disabledWhen } = shortcut;
// Check if shortcut should be disabled in current context
if (disabledWhen && disabledWhen()) continue;
// Don't trigger shortcuts when typing in inputs (unless explicitly allowed)
if (inInput && !(meta || ctrl)) continue;
const keyMatches = event.key.toLowerCase() === key.toLowerCase();
const metaMatches = !!meta === (event.metaKey || event.ctrlKey);
const shiftMatches = !!shift === event.shiftKey;
const altMatches = !!alt === event.altKey;
const ctrlMatches = !!ctrl === (event.ctrlKey || event.metaKey);
if (keyMatches && metaMatches && shiftMatches && altMatches && ctrlMatches) {
event.preventDefault();
event.stopPropagation();
shortcut.action();
break;
}
}
},
[enabled, isInputFocused]
);
useEffect(() => {
if (!enabled) return;
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [enabled, handleKeyDown]);
// Return categorized shortcuts for help modal
const categorized = useMemo(() => {
const categories: Record<string, KeyboardShortcut[]> = {};
for (const shortcut of shortcuts) {
if (!categories[shortcut.category]) {
categories[shortcut.category] = [];
}
categories[shortcut.category].push(shortcut);
}
return Object.entries(categories).map(([name, shortcuts]) => ({ name, shortcuts }));
}, [shortcuts]);
return { categorized };
}
export function formatShortcut(shortcut: KeyboardShortcut): string {
const parts: string[] = [];
if (shortcut.meta) parts.push(navigator.platform.includes("Mac") ? "⌘" : "Ctrl");
if (shortcut.ctrl && !shortcut.meta) parts.push("Ctrl");
if (shortcut.shift) parts.push("Shift");
if (shortcut.alt) parts.push(navigator.platform.includes("Mac") ? "⌥" : "Alt");
parts.push(shortcut.key.toUpperCase());
return parts.join(" + ");
}