DropdownMenu

Organism

Accessible dropdown using role="menu" + role="menuitem". Closes on Escape and outside click. Supports left/right alignment, icons, separators, danger and disabled items, and arrow-key navigation. Items take an optional id, href, group, order, hidden and disabledReason so other modules can contribute entries; `portal` escapes clipping parents.

Default

Preview
Code
<DropdownMenu
  trigger={<Button variant="outline" size="sm">Actions ▾</Button>}
  items={[
    { label: 'Edit', icon: '✏' },
    { label: 'Duplicate', icon: '⧉' },
    { type: 'separator' },
    { label: 'Delete', icon: '🗑', danger: true },
  ]}
/>

Right-aligned

Preview
Code
<DropdownMenu align="right"
  trigger={<Button variant="ghost" size="sm">⋮</Button>}
  items={[{ label: 'View details' }, { label: 'Remove', danger: true }]}
/>

Contributed items: group, order, hidden, href, disabledReason

Preview
Code
// Items sort by group (ranked by their lowest order) and order; a separator splits groups.
<DropdownMenu
  trigger={<Button variant="outline" size="sm">Contact ▾</Button>}
  items={[
    { id: 'edit', label: 'Edit', group: 'record', order: 10 },
    { id: 'call', label: 'Call', group: 'contact', order: 20, href: '/calls/new?to=…' },
    { id: 'sign', label: 'Send for signature', group: 'contact', order: 30, disabled: true, disabledReason: 'Add an email address first' },
    { id: 'campaign', label: 'Add to campaign', group: 'contact', order: 40, hidden: !canMarket },
    { id: 'delete', label: 'Delete', group: 'danger', order: 1000, danger: true },
  ]}
/>
// Merge contributed items into a base list (same id replaces in place):
const items = mergeMenuItems(baseItems, contributedItems);
Sourcemodules/ui/DropdownMenu.tsx