import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import { IconButton, MenuItem, Stack, Typography, type IconButtonProps, type PaperProps, } from "@mui/material"; import Menu, { type MenuProps } from "@mui/material/Menu"; import React, { createContext, useContext, useMemo, useState } from "react"; interface OverflowMenuContextT { close: () => void; } const OverflowMenuContext = createContext( undefined, ); interface OverflowMenuProps { /** * An ARIA identifier for the overflow menu when it is displayed. */ ariaID: string; /** * The icon for the trigger button. * * If not provided, then by default the MoreHoriz icon from MUI is used. */ triggerButtonIcon?: React.ReactNode; /** * Optional additional properties for the trigger icon button. */ triggerButtonSxProps?: IconButtonProps["sx"]; /** * Optional additional sx props for the MUI {@link Paper} that underlies the * {@link Menu}. */ menuPaperSxProps?: PaperProps["sx"]; } /** * An overflow menu showing {@link OverflowMenuOptions}, alongwith a button to * trigger the visibility of the menu. */ export const OverflowMenu: React.FC< React.PropsWithChildren > = ({ ariaID, triggerButtonIcon, triggerButtonSxProps, menuPaperSxProps, children, }) => { const [anchorEl, setAnchorEl] = useState(); const context = useMemo( () => ({ close: () => setAnchorEl(undefined) }), [], ); return ( setAnchorEl(event.currentTarget)} aria-controls={anchorEl ? ariaID : undefined} aria-haspopup="true" aria-expanded={anchorEl ? "true" : undefined} sx={triggerButtonSxProps} > {triggerButtonIcon ?? } setAnchorEl(undefined)} MenuListProps={{ // Disable padding at the top and bottom of the menu list. disablePadding: true, "aria-labelledby": ariaID, }} slotProps={{ paper: { sx: menuPaperSxProps } }} anchorOrigin={{ vertical: "bottom", horizontal: "right" }} transformOrigin={{ vertical: "top", horizontal: "right" }} > {children} ); }; interface OverflowMenuOptionProps { /** * Called when the menu option is clicked. */ onClick: () => void; /** * The color of the text and icons. * * Default: "primary". */ color?: "primary" | "critical"; /** * An optional icon to show at the leading edge of the menu option. */ startIcon?: React.ReactNode; /** * An optional icon to show at the trailing edge of the menu option. */ endIcon?: React.ReactNode; } /** * Individual options meant to be shown inside an {@link OverflowMenu}. */ export const OverflowMenuOption: React.FC< React.PropsWithChildren > = ({ onClick, color = "primary", startIcon, endIcon, children }) => { const menuContext = useContext(OverflowMenuContext)!; const handleClick = () => { onClick(); menuContext.close(); }; return ( ({ minWidth: 220, color: theme.vars.palette[color].main, // Reduce the size of the icons a bit to make it fit better with // the text. "& .MuiSvgIcon-root": { fontSize: "20px", }, })} > {startIcon} {children} {endIcon} ); };