Context menu
PortedA menu that opens at the pointer on a right-click or a long press, and under the focused element on Shift+F10 or the context menu key. Nothing shows that it is there, so every action in it must also be reachable another way, such as a row's actions button. Contains a ContextMenuTrigger and a ContextMenuContent.
import { CopyIcon, PencilSimpleIcon, TrashIcon } from "@phosphor-icons/react";
import { type CSSProperties } from "react";
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from "@etched/monet/context-menu";
// A dashed area to right-click, for the demos that are not a table.
const area: CSSProperties = {
display: "grid",
placeItems: "center",
maxInlineSize: "22rem",
blockSize: "7rem",
borderRadius: "var(--et-card-radius)",
boxShadow: "inset 0 0 0 1px var(--et-control-border)",
color: "var(--et-text-subtle)",
};
export function Preview() {
return (
<ContextMenu>
<ContextMenuTrigger style={area}>Right-click here</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem icon={<PencilSimpleIcon />}>Rename</ContextMenuItem>
<ContextMenuItem icon={<CopyIcon />}>Copy ID</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem icon={<TrashIcon />} variant="destructive">
Delete
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}Installation
import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuLinkItem, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger } from "@etched/monet/context-menu";Examples
Table rows
Right-click a row, or press Shift+F10 or the context menu key on its link. The actions button opens the same actions, so they can be found.
Last action: none
| Lot | Site | Chips | |
|---|---|---|---|
| Lot L-7712 | TPE-2 | 96 | |
| Lot L-7709 | SJC-1 | 48 | |
| Lot L-7705 | TPE-1 | 96 |
import { ArrowBendUpRightIcon, CopyIcon, DotsThreeIcon, PencilSimpleIcon, TrashIcon } from "@phosphor-icons/react";
import { useState } from "react";
import { Button } from "@etched/monet/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@etched/monet/dropdown";
import { Link } from "@etched/monet/link";
import { Table } from "@etched/monet/table";
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger } from "@etched/monet/context-menu";
interface Lot {
id: string;
site: string;
chips: number;
}
const lots: Lot[] = [
{ id: "L-7712", site: "TPE-2", chips: 96 },
{ id: "L-7709", site: "SJC-1", chips: 48 },
{ id: "L-7705", site: "TPE-1", chips: 96 },
];
const sites = ["SJC-1", "TPE-1", "TPE-2"];
// One list of actions for both menus: the context menu's parts are the dropdown menu's.
function LotActions({ lot, onAction }: { lot: Lot; onAction: (action: string) => void }) {
return (
<>
<ContextMenuItem icon={<PencilSimpleIcon />} onClick={() => onAction("Rename")}>
Rename
</ContextMenuItem>
<ContextMenuItem icon={<CopyIcon />} onClick={() => onAction("Copy ID")}>
Copy ID
<ContextMenuShortcut>⌘C</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger icon={<ArrowBendUpRightIcon />}>Move to site</ContextMenuSubTrigger>
<ContextMenuSubContent>
{sites.map((site) => (
<ContextMenuItem key={site} disabled={site === lot.site} onClick={() => onAction(`Move to ${site}`)}>
{site === lot.site ? `${site} (current)` : site}
</ContextMenuItem>
))}
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem icon={<TrashIcon />} variant="destructive" onClick={() => onAction("Delete")}>
Delete
</ContextMenuItem>
</>
);
}
export function TableRows() {
const [last, setLast] = useState("none");
return (
<>
<p>Last action: {last}</p>
<div className="card">
<Table aria-label="Lots">
<thead>
<tr>
<th>Lot</th>
<th>Site</th>
<th>Chips</th>
{/* The button and the cell's padding. */}
<th aria-label="Actions" style={{ inlineSize: "calc(var(--et-control-sm) + 2 * var(--et-space-4))" }} />
</tr>
</thead>
<tbody>
{lots.map((lot) => {
const act = (action: string) => setLast(`${action}, lot ${lot.id}`);
return (
<ContextMenu key={lot.id}>
<ContextMenuTrigger render={<tr />}>
<th scope="row">
<Link href={`#lot-${lot.id}`}>Lot {lot.id}</Link>
</th>
<td>{lot.site}</td>
<td>{lot.chips}</td>
<td>
<DropdownMenu>
<DropdownMenuTrigger
render={<Button size="sm" shape="square" variant="ghost" aria-label={`Actions for lot ${lot.id}`} icon={<DotsThreeIcon />} />}
/>
<DropdownMenuContent align="end">
<LotActions lot={lot} onAction={act} />
</DropdownMenuContent>
</DropdownMenu>
</td>
</ContextMenuTrigger>
<ContextMenuContent>
<LotActions lot={lot} onAction={act} />
</ContextMenuContent>
</ContextMenu>
);
})}
</tbody>
</Table>
</div>
</>
);
}Checkbox and radio items
The menu stays open while you check items. This area takes focus, so Shift+F10 opens its menu.
import { useState, type CSSProperties } from "react";
import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuTrigger } from "@etched/monet/context-menu";
// A dashed area to right-click, for the demos that are not a table.
const area: CSSProperties = {
display: "grid",
placeItems: "center",
maxInlineSize: "22rem",
blockSize: "7rem",
borderRadius: "var(--et-card-radius)",
boxShadow: "inset 0 0 0 1px var(--et-control-border)",
color: "var(--et-text-subtle)",
};
export function CheckboxAndRadioItems() {
const [grid, setGrid] = useState(true);
const [markers, setMarkers] = useState(false);
const [unit, setUnit] = useState("ns");
return (
<ContextMenu>
<ContextMenuTrigger style={area} tabIndex={0}>
Trace view, in {unit}
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuCheckboxItem checked={grid} onCheckedChange={setGrid}>
Show grid
</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem checked={markers} onCheckedChange={setMarkers}>
Show markers
</ContextMenuCheckboxItem>
<ContextMenuSeparator />
<ContextMenuRadioGroup value={unit} onValueChange={setUnit}>
<ContextMenuLabel>Time unit</ContextMenuLabel>
<ContextMenuRadioItem value="ns">Nanoseconds</ContextMenuRadioItem>
<ContextMenuRadioItem value="µs">Microseconds</ContextMenuRadioItem>
</ContextMenuRadioGroup>
</ContextMenuContent>
</ContextMenu>
);
}Accessibility
Shift+F10or the context menu key on a focused element in the trigger opens the menu under it.ArrowUp/ArrowDownandHome/Endmove;ArrowRightopens a submenu.Enterchooses an item andEscapecloses the menu. Focus goes back to the element that opened it.- A
menuofmenuitem,menuitemcheckboxandmenuitemradioitems, witharia-checkedandaria-disabled. While it is open, the page behind is hidden from screen readers. - Nothing shows that the menu is there. Put every action somewhere else too, such as a row's actions button.
- Give an area with nothing focusable in it
tabIndex={0}, so the keys can open its menu.