# Action Keys

> Gives a list of actions their single-key shortcuts. It shows nothing, and it ignores keys while someone is typing.

- Name: `action-keys`
- Status: beta
- Categories: actions
- File: the install writes `action-keys.tsx` to your `ui` alias, `@/components/ui` by default.
- Gallery: https://arbokui.com/components/action-keys
- Registry item: https://arbokui.com/r/action-keys.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/action-keys
   ```

## Use it for

- Single-key shortcuts for a focused or hovered row's actions, like R for Reply.
- Pass the same action objects you pass to HoverActions or BulkBar, so the key, the tooltip, and the button never drift apart.
- Pass the toolbar's ref as within. A key then presses its button, so it shows the same spinner, result, and Undo a click does.
- Turn it on for one row at a time with enabled. RowActions does this for you.
- When two are on at once, like a row under the pointer and another with focus, a key goes to the one turned on last that has it. It never acts on both.
- Put KeepTabInside last, right inside a popup of your own that keeps focus in, so a fast Tab cannot leave it.

## Avoid it for

- Shortcuts with Cmd or Ctrl. It ignores keys with a modifier, so it never takes the browser's own.
- Keys inside a field. It ignores keys while someone types in an input, a textarea, or editable text.
- Turning it on for every row in a list at once. Only the row turned on last would answer, and no one could see which row that is.

## Props

- `actions`: `ActionKey[]`. Required. The actions. Each with a shortcut gets its key.
- `ActionKey`: `{ label: string; shortcut?: string; onClick?: () => void | Promise<unknown>; disabled?: boolean }`. Type. shortcut is one key, matched in either case. A disabled action ignores its key. HoverAction and BulkBarAction fit it.
- `enabled`: `boolean`. Default `true`. Listens for keys. Turn it on for the focused or hovered row only. When two are on, the one turned on last answers a key.
- `within`: `RefObject<HTMLElement | null>`. The toolbar that holds the actions' buttons, like HoverActions' ref. A key presses the button whose data-shortcut matches. Without it, a key calls onClick.
- `isPageKey`: `(event: KeyboardEvent, within?: HTMLElement | null) => boolean`. Helper. Whether a key is free for page shortcuts: no one is typing, no modifier is held, and focus is not in an open dialog or menu.
- `KeepTabInside`: `{}`. Part, with no props. Put it last, right inside a popup that keeps focus in, not in a wrapper. Tab and Shift+Tab then wrap at the ends at once, so a fast Tab never lands on the page behind. Dialog, Alert Dialog, Sheet, Lightbox, Command Menu, and Notifications use it.

## Accessibility

- Every key it listens for should be taught where people see the action, like HoverActions' tooltips, and set as aria-keyshortcuts on the button. ToolButton does both.
- It ignores keys while focus is in a text field, a textarea, a select, or editable text, so typing never runs an action.
- It ignores keys with Cmd, Ctrl, or Alt held, keys held down, and keys another handler already took.
- It ignores keys while focus is in an open dialog or menu that its toolbar is not part of.
- Single-key shortcuts can clash with speech input. Keep them on only while their row is hovered or focused, as WCAG 2.1.4 asks.
- KeepTabInside wraps Tab only past the last stop and Shift+Tab only before the first. In between, the browser moves focus as it always does. A field that takes Tab itself, and calls preventDefault, keeps it.
- Safari's Tab skips plain buttons and links, so it can pass the last stop KeepTabInside knows of. KeepTabInside then sends focus back in at once.
- A native radio group with a checked choice is one Tab stop, at that choice, as in the browser. Shift+Tab wraps back to it.

## Examples

### Keys for a focused row (needs @arbokui/hover-actions)

```tsx
import { Pencil, Reply } from "lucide-react";
import { useRef } from "react";

import { HoverActions, type HoverAction } from "@/components/ui/hover-actions";

function MessageActions({ focused, actions }: { focused: boolean; actions: HoverAction[] }) {
  const bar = useRef<HTMLDivElement>(null);
  return (
    <>
      <HoverActions ref={bar} visible={focused} actions={actions} />
      <ActionKeys actions={actions} enabled={focused} within={bar} />
    </>
  );
}

<MessageActions focused actions={[{ icon: <Reply />, label: "Reply", shortcut: "R" }, { icon: <Pencil />, label: "Edit", shortcut: "E" }]} />
```
