ActionsMenu
Collapses per-record actions into a ⋮ menu, so a row or card reads as its
record rather than as its controls.
import { ActionsMenu } from "@evolve-storefront/ui/actions-menu";Design notes
Actions are data, not children, and that is not a style preference. A menu item unmounts when the menu closes on select — so an item wrapping its own dialog trigger takes that trigger with it, and the dialog never opens. An action therefore sets state, and the surface renders its dialog outside the menu. Every consumer meets this, and the failure does not point at its cause, which is why it is worth a shared component rather than a documented pattern.
Reaching one of these from CSS: the trigger is a button and carries no name
of its own, because a button's identity is invariant. Compose the row's own name
with the trigger's role — the menu is portalled, so the trigger is a sibling
of it and aria-haspopup is what distinguishes it from other buttons in the row:
[data-component="associate-row"] [data-component="button"][aria-haspopup] {
--control-height: 2rem;
}
Preview
Usage
<ActionsMenu label={t("rowActions")} actions={[ { id: "rename", label: t("rename"), onSelect: onRename }, { id: "duplicate", label: t("duplicate"), onSelect: onDuplicate }, { id: "delete", label: t("delete"), destructive: true, onSelect: onDelete }, ]}/>;Props
| Prop | Type | Description |
|---|---|---|
actions | MenuAction[] | The actions, as data rather than children — see the note on the component. Renders nothing when empty, so a caller can pass a filtered list without guarding the menu itself. |
label | string | Accessible name for the trigger. Required, and deliberately not defaulted:
|
variant? | ButtonVariant | |
tone? | ButtonTone | |
data-component? | undefined | Not a caller's to rename — |
placement? | DropdownMenuPlacement | Where the menu opens. Defaults to |
Any other props go to the root element, like on a plain HTML element.
Examples
No actionsAn empty list renders nothing, so a caller can filter without guarding.
// Filter freely: an empty list renders nothing, so no guard is needed.<ActionsMenu label={t("rowActions")} actions={actions.filter((action) => !action.disabled)} />;With a confirm dialogThe reason actions are data.
const [confirming, setConfirming] = useState(false);
<ActionsMenu label={t("rowActions")} actions={[ { id: "rename", label: t("rename"), onSelect: onRename }, { id: "delete", label: t("delete"), destructive: true, onSelect: () => setConfirming(true) }, ]}/>{/* outside the menu, or it unmounts with the item that opened it */}<AlertDialog open={confirming} onOpenChange={setConfirming}>…</AlertDialog>
