{/* ArrowLeft steps back out to the parent row — Escape would close the whole stack, and a
submenu you can only leave by dismissing everything is not a submenu. Focus returns to
the trigger via the surface's own returnFocus. */}
{
if (e.key === "ArrowLeft") {
e.preventDefault();
e.stopPropagation();
setOpen(false);
}
}}
>
>
);
}
export default function ActionMenu({
items,
ariaLabel,
className,
trigger,
}: {
items: readonly ActionMenuItem[];
/** Accessible name for the trigger (e.g. "Catalog actions"). */
ariaLabel: string;
/** Trigger className override; omit for the default glass ⋯ icon button. */
className?: string;
/** Custom trigger content; omit for the default ⋯ icon. */
trigger?: ReactNode;
}) {
const [open, setOpen] = useState(false);
// A disabled item (a mutation already in flight) is skipped by roving keyboard nav automatically:
// PopoverItem renders it as a native `disabled` button, which useListNavigation reads and never
// focuses. No explicit disabledIndices needed. See ActionMenu.dom.test.tsx for the guard.
const pop = useAnchoredPopover({ open, onOpenChange: setOpen, placement: "bottom-end" });
const { setReference } = pop.refs;
return (
<>
{/* A caller-shaped trigger (a labelled pill) is a surface; the default "…" is a glyph. */}
{className ? (