Input & Keymaps

Navigate deployments with the keyboard and add a ? help overlay.

This chapter's app is running in your console. Open it with ⌘⌥JCtrl+Shift+J, then try it.
Watch the chapter3:26 · narrated · captions

In this chapter

  • `useInput`
  • Keymaps
  • The help overlay
  • Focus

Keystrokes reach your app through a hidden input on the page, so the page needs focus. That's why every Text.js app asks you to click the page first. Once it has focus, every key you press is delivered to React.

useInput

useInput is the low-level hook you've used so far. It gets a KeyEvent with a normalized key ("up", "return", "escape"), the typed character in input, and ctrl, alt, shift and meta.

useInput((e) => {  if (e.ctrl && e.key === "k") openPalette();});

Call e.stopPropagation() to keep a key from reaching handlers further down the tree.

Keymaps

Once a screen has more than a couple of keys, describe them with useKeymap. Bindings are strings: "up, k" is either key, "g g" is a sequence, "ctrl+k" a chord. Labeled bindings also appear in <KeyHelp>, <KeyHints> and the command palette.

app/deployments/page.tsx+31tsx
Added: import { useState } from "react";Added: import { Box, KeyHelp, Text, useKeymap } from "@textjs/next";Added: import { DEPLOYMENTS } from "../../lib/deployments";Added: Added: export default function DeploymentsPage() {Added:   const [index, setIndex] = useState(0);Added:   const [open, setOpen] = useState(false);Added:   const move = (by: number) => setIndex((i) => Math.max(0, Math.min(DEPLOYMENTS.length - 1, i + by)));Added: Added:   useKeymap({Added:     "up, k": { label: "Previous deployment", run: () => move(-1) },Added:     "down, j": { label: "Next deployment", run: () => move(1) },Added:     enter: { label: "Toggle details", run: () => setOpen((o) => !o) },Added:     "g g": { label: "First deployment", run: () => setIndex(0) },Added:   });Added: Added:   return (Added:     <Box className="flex-row grow p-1 gap-2">Added:       <Box className="flex-col grow">Added:         {DEPLOYMENTS.map((d, i) => (Added:           <Box key={d.id} className="flex-row h-1 gap-2 px-1" backgroundColor={i === index ? "selection" : undefined}>Added:             <Text>{i === index ? "›" : " "}</Text>Added:             <Text bold={i === index} wrap="truncate-end">{d.commit}</Text>Added:           </Box>Added:         ))}Added:       </Box>Added:       {open && <Details deployment={DEPLOYMENTS[index]} />}Added:       <KeyHelp />Added:     </Box>Added:   );Added: }

DEPLOYMENTS is a plain array in lib/deployments.ts for now. Chapter 6 moves it behind a route handler.

The help overlay

<KeyHelp /> listens for ? and opens a modal listing every labeled binding that is live right now, grouped. Press ? in the console, then ? or q to close it. Bindings inside a modal trap focus, so the list underneath stops reacting until it closes.

Focus

Components like <Input>, <Select> and <Table> only take keys while focused. Tab moves focus between them; useFocus() and useFocusManager() let you do it yourself.

Try it

This is Acme Deploys at the end of the chapter, running in DevTools. Click the pad, then use the keys below.

▼ 04 · Input & Keymaps

Open the console

⌘⌥J
CtrlShiftJ

The app renders in DevTools. Then click here and press ↓.

↑↓selectEnterdetails?shortcuts

Open on a desktop

This demo renders in the Chrome DevTools console, which needs a desktop browser.

Quiz

2 questions

Answer every question to complete the chapter.

In useKeymap, what does the binding "g g" mean?

 

Why do you click the page before typing?