Skip to content

Patterns

Complete integration examples for @diffgazer/keys with @diffgazer/ui components including Menu, Dialog, CommandPalette, Tabs, and multi-zone layouts.

@diffgazer/ui components include their local keyboard behavior. Use @diffgazer/keys when you need app-level shortcuts, scoped shortcut stacks, package-mode hook imports, or custom composites that are not already covered by a UI component.


tsx
import { useState } from "react"
import { Menu, MenuItem } from "@/components/ui/menu"

function ControlledMenu() {
  const [selectedId, setSelectedId] = useState<string | null>(null)
  const [highlighted, setHighlighted] = useState<string | null>(null)

  return (
    <Menu
      aria-label="File menu"
      selectedId={selectedId}
      highlighted={highlighted}
      onSelect={setSelectedId}
      onHighlightChange={setHighlighted}
    >
      <MenuItem id="new">New File</MenuItem>
      <MenuItem id="open">Open File</MenuItem>
      <MenuItem id="save">Save</MenuItem>
    </Menu>
  )
}

Dialog with shortcut scope

tsx
import { useScope, useKey } from "@diffgazer/keys"
import {
  Dialog, DialogContent, DialogHeader, DialogTitle,
  DialogBody, DialogFooter, DialogClose, DialogAction,
} from "@/components/ui/dialog"

function ConfirmDialog({ open, onOpenChange, onConfirm }) {
  const scope = useScope("confirm-dialog", { enabled: open })
  useKey("mod+enter", onConfirm, { scope })

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Confirm Action</DialogTitle>
        </DialogHeader>
        <DialogBody>Save changes?</DialogBody>
        <DialogFooter>
          <DialogClose bracket variant="ghost">Cancel</DialogClose>
          <DialogAction onClick={onConfirm}>Save</DialogAction>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

Dialog owns focus trapping, Escape handling, and scroll locking. Use @diffgazer/keys here for app-level shortcuts that should be active only while the dialog scope is on top.


Command palette with useScope

tsx
import { useScope } from "@diffgazer/keys"
import {
  CommandPalette, CommandPaletteContent, CommandPaletteInput,
  CommandPaletteList, CommandPaletteGroup, CommandPaletteItem,
} from "@/components/ui/command-palette"

function AppPalette({ open, onOpenChange, onActivate }) {
  useScope("palette", { enabled: open })

  return (
    <CommandPalette open={open} onOpenChange={onOpenChange} onActivate={onActivate}>
      <CommandPaletteContent>
        <CommandPaletteInput placeholder="Search..." />
        <CommandPaletteList>
          <CommandPaletteGroup heading="Files">
            <CommandPaletteItem id="readme">README.md</CommandPaletteItem>
            <CommandPaletteItem id="index">index.ts</CommandPaletteItem>
          </CommandPaletteGroup>
        </CommandPaletteList>
      </CommandPaletteContent>
    </CommandPalette>
  )
}

The scope stack ensures that while the palette is open, only palette-scoped handlers fire. Global shortcuts like mod+k are automatically suppressed until the palette closes.


Multi-zone layout with useFocusZone

tsx
import { useFocusZone, useKey } from "@diffgazer/keys"

function TwoPaneLayout() {
  const { zone, isZone, getKeyOptions, getZoneProps } = useFocusZone({
    initial: "sidebar",
    zones: ["sidebar", "main"] as const,
    tabCycle: ["sidebar", "main"] as const,
    transitions: ({ zone, key }) => {
      if (zone === "sidebar" && key === "ArrowRight") return "main"
      if (zone === "main" && key === "ArrowLeft") return "sidebar"
      return null
    },
  })

  useKey("n", () => createNew(), getKeyOptions("sidebar"))
  useKey("e", () => edit(), getKeyOptions("main"))

  return (
    <div className="flex">
      <aside
        {...getZoneProps("sidebar")}
        className={isZone("sidebar") ? "ring-1 ring-foreground" : ""}
      >
        Sidebar
      </aside>
      <main
        {...getZoneProps("main")}
        className={isZone("main") ? "ring-1 ring-foreground" : ""}
      >
        Content
      </main>
    </div>
  )
}

Tabs with built-in navigation

tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"

function KeyboardTabs() {
  return (
    <Tabs defaultValue="preview">
      <TabsList>
        <TabsTrigger value="preview">Preview</TabsTrigger>
        <TabsTrigger value="code">Code</TabsTrigger>
        <TabsTrigger value="api">API</TabsTrigger>
      </TabsList>
      <TabsContent value="preview">Preview content</TabsContent>
      <TabsContent value="code">Code content</TabsContent>
      <TabsContent value="api">API content</TabsContent>
    </Tabs>
  )
}

Custom composite with useNavigation

tsx
import { useRef } from "react"
import { useNavigation } from "@diffgazer/keys"

const items = [
  { id: "main", label: "main" },
  { id: "develop", label: "develop" },
  { id: "feature-auth", label: "feature/auth" },
]

function getOptionId(id) {
  return `branch-${id}`
}

function BranchList({ onChoose }) {
  const containerRef = useRef<HTMLDivElement>(null)
  const { highlighted, isHighlighted, onKeyDown } = useNavigation({
    containerRef,
    role: "option",
    wrap: true,
    onEnter: onChoose,
    onSelect: onChoose,
  })

  return (
    <div
      ref={containerRef}
      role="listbox"
      aria-label="Branches"
      aria-activedescendant={highlighted ? getOptionId(highlighted) : undefined}
      tabIndex={0}
      onKeyDown={onKeyDown}
    >
      {items.map((item) => (
        <div
          key={item.id}
          id={getOptionId(item.id)}
          data-value={item.id}
          role="option"
          aria-selected={isHighlighted(item.id)}
          data-highlighted={isHighlighted(item.id) ? "" : undefined}
        >
          {item.label}
        </div>
      ))}
    </div>
  )
}

Combined layout example

tsx
import { useState } from "react"
import { useFocusZone, useKey, useScope } from "@diffgazer/keys"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
import { Menu, MenuItem } from "@/components/ui/menu"

function ReviewWorkspace() {
  const [confirmOpen, setConfirmOpen] = useState(false)
  const scope = useScope("confirm", { enabled: confirmOpen })
  const { isZone, getKeyOptions, getZoneProps } = useFocusZone({
    initial: "queue",
    zones: ["queue", "details"] as const,
    transitions: ({ zone, key }) => {
      if (zone === "queue" && key === "ArrowRight") return "details"
      if (zone === "details" && key === "ArrowLeft") return "queue"
      return null
    },
  })

  useKey("mod+k", () => openCommandPalette(), { preventDefault: true })
  useKey("Enter", () => setConfirmOpen(true), getKeyOptions("queue"))
  useKey("Escape", () => setConfirmOpen(false), { scope })

  return (
    <div className="grid grid-cols-[16rem_1fr] gap-4">
      <Menu
        {...getZoneProps("queue")}
        aria-label="Review queue"
        className={isZone("queue") ? "ring-1 ring-foreground" : ""}
      >
        <MenuItem id="api">API issue</MenuItem>
        <MenuItem id="tests">Missing test</MenuItem>
      </Menu>
      <section
        {...getZoneProps("details")}
        className={isZone("details") ? "ring-1 ring-foreground" : ""}
      >
        <Button onClick={() => setConfirmOpen(true)}>Apply fix</Button>
      </section>
      <Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
        <DialogContent>
          <DialogTitle>Apply fix?</DialogTitle>
          <Button onClick={() => setConfirmOpen(false)}>Confirm</Button>
        </DialogContent>
      </Dialog>
    </div>
  )
}