Clawscale

Hotkeys

HotkeysTarget binds keyboard shortcuts to part of your UI. Local hotkeys fire while that part has focus. Global hotkeys fire anywhere on the page.

Usage#

import { HotkeysTarget, KeyComboTag } from "@clawscale/react";
<HotkeysTarget hotkeys={hotkeys}>
  {({ handleKeyDown, handleKeyUp }) => (
    <div tabIndex={0} onKeyDown={handleKeyDown} onKeyUp={handleKeyUp}>
      <PipelineList />
    </div>
  )}
</HotkeysTarget>

A HotkeysProvider above them collects hotkeys for the dialog. ClawscaleProvider includes one. In function components, the useHotkeys hook does the same job without a wrapper. HotkeysTarget2 is a deprecated alias of HotkeysTarget.

Memoize the hotkeys array, for example with useMemo. A new array on every render registers the hotkeys again.

Examples#

Local hotkeys#

A local hotkey needs a group, which titles it in the hotkeys dialog. Bind handleKeyDown and handleKeyUp to an element with a tabIndex so it can take focus. Click the list, then press J or K.

ingest-orders
enrich-customers
export-ledger
train-forecast
import { Card, CardList, HotkeysTarget } from "@clawscale/react";
import { useMemo, useState } from "react";

const PIPELINES = ["ingest-orders", "enrich-customers", "export-ledger", "train-forecast"];

export default function HotkeysLocal() {
  const [index, setIndex] = useState(0);
  const hotkeys = useMemo(
    () => [
      {
        combo: "j",
        group: "Pipeline list",
        label: "Select next pipeline",
        onKeyDown: () => setIndex((i) => Math.min(i + 1, PIPELINES.length - 1)),
      },
      {
        combo: "k",
        group: "Pipeline list",
        label: "Select previous pipeline",
        onKeyDown: () => setIndex((i) => Math.max(i - 1, 0)),
      },
    ],
    [],
  );
  return (
    <HotkeysTarget hotkeys={hotkeys}>
      {({ handleKeyDown, handleKeyUp }) => (
        <CardList compact tabIndex={0} onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} style={{ maxWidth: 320 }}>
          {PIPELINES.map((name, i) => (
            <Card role="listitem" key={name} selected={i === index}>
              {name}
            </Card>
          ))}
        </CardList>
      )}
    </HotkeysTarget>
  );
}

Global hotkeys#

global: true fires a hotkey wherever focus is. When every hotkey is global, pass an element instead of a render function. No hotkey fires in a text input unless it sets allowInInput.

PressPanywhere on the page.Ingestion running
import { HotkeysTarget, KeyComboTag, Tag } from "@clawscale/react";
import { useMemo, useState } from "react";

export default function HotkeysGlobal() {
  const [paused, setPaused] = useState(false);
  const hotkeys = useMemo(
    () => [{ combo: "p", global: true, label: "Pause or resume ingestion", onKeyDown: () => setPaused((p) => !p) }],
    [],
  );
  return (
    <HotkeysTarget hotkeys={hotkeys}>
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
        <span>Press</span>
        <KeyComboTag combo="p" />
        <span>anywhere on the page.</span>
        <Tag minimal intent={paused ? "warning" : "success"}>
          {paused ? "Ingestion paused" : "Ingestion running"}
        </Tag>
      </div>
    </HotkeysTarget>
  );
}

Key combos#

KeyComboTag renders a combo string such as mod+s. mod shows as cmd on macOS and ctrl elsewhere, and macOS shows modifier keys as icons. On the server it renders PC keys, then switches to the visitor's platform after hydration. minimal drops the key caps.

ActionDefaultMinimal
Save view
Refresh all panels
Move row up
Clear selection
import { HTMLTable, KeyComboTag } from "@clawscale/react";
import { useEffect, useState } from "react";

const SHORTCUTS = [
  { combo: "mod+s", label: "Save view" },
  { combo: "shift+r", label: "Refresh all panels" },
  { combo: "alt+up", label: "Move row up" },
  { combo: "esc", label: "Clear selection" },
];

export default function HotkeysKeyCombo() {
  // KeyComboTag output depends on the platform, so render it after mount.
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  return (
    <HTMLTable compact>
      <thead>
        <tr>
          <th>Action</th>
          <th>Default</th>
          <th>Minimal</th>
        </tr>
      </thead>
      <tbody>
        {SHORTCUTS.map(({ combo, label }) => (
          <tr key={combo}>
            <td>{label}</td>
            <td>{mounted && <KeyComboTag combo={combo} />}</td>
            <td>{mounted && <KeyComboTag combo={combo} minimal />}</td>
          </tr>
        ))}
      </tbody>
    </HTMLTable>
  );
}

Hotkeys dialog#

Press ? on this page to open the hotkeys dialog. It lists every registered hotkey by group, global hotkeys first. Set showDialogKeyCombo in options to change the combo, or pass false to turn it off.

Props interface#

interface HotkeysTargetProps@clawscale/react
PropsDescription
childrenrequired
React.JSX.Element | ((props: HotkeysTargetRenderProps) => React.JSX.Element)
Render prop which receives the same callback handlers generated by the useHotkeys hook. If your hotkey definitions are all global, you may supply an element instead.
hotkeysrequired
readonly HotkeyConfig[]
Hotkey definitions.
options
UseHotkeysOptions
Hook customization options.

Hotkey configuration#

Each entry in hotkeys takes these fields.

interface HotkeyConfig@clawscale/react
PropsDescription
allowInInput
boolean = false
Whether the hotkey should be triggerable when focused in a text input.
comborequired
string
Hotkey combination string (AKA "key combo"), such as "space" or "cmd+n".
disabled
boolean = false
Whether the hotkey cannot be triggered.
global
boolean = false
If false, the hotkey is active only when the target is focused. If true, the hotkey can be triggered regardless of what component is focused.
group
string
Unless the hotkey is global, you must specify a group where the hotkey will be displayed in the hotkeys dialog. This string will be displayed in a header at the start of the group of hotkeys.
labelrequired
React.ReactNode
Human-friendly label for the hotkey.
onKeyDown
(e: KeyboardEvent): any
keydown event handler.
onKeyUp
(e: KeyboardEvent): any
keyup event handler.
preventDefault
boolean = false
When true, invokes event.preventDefault() before the respective onKeyDown and onKeyUp callbacks are invoked. Enabling this can simplify handler implementations.
stopPropagation
boolean = false
When true, invokes event.stopPropagation() before the respective onKeyDown and onKeyUp callbacks are invoked. Enabling this can simplify handler implementations.

KeyComboTag#

interface KeyComboTagProps extends Props@clawscale/react
PropsDescription
className
string
A space-delimited list of class names to pass along to a child element.
Inherited from Props
comborequired
string
The key combo to display, such as "cmd + s".
minimal
boolean = false
Whether to render in a minimal style. If false, each key in the combo will be rendered inside a <kbd> tag. If true, only the icon or short name of a key will be rendered with no wrapper styles.