Skip to the content

Capsid menu

components/shortcuts/shortcuts.react.tsx

132 lines and 20 functions in DrDustinEdwards/capsomer, at commit d3966cc on 2026-10-24. This file on GitHub.

Functions

The functions of shortcuts.react.tsx
FunctionLinesComplexityCoveredCRAPMutation
useShortcut7 to 252No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:13>13 to 243No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
run19 to 191No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
when20 to 202No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
Keys28 to 452No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:32>32 to 422No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:35>35 to 402No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
ShortcutSheet54 to 1412No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:61>61 to 641No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:62>62 to 621No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:63>63 to 631No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:66>66 to 711No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
sync67 to 671No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:70>70 to 701No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:73>73 to 752No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:78>78 to 812No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:94>94 to 1002No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:106>106 to 1231No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:110>110 to 1202No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:128>128 to 1281No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet

Imports

It imports components/dialog/dialog.react.tsx, components/shortcuts/shortcuts.ts. Imported by components/command-menu/command-menu.react.tsx.

Source

import { Fragment, useCallback, useEffect, useId, useRef, useState } from "react";
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "../dialog/dialog.react.tsx";
import { SINGLE_KEYS_NOTE, grouped, keyCaps, modifiedShortcutFor, register, setSheetOpener, setSingleKeys, singleKeysOn, type Shortcut } from "./shortcuts.ts";

// Registers a shortcut while the component is mounted. The latest `run` is always the one
// called, so passing a new function each render does not re-register.
export function useShortcut(key: string | string[], run: (e: KeyboardEvent) => void, opts: { label?: string; about?: string; group?: string; when?: () => boolean } = {}): void {
  const latest = useRef(run);
  latest.current = run;
  const when = useRef(opts.when);
  when.current = opts.when;
  const keyId = Array.isArray(key) ? key.join("|") : key;
  useEffect(() => {
    const s: Shortcut = {
      key,
      label: opts.label ?? keyId,
      about: opts.about,
      group: opts.group ?? "Other",
      run: (e) => latest.current(e),
      when: () => (when.current ? when.current() : true),
    };
    return register(s);
    // The key, label and group identify the registration; run and when are read live.
  }, [keyId, opts.label, opts.about, opts.group]);
}

// A shortcut's key caps, "or" between alternatives (the command menu shows them too).
export function Keys({ spec }: { spec: string | string[] }) {
  const specs = Array.isArray(spec) ? spec : [spec];
  return (
    <>
      {specs.map((s, i) => (
        <Fragment key={s}>
          {i > 0 ? " or " : null}
          {keyCaps(s).map((cap, j) => (
            <Fragment key={`${cap}-${j}`}>
              {j > 0 ? " " : null}
              <kbd>{cap}</kbd>
            </Fragment>
          ))}
        </Fragment>
      ))}
    </>
  );
}

export interface ShortcutSheetProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

// The "?" sheet: every registered shortcut by group, and the switch for single keys. While
// mounted it is the sheet "?" opens.
export function ShortcutSheet({ open, onOpenChange }: ShortcutSheetProps) {
  const id = useId();
  const dlg = useRef<HTMLDialogElement | null>(null);
  const closeButton = useRef<HTMLElement | null>(null);
  const [single, setSingle] = useState(true);
  const [groups, setGroups] = useState<ReturnType<typeof grouped>>([]);

  useEffect(() => {
    setSheetOpener(() => onOpenChange(true));
    return () => setSheetOpener(null);
  }, [onOpenChange]);

  useEffect(() => {
    const sync = () => setSingle(singleKeysOn());
    sync();
    document.addEventListener("cap-single-keys", sync);
    return () => document.removeEventListener("cap-single-keys", sync);
  }, []);

  useEffect(() => {
    if (open) setGroups(grouped());
  }, [open]);

  // Focus starts on Close: the dialog's own Close button, found once the markup is there.
  const setRef = useCallback((el: HTMLDialogElement | null) => {
    dlg.current = el;
    closeButton.current = el?.querySelector<HTMLElement>("[data-cap-part='close']") ?? null;
  }, []);

  return (
    <Dialog
      ref={setRef}
      className="cap-keys"
      open={open}
      onOpenChange={onOpenChange}
      placement="center"
      size="md"
      initialFocus={closeButton}
      aria-labelledby={`${id}-title`}
      // A shortcut with Ctrl or Cmd still works over the sheet: the sheet closes, then it runs.
      onKeyDown={(e) => {
        const hit = modifiedShortcutFor(e.nativeEvent);
        if (!hit) return;
        e.preventDefault();
        dlg.current?.close();
        hit.run(e.nativeEvent);
      }}
    >
      <DialogHeader divider>
        <DialogTitle id={`${id}-title`}>Keyboard shortcuts</DialogTitle>
      </DialogHeader>
      <DialogBody className="cap-keys-body" data-cap-part="keys-list">
        {groups.map((g) => (
          <div className="cap-keys-group" key={g.group}>
            <h3 className="cap-keys-group-title">{g.group}</h3>
            <dl className="cap-keys-list">
              {g.items.map((s, i) => (
                <div key={`${s.label}-${i}`}>
                  <dt>
                    <Keys spec={s.key} />
                  </dt>
                  <dd>
                    {s.label}
                    {s.about ? <span className="cap-keys-about">{s.about}</span> : null}
                  </dd>
                </div>
              ))}
            </dl>
          </div>
        ))}
      </DialogBody>
      <DialogFooter align="start">
        <div className="cap-keys-foot">
          <label className="cap-switch">
            <input type="checkbox" role="switch" data-cap-part="single-keys" aria-describedby={`${id}-note`} checked={single} onChange={(e) => setSingleKeys(e.target.checked)} />
            <span>Single-key shortcuts</span>
            <span className="cap-switch-state" aria-hidden="true">
              {single ? "On" : "Off"}
            </span>
          </label>
          <p className="cap-keys-note" id={`${id}-note`}>
            {SINGLE_KEYS_NOTE}
          </p>
        </div>
      </DialogFooter>
    </Dialog>
  );
}