> ## Documentation Index
> Fetch the complete documentation index at: https://help.stralos.app/llms.txt
> Use this file to discover all available pages before exploring further.

# RowMenu

> Row action kebab component in @stralos-co-nz/ui and how to test it

`@stralos-co-nz/ui` exports `RowMenu`, the kebab that carries a row's actions in `DataTable` and the scheduler's resource grid. Every row action surface in the suite — `rowActions`, `sectionActions`, and the scheduler's per-resource menu — is built from it.

```tsx theme={null}
import { RowMenu } from "@stralos-co-nz/ui";
```

## Props

| Prop | Type | Default | Description |
| - | - | - | - |
| `items` | `RowMenuItem[]` | Required | The menu items. |
| `label` | `string` | `"Row actions"` | Accessible name for the trigger button. `DataTable` supplies `Actions for <row label>`. |
| `size` | `"xs"` \| `"2xs"` | `"xs"` | Button size. `DataTable` passes `"2xs"` for `density="compact"` rows. |
| `placement` | Popper placement string | — | Where the menu opens relative to the trigger. Default opens below. Pass `"left-start"` for a trigger beside money columns. |

Each `RowMenuItem` accepts:

| Field | Type | Default | Description |
| - | - | - | - |
| `label` | `string` | Required | Item text and accessible name. |
| `onSelect` | `() => void` | Required | Called when the item is activated. |
| `destructive` | `boolean` | — | Renders in the error colour. Pair with a `ConfirmDialog`. |
| `disabled` | `boolean` | — | Prevents activation. `RowMenu` guards this itself. |
| `group` | `string` | — | Consecutive items sharing an equal group name render under a shared heading, separated from other groups. |
| `separatorBefore` | `boolean` | — | Inserts a hairline above the item without a group heading. Has no effect on the first item. |

**Use instead of** the seven row-action vocabularies in the estate: a **View** link, an **Edit** button, **Open**, **Open** plus a separate kebab, two inline buttons, or nothing at all.

## How activation works (from 0.34.0)

Each item's `onSelect` handler is wired to the item's own `onClick` event. This replaces the previous wiring through Ark's `Menu.Root onSelect`, which dropped activations silently when the menu machine had no highlighted item.

The old path failed in three cases that look unrelated in tests and in production:

* **Assistive technology** — an activate gesture (`Space`, `Enter`, or a pointer click) with no prior hover over the item left the machine's `highlightedValue` as `null`. The menu closed; the handler was not called.
* **Programmatic activation** — any `click()` or `fireEvent.click()` that arrived without a preceding `pointermove` at distinct coordinates hit the same gap.
* **Later selections in a test file** — `@zag-js/focus-visible` stores `lastPointerPosition` at module scope. In happy-dom, where every pointer event reports `(0, 0)`, only the first `pointermove` in a module updated the interaction modality. Every later pointer event was treated as stationary, so the modality reverted to `"virtual"` when the menu opened and highlighted nothing.

From 0.34.0, `RowMenu` and the scheduler's resource menu wire each handler directly to the item's `onClick`. The keyboard path is unchanged — Ark activates the highlighted item by calling `element.click()` on it, which fires `onClick`. A `disabled` item is now stopped by `RowMenu`'s own guard rather than relying on Ark refusing to emit, so the `disabled` prop is load-bearing.

## Testing a RowMenu (from 0.34.0)

Open the trigger, then click the item. `fireEvent` is sufficient — `@testing-library/user-event` is not required, and the pattern works any number of times in one test file.

```tsx theme={null}
fireEvent.click(screen.getByRole("button", { name: "Actions for 2A Sand" }));
await waitFor(() => expect(screen.getByText("Delete")).toBeTruthy());
fireEvent.click(screen.getByText("Delete"));
await waitFor(() => expect(remove).toHaveBeenCalledTimes(1));
```

`userEvent.click` also works. The keyboard path is `{ArrowDown}{Enter}` after opening the trigger.

**Assert `toHaveBeenCalledTimes(1)`, not `toHaveBeenCalled()`.** The failure mode this replaced was a silent zero; a future rewiring could produce a two. Both are invisible to `toHaveBeenCalled`.

**Add a positive control alongside any "handler did not fire" assertion.** A click that reaches nothing satisfies every negative assertion. Without a positive control you cannot distinguish a correctly blocked action from a broken gesture.

### Migrating older tests

Tests written against 0.33.0 and earlier used workarounds: keyboard-only activation, a single selection per test file, or a full `pointerDown`/`pointerUp`/`click` sequence. Those workarounds continue to pass — the fix does not change what the workaround gestures do. Remove them when you are already editing the test for an unrelated reason; do not leave them as the sole form of coverage.

## Accessible name

`DataTable` passes a label derived from the row. The label comes from `getRowLabel` when supplied. When `getRowLabel` is not set, the label comes from the first column's rendered text if that column renders a plain string, and falls back to `getRowId` otherwise.

| First column | Trigger label |
| - | - |
| `accessor: "name"` → `"2A Sand"` | `Actions for 2A Sand` |
| `render: (r) => <Link>{r.name}</Link>` | `Actions for row 0f9c1a8e-…` |
| `getRowLabel={(r) => r.name}` | `Actions for 2A Sand` |

Pass `getRowLabel` whenever the first column renders a link, a badge, or anything that is not plain text.

## Use in DataTable

`DataTable` creates the kebab column automatically when `rowActions` is set. The column is shared with the `columnPicker` slot; `DataTable` creates it even when only one of the two is present.

```tsx theme={null}
<DataTable
  rows={rates}
  getRowId={(rate) => rate.id}
  getRowLabel={(rate) => rate.name}
  columns={columns}
  rowActions={(rate) => [
    { label: "Edit", onSelect: () => edit(rate) },
    { label: "Delete", onSelect: () => confirmDelete(rate), destructive: true },
  ]}
/>
```

Section actions work the same way through `sectionActions`. The section kebab is named after the section label, falling back to `section <id>`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.