marmar-context / web / context / src / clinician / shell / keys.ts
keys.ts
Raw
import { useEffect, useLayoutEffect, useRef } from "react";
import { navigate } from "../router";
import { navItems } from "./nav";

const typing = (t: EventTarget | null) =>
  t instanceof HTMLElement && (t.isContentEditable || t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT");
const overlayOpen = () => !!document.querySelector("dialog[open], [role=dialog], [role=menu]");

/** ⌘B (Ctrl+B) folds the sidebar, ⌘K opens search, G then a letter jumps to a page. */
export function useShellKeys(actions: { toggle: () => void; search: () => void }) {
  const ref = useRef(actions);
  useLayoutEffect(() => {
    ref.current = actions;
  });
  useEffect(() => {
    let gAt = 0;
    const onKey = (e: KeyboardEvent) => {
      const key = e.key.toLowerCase();
      const mod = (e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey;
      if (mod && key === "b" && !(e.target instanceof HTMLElement && e.target.isContentEditable)) {
        e.preventDefault();
        ref.current.toggle();
        return;
      }
      if (mod && key === "k") {
        e.preventDefault();
        ref.current.search();
        return;
      }
      if (e.metaKey || e.ctrlKey || e.altKey || e.defaultPrevented || typing(e.target) || overlayOpen()) return;
      const after = Date.now() - gAt < 1200;
      gAt = key === "g" && !after ? Date.now() : 0;
      const item = after ? navItems.find((n) => n.key.toLowerCase() === key) : undefined;
      if (item) {
        e.preventDefault();
        navigate(item.to);
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);
}