Menu

A trigger and the MenuList of commands it opens. It isn't modal: no focus trap, no scroll lock, nothing laid over the page, so the pointer passes over it and the page stays usable around it.

A trigger and the MenuList of commands it opens. It isn't modal: no focus trap, no scroll lock, nothing laid over the page, so the pointer passes over it and the page stays usable around it. The list is placed position: fixed in the window, under the trigger, so a panel that scrolls or clips its content can't cut it off - but it stays inside the menu's own box in the DOM, so focus moving inside it is still focus inside the menu.

import { Menu, MenuDivider } from '@soroush.tech/design-system/Menu'
import { MenuItem } from '@soroush.tech/design-system/MenuItem'
import { Button } from '@soroush.tech/design-system/Button'

;<Menu trigger={<Button variant="text">main</Button>} onSelect={run}>
  <MenuItem value="new">New branch...</MenuItem>
  <MenuItem value="checkout" disabled note="Coming later">
    Checkout tag or revision...
  </MenuItem>
  <MenuDivider />
  <MenuItem value="rename">Rename...</MenuItem>
</Menu>

Examples

Default

const args: Partial<MenuProps> = {
  align: 'start',
  defaultOpen: false,
  marginThreshold: 2,
  elevation: 8,
  layer: 'modal',
  color: 'primary',
  size: 'md',
}

<Menu {...args} trigger={<Button variant="outlined">main</Button>} />

Current Choice

const args: Partial<MenuProps> = {
  align: 'start',
  defaultOpen: false,
  marginThreshold: 2,
  elevation: 8,
  layer: 'modal',
  color: 'primary',
  size: 'md',
}

<Branches {...args} />

Aligned

const args: Partial<MenuProps> = {
  align: 'start',
  defaultOpen: false,
  marginThreshold: 2,
  elevation: 8,
  layer: 'modal',
  color: 'primary',
  size: 'md',
}

<Flex justifyContent="space-between" width="100%">
  <Menu {...args} align="start" trigger={<Button variant="outlined">Start</Button>}>
    {branchRows}
  </Menu>
  <Menu {...args} align="end" trigger={<Button variant="outlined">End</Button>}>
    {branchRows}
  </Menu>
</Flex>

Icons And Shortcuts

const args: Partial<MenuProps> = {
  align: 'start',
  defaultOpen: false,
  marginThreshold: 2,
  elevation: 8,
  layer: 'modal',
  color: 'primary',
  size: 'md',
}

<Menu {...args} trigger={<Button variant="text">File</Button>}>
  <MenuItem startIcon={<Icon name="folder" size="sm" color="inherit" />} shortcut="Control+O">
    Open...
  </MenuItem>
  <MenuItem startIcon={<Icon name="refresh" size="sm" color="inherit" />} shortcut="F5">
    Reload
  </MenuItem>
  <MenuDivider />
  <MenuItem shortcut="Control+W">Close</MenuItem>
</Menu>

Empty

const args: Partial<MenuProps> = {
  align: 'start',
  defaultOpen: false,
  marginThreshold: 2,
  elevation: 8,
  layer: 'modal',
  emptyLabel: 'Coming soon',
  color: 'primary',
  size: 'md',
}

<Menu {...args} trigger={<Button variant="outlined">Remote branches</Button>} />

Controlled Menu Bar

Open a menu, then step to its neighbours with Left and Right.
const args: Partial<MenuProps> = {
  align: 'start',
  defaultOpen: false,
  marginThreshold: 2,
  elevation: 8,
  layer: 'modal',
  color: 'primary',
  size: 'md',
}

<>
  <Typography variant="caption" mb={1}>
    Open a menu, then step to its neighbours with Left and Right.
  </Typography>
  <MenuBar {...args} />
</>

Submenus

const args: Partial<MenuProps> = {
  align: 'start',
  defaultOpen: false,
  marginThreshold: 2,
  elevation: 8,
  layer: 'modal',
  color: 'primary',
  size: 'md',
}

<BranchMenu {...args} />

Nested Submenus

const args: Partial<MenuProps> = {
  align: 'start',
  defaultOpen: false,
  marginThreshold: 2,
  elevation: 8,
  layer: 'modal',
  color: 'primary',
  size: 'md',
}

<Menu {...args} trigger={<Button variant="text">Insert</Button>}>
  <MenuItem
    submenu={[
      <MenuItem
        key="text"
        submenu={[
          <MenuItem key="h1">Heading</MenuItem>,
          <MenuItem key="p">Paragraph</MenuItem>,
        ]}
      >
        Text
      </MenuItem>,
      <MenuItem
        key="media"
        submenu={[<MenuItem key="img">Image</MenuItem>, <MenuItem key="vid">Video</MenuItem>]}
      >
        Media
      </MenuItem>,
    ]}
  >
    Element
  </MenuItem>
  <MenuItem>Component...</MenuItem>
</Menu>

Default

const args: Partial<MenuListProps> = {
  'aria-label': 'Branch',
  shouldWrap: true,
  shouldAutoFocus: false,
  color: 'primary',
  size: 'md',
}

<MenuList {...args} />

Icons And Shortcuts

const args: Partial<MenuListProps> = {
  'aria-label': 'Branch',
  shouldWrap: true,
  shouldAutoFocus: false,
  color: 'primary',
  size: 'md',
}

<MenuList {...args} aria-label="File">
  <MenuItem startIcon={<Icon name="folder" size="sm" color="inherit" />} shortcut="Control+O">
    Open...
  </MenuItem>
  <MenuItem startIcon={<Icon name="refresh" size="sm" color="inherit" />} shortcut="F5">
    Reload
  </MenuItem>
  <MenuItem shortcut="Control+W">Close</MenuItem>
  <MenuDivider />
  <MenuItem startIcon={<Icon name="terminal" size="sm" color="inherit" />}>
    Open terminal
  </MenuItem>
</MenuList>

Radio And Checkbox Rows

const args: Partial<MenuListProps> = {
  'aria-label': 'Branch',
  shouldWrap: true,
  shouldAutoFocus: false,
  color: 'primary',
  size: 'md',
}

<CheckableRows {...args} />

Type Ahead

Focus a row and type a label's first letters.
const args: Partial<MenuListProps> = {
  'aria-label': 'Branch',
  shouldWrap: true,
  shouldAutoFocus: false,
  color: 'primary',
  size: 'md',
}

<>
  <Typography variant="caption" px={2} pt={1}>
    Focus a row and type a label&apos;s first letters.
  </Typography>
  <MenuList {...args} aria-label="Edit" shouldAutoFocus>
    {['Undo', 'Redo', 'Cut', 'Copy', 'Paste', 'Rename', 'Run'].map((label) => (
      <MenuItem key={label}>{label}</MenuItem>
    ))}
  </MenuList>
</>

Long List

const args: Partial<MenuListProps> = {
  'aria-label': 'Branch',
  shouldWrap: true,
  shouldAutoFocus: false,
  color: 'primary',
  size: 'md',
  maxHeight: '200px',
}

<MenuList {...args} aria-label="Remote branches" overflowY="auto">
  {Array.from({ length: 20 }, (_, index) => (
    <MenuItem key={index} value={index}>
      origin/feature-{index + 1}
    </MenuItem>
  ))}
</MenuList>

Row Actions

const args: Partial<MenuListProps> = {
  'aria-label': 'Branch',
  shouldWrap: true,
  shouldAutoFocus: false,
  color: 'primary',
  size: 'md',
}

<OpenFiles {...args} />

Row Roles

<MenuList aria-label="Roles">
  {menuItemRoleTokens.map((role) => (
    <MenuItem key={role} role={role} selected>
      {role}
    </MenuItem>
  ))}
</MenuList>

ContextMenu

Default

Right-click here, or focus and press Shift+F10

const args: Partial<ContextMenuProps> = {
  marginThreshold: 2,
  elevation: 8,
  layer: 'modal',
  color: 'primary',
  size: 'md',
  'aria-label': 'Canvas',
}

<ContextMenu {...args} target={canvas} />

Opened From Code

const args: Partial<ContextMenuProps> = {
  marginThreshold: 2,
  elevation: 8,
  layer: 'modal',
  color: 'primary',
  size: 'md',
  'aria-label': 'Canvas',
}

<OpenedFromCodeDemo {...args} />

Rows From Data

Chosen: nothing yet

Right-click here, or focus and press Shift+F10

const args: Partial<ContextMenuProps> = {
  marginThreshold: 2,
  elevation: 8,
  layer: 'modal',
  color: 'primary',
  size: 'md',
  'aria-label': 'Canvas',
}

<FromData {...args} />

Default

<MenuDivider />

Default

<MenuItems
  items={[{ value: 'new', label: 'New branch...', shortcut: 'Control+B' }, { value: 'checkout', label: 'Checkout...', disabled: true, note: 'Coming later' }, { type: 'divider' }, { label: 'Local branches', submenu: [{ value: 'main', label: 'main', role: 'menuitemradio', selected: true }, { value: 'feat', label: 'feat/menu', role: 'menuitemradio' }] }, { label: 'Remote branches', submenu: [], emptyLabel: 'Coming soon' }]}
/>