84 lines
2.1 KiB
TypeScript
84 lines
2.1 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|||
|
|
|
||
|
|
// Keyboard shortcuts (SPEC §9.4). Ignores keystrokes while typing in inputs,
|
||
|
|
// except Escape. Supports the `g i` chord (go to inbox).
|
||
|
|
export interface Shortcuts {
|
||
|
|
onSearch?: () => void;
|
||
|
|
onCompose?: () => void;
|
||
|
|
onReply?: () => void;
|
||
|
|
onReplyAll?: () => void;
|
||
|
|
onForward?: () => void;
|
||
|
|
onArchive?: () => void;
|
||
|
|
onGoInbox?: () => void;
|
||
|
|
onEscape?: () => void;
|
||
|
|
onNext?: () => void;
|
||
|
|
onPrev?: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
function isTyping(el: EventTarget | null): boolean {
|
||
|
|
if (!(el instanceof HTMLElement)) return false;
|
||
|
|
const tag = el.tagName;
|
||
|
|
return tag === "INPUT" || tag === "TEXTAREA" || el.isContentEditable;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useKeyboardShortcuts(shortcuts: Shortcuts): void {
|
||
|
|
const ref = useRef(shortcuts);
|
||
|
|
ref.current = shortcuts;
|
||
|
|
const pendingG = useRef(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const handler = (e: KeyboardEvent) => {
|
||
|
|
const s = ref.current;
|
||
|
|
if (e.key === "Escape") {
|
||
|
|
s.onEscape?.();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (isTyping(e.target)) return;
|
||
|
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||
|
|
|
||
|
|
if (pendingG.current) {
|
||
|
|
pendingG.current = false;
|
||
|
|
if (e.key === "i") {
|
||
|
|
e.preventDefault();
|
||
|
|
s.onGoInbox?.();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
switch (e.key) {
|
||
|
|
case "/":
|
||
|
|
e.preventDefault();
|
||
|
|
s.onSearch?.();
|
||
|
|
break;
|
||
|
|
case "c":
|
||
|
|
s.onCompose?.();
|
||
|
|
break;
|
||
|
|
case "r":
|
||
|
|
s.onReply?.();
|
||
|
|
break;
|
||
|
|
case "a":
|
||
|
|
s.onReplyAll?.();
|
||
|
|
break;
|
||
|
|
case "f":
|
||
|
|
s.onForward?.();
|
||
|
|
break;
|
||
|
|
case "e":
|
||
|
|
s.onArchive?.();
|
||
|
|
break;
|
||
|
|
case "j":
|
||
|
|
s.onNext?.();
|
||
|
|
break;
|
||
|
|
case "k":
|
||
|
|
s.onPrev?.();
|
||
|
|
break;
|
||
|
|
case "g":
|
||
|
|
pendingG.current = true;
|
||
|
|
window.setTimeout(() => (pendingG.current = false), 800);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
window.addEventListener("keydown", handler);
|
||
|
|
return () => window.removeEventListener("keydown", handler);
|
||
|
|
}, []);
|
||
|
|
}
|