# Dropdown Menu

> Menu surface with `role="menu"` + `menuitem`, roving-tabindex arrow-key navigation, and focus return.

- Available in: `@sisyphos-ui/react`, `@sisyphos-ui/vue`, `@sisyphos-ui/angular`
- Docs: https://www.sisyphosui.com/docs/components/dropdown-menu
- WAI-ARIA pattern: [Menu Button](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/)

## Installation

Pick the framework binding that matches your stack:

```bash
pnpm add @sisyphos-ui/react   # React 18+
pnpm add @sisyphos-ui/vue     # Vue 3+
pnpm add @sisyphos-ui/angular # Angular 17+
```

## Import

```tsx
import "@sisyphos-ui/react/styles.css";
import { DropdownMenu } from "@sisyphos-ui/react";
```

## Framework usage

### React 18+

```tsx
import { DropdownMenu, Button } from "@sisyphos-ui/react";

export const RowActions = () => (
  <DropdownMenu
    items={[
      { label: "Edit", onSelect: () => {} },
      { label: "Duplicate", onSelect: () => {} },
      { type: "separator" },
      { label: "Delete", destructive: true, onSelect: () => {} },
    ]}
  >
    <Button variant="outlined">Actions ▾</Button>
  </DropdownMenu>
);
```

### Vue 3+

```vue
<script setup lang="ts">
import { DropdownMenu, Button } from "@sisyphos-ui/vue";

const items = [
  { label: "Edit", onSelect: () => {} },
  { label: "Duplicate", onSelect: () => {} },
  { type: "separator" as const },
  { label: "Delete", destructive: true, onSelect: () => {} },
];
</script>

<template>
  <DropdownMenu :items="items">
    <Button variant="outlined">Actions ▾</Button>
  </DropdownMenu>
</template>
```

### Angular 17+

```ts
import { Component } from "@angular/core";
import { DropdownMenu, Button } from "@sisyphos-ui/angular";
import type { DropdownMenuItem } from "@sisyphos-ui/angular";

@Component({
  selector: "app-row-actions",
  standalone: true,
  imports: [DropdownMenu, Button],
  template: `
    <sui-dropdown-menu [items]="items">
      <sui-button variant="outlined">Actions ▾</sui-button>
    </sui-dropdown-menu>
  `,
})
export class RowActionsComponent {
  items: DropdownMenuItem[] = [
    { label: "Edit", onSelect: () => {} },
    { label: "Duplicate", onSelect: () => {} },
    { type: "separator" },
    { label: "Delete", destructive: true, onSelect: () => {} },
  ];
}
```

## Examples

### Default

Declarative menu items — supports separators, labels, shortcuts, destructive items.

```tsx
import { Button, DropdownMenu } from "@sisyphos-ui/react";

export function Example() {
  return (
    <DropdownMenu
      items={[
        { type: "label", label: "Account" },
        { label: "Profile",  onSelect: () => console.log("profile") },
        { label: "Billing",  onSelect: () => console.log("billing") },
        { label: "Settings", shortcut: "⌘,", onSelect: () => console.log("settings") },
        { type: "separator" },
        { type: "label", label: "Team" },
        { label: "Invite members", onSelect: () => console.log("invite") },
        { label: "Manage roles",   onSelect: () => console.log("roles") },
        { type: "separator" },
        { label: "Sign out", destructive: true, onSelect: () => console.log("sign-out") },
      ]}
    >
      <Button variant="outlined">Open menu</Button>
    </DropdownMenu>
  );
}
```

### Placement

Anchor the menu to any corner of the trigger.

```tsx
import { Button, DropdownMenu } from "@sisyphos-ui/react";

export function Example() {
  return (
    <>
      {(["bottom-start", "bottom-end", "top-start", "top-end"] as const).map((p) => (
        <DropdownMenu
          key={p}
          placement={p}
          items={[
            { label: "Open", onSelect: () => {} },
            { label: "Duplicate", onSelect: () => {} },
            { type: "separator" },
            { label: "Delete", destructive: true, onSelect: () => {} },
          ]}
        >
          <Button variant="outlined" size="sm">{p}</Button>
        </DropdownMenu>
      ))}
    </>
  );
}
```

### Disabled items

Greyed-out items stay visible for discoverability but skip in keyboard navigation.

```tsx
import { Button, DropdownMenu } from "@sisyphos-ui/react";

export function Example() {
  return (
    <DropdownMenu
      items={[
        { label: "Save", shortcut: "⌘S", onSelect: () => {} },
        { label: "Save as…", shortcut: "⇧⌘S", onSelect: () => {} },
        { type: "separator" },
        { label: "Export PDF", disabled: true, onSelect: () => {} },
        { label: "Export CSV", disabled: true, onSelect: () => {} },
        { type: "separator" },
        { label: "Print", shortcut: "⌘P", onSelect: () => {} },
      ]}
    >
      <Button variant="outlined">File</Button>
    </DropdownMenu>
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| **items** (required) | `DropdownMenuItem[]` | — | Actions, separators, and labels rendered inside the menu. |
| **children** (required) | `ReactElement` | — | Single focusable element used as the trigger. |
| placement | `"top" \| "bottom" \| "left" \| "right" \| "top-start" \| "top-end" \| "bottom-start" \| "bottom-end"` | `"bottom-start"` | Preferred placement; auto-flips to the opposite side if it doesn't fit the viewport. |
| open | `boolean` | — | Controlled open state. |
| defaultOpen | `boolean` | `false` | Initial open state when uncontrolled. |
| onOpenChange | `(open: boolean) => void` | — | Fires when the menu opens or closes. |
| header | `ReactNode` | — | Slot rendered above the items (for `Notifications · 3` style labels). |
| footer | `ReactNode` | — | Slot rendered below the items (for `View all` links). |
| emptyState | `ReactNode` | — | Rendered when `items` is empty. |
| onScrollEnd | `() => void` | — | Fires when the menu scrolls within `scrollEndThreshold` px of the bottom — use for infinite notification lists. |
| scrollEndThreshold | `number` | `48` | Pixel distance from the bottom that triggers `onScrollEnd`. |
| maxHeight | `number \| string` | — | Cap the menu height so long item lists become scrollable. |

## Keyboard interactions

- **ArrowDown + ArrowUp** — Move between enabled items, wrapping at the edges. On a closed menu, ArrowDown opens it.
- **Home + End** — Jump to the first / last enabled item.
- **Enter + Space** — Open a closed menu, or activate the focused item (`onSelect`).
- **Esc** — Close the menu and return focus to the trigger.
- **Tab** — Close the menu.

## Accessibility notes

- The trigger is wired with `aria-haspopup="menu"`, `aria-expanded`, and `aria-controls`; the list renders `role="menu"` with `role="menuitem"` items.
- Separators and section labels are exposed as `role="separator"` / `role="presentation"`, so assistive tech skips them during item navigation.
- Disabled items carry `aria-disabled` and are excluded from arrow-key navigation.
- Roving `tabIndex` keeps exactly one item focusable; hovering an item moves the active position so pointer and keyboard stay in sync.
- Escape and item activation close the menu and return focus to the trigger.

<!-- exports: { "DropdownMenu": "@sisyphos-ui/react" } -->