# Context Menu

## Overview

`ContextMenu` is an anchored panel of selectable cells. The component is built around two primitives: a single `ContextMenuItem` whose `type` prop switches between cell variants (`option`, `search`, `heading`, `divider`), and a panel that supports a preset shorthand (`type="list" | "phone" | "checkbox" | "radio" | "status" | "brandLogo" | "avatar" | "loading"`) plus a fully custom composition path via `children`.

## When to use

- A primary action menu attached to a trigger (e.g. a button's overflow actions).
- A selection control with checkbox or radio cells (multi-select or single-select).
- A lightweight picker for status, country, or brand-logo lists.

## When not to use

- For form fields driven by validation — use `Select`, `Autocomplete` or a plain `Input` instead.
- For navigational menus or app-level navigation — use the appropriate navigation primitives.
- For a single destructive confirmation — use a `Dialog`.

## Installation

```bash
yarn add @xsolla/xui-context-menu
```

## Two API paths

### Preset path

Pass `type` and `items`. The panel renders the preset's chrome and composes each option with the right control or slot.

```tsx
import { ContextMenu } from "@xsolla/xui-context-menu";

<ContextMenu
  type="list"
  trigger={<Button>Open</Button>}
  items={[
    { type: "option", label: "Edit" },
    { type: "option", label: "Duplicate" },
    { type: "option", label: "Delete", destructive: true },
  ]}
/>;
```

### Custom path

Compose cells as children when you need full control of the layout, want to mix headings and dividers freely, or render a slot the preset path doesn't cover.

```tsx
import { ContextMenu, ContextMenuItem } from "@xsolla/xui-context-menu";

<ContextMenu trigger={<Button>Open</Button>} aria-label="Actions">
  <ContextMenuItem type="heading" label="Workspace" />
  <ContextMenuItem type="option" label="Personal" />
  <ContextMenuItem type="option" label="Acme Inc." />
  <ContextMenuItem type="divider" />
  <ContextMenuItem type="option" label="Sign out" destructive />
</ContextMenu>;
```

Choose the preset path for typical menus where the data is uniform; choose the custom path when cells differ structurally or when you need to drop in bespoke nodes between cells.

## `ContextMenuItem` reference

`ContextMenuItem` is a discriminated union on `type`. All cell types accept `size`, `data-testid` and theme-override props.

### `type="option"`

| Prop | Type | Purpose |
| --- | --- | --- |
| `label` | `ReactNode` | Primary cell text (required). |
| `description` | `ReactNode` | Secondary line beneath the label. |
| `leadingControl` | `"checkbox" \| "radio"` | Renders a `Checkbox` or `Radio` at the start. |
| `leadingIcon` | `ReactNode` | Icon node before the label group. |
| `status` | `ReactNode` | Status indicator slot (e.g. `<Status>`). |
| `iconWrapper` | `ReactNode` | Wrapped icon / avatar slot. |
| `slot` / `slotContent` | `ReactNode` | Generic slot before the label. |
| `value` | `ReactNode` | Right-side primary text (e.g. shortcut value, dial code). |
| `hint` | `ReactNode` | Right-side secondary text below `value`. |
| `trailingIcon` | `ReactNode` | Trailing icon at the end of the cell. |
| `keyboardShortcut` | `string` | Display-only shortcut rendered as `<kbd>` and exposed via `aria-keyshortcuts`. |
| `hasSubmenu` | `boolean` | Marks the cell as a submenu trigger and renders a chevron. |
| `submenu` | `ReactNode` | A nested `<ContextMenu>` opened on hover/`ArrowRight`/`Enter`. |
| `checked` | `boolean` | Fully controlled checked state. |
| `disabled` | `boolean` | Disables interaction and applies the disabled style. |
| `destructive` | `boolean` | Applies the destructive content colour. |
| `onSelect` | `() => void` | Fires on activation (click, `Enter`, `Space`). |
| `onCheckedChange` | `(checked: boolean) => void` | Optional change callback for controls. |

Render order (left → right): `leadingControl`, `leadingIcon`, `status`, `iconWrapper`, `slotContent`, `label` (with optional `description` below), `value` (with optional `hint` below), `keyboardShortcut`, submenu chevron, `trailingIcon`.

### `type="search"`

| Prop | Type | Purpose |
| --- | --- | --- |
| `value` | `string` | Controlled value (required). |
| `onValueChange` | `(value: string) => void` | Change callback (required). |
| `placeholder` | `string` | Defaults to `"Search"`. |
| `autoFocus` | `boolean` | Focuses the input on mount. |
| `aria-label` | `string` | Defaults to `"Search options"`. |

### `type="heading"`

| Prop | Type | Purpose |
| --- | --- | --- |
| `label` | `ReactNode` | Section title (uppercase styling). |
| `description` | `ReactNode` | Optional helper line beneath. |

### `type="divider"`

A horizontal rule with `role="separator"`. No content props.

## `ContextMenu` reference

| Prop | Type | Purpose |
| --- | --- | --- |
| `type` | `"list" \| "loading" \| "phone" \| "checkbox" \| "status" \| "brandLogo" \| "radio" \| "avatar"` | Panel preset; works with `items`. |
| `items` | `ReadonlyArray<Option \| Heading \| Divider>` | Data-driven cells for the preset path. |
| `children` | `ReactNode` | Custom-composition cells (alternative to `items`). |
| `size` | `"sm" \| "md" \| "lg" \| "xl"` | Controls cell sizing across the panel. Default `md`. |
| `searchable` | `boolean` | Auto-renders a sticky search cell and filters options. |
| `loading` | `boolean` | Renders a centred spinner instead of the cell list. |
| `emptyMessage` | `string` | Custom message for the default empty state. |
| `empty` | `ReactNode` | Replace the empty state entirely. |
| `trigger` | `ReactNode` | Element that toggles the panel; receives `aria-haspopup` / `aria-expanded`. |
| `placement` | `"bottom-start" \| "top-start" \| "bottom-end" \| "top-end"` | Initial placement. Auto-flips when clipped. |
| `isOpen` | `boolean` | Controlled open state. |
| `onOpenChange` | `(open: boolean) => void` | Open-state callback. |
| `closeOnSelect` | `boolean` | Override the per-preset default. |
| `width` | `number` | Forces panel width (px). |
| `maxHeight` | `number` | Caps panel height (px); body scrolls and search stays sticky. |
| `onSelect` | `(item: ContextMenuOptionItemProps) => void` | Fires for the preset path on option activation. |
| `aria-label` | `string` | Accessible name for the menu container. |
| `data-testid` | `string` | Testing handle. |

## Behaviour & accessibility

- The panel root is `role="menu"` (or hosts `role="menuitemcheckbox"` / `role="menuitemradio"` cells when `checked` is provided). Headings render as `role="presentation"`, dividers as `role="separator"`, and the search cell as `role="searchbox"`.
- `closeOnSelect` defaults to `true` for every preset except `checkbox`, where multi-select keeps the panel open.
- On open, focus moves to the search input when present, otherwise to the first option.
- On close, focus returns to the trigger.
- The trigger element receives `aria-haspopup="menu"` and `aria-expanded` synced to the open state.

## Keyboard reference

| Key | Action |
| --- | --- |
| `↑` / `↓` | Move active option up/down (skips heading/divider). |
| `Enter` / `Space` | Activate the focused option. |
| `Esc` | Close the menu and return focus to the trigger. |
| `Tab` | Close the menu and continue natural focus order. |
| `Home` / `End` | Jump to the first/last option. |
| `→` / `Enter` | Open a submenu when on a `hasSubmenu` option. |
| `←` / `Esc` | Close the submenu and return focus to its parent option. |

## Content guidelines

- Use short, imperative labels ("Edit", "Duplicate", "Sign out").
- Use sentence case for option labels.
- Use uppercase for headings — the heading cell already applies the visual treatment.
- Place destructive options at the bottom of the list, ideally separated by a divider.
- Prefer specific empty messages ("No countries match") over the generic default.
- Aim for seven options or fewer per panel; group with headings or split into submenus when longer.

## Migration from prior API

| Old | New |
| --- | --- |
| `ContextMenuCheckboxItem` | `<ContextMenuItem type="option" leadingControl="checkbox" />` |
| `ContextMenuRadioItem` | `<ContextMenuItem type="option" leadingControl="radio" />` |
| `ContextMenuRadioGroup` | `type="radio"` panel preset with shared selected state |
| `ContextMenuGroup` | `<ContextMenuItem type="heading" />` |
| `ContextMenuSeparator` | `<ContextMenuItem type="divider" />` |
| `ContextMenuSearch` | `<ContextMenuItem type="search" />` (or set `searchable: true` on the panel for auto-render) |
| Size scale `s` / `m` / `l` / `xl` | `sm` / `md` / `lg` / `xl` |
