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. */ triggerButtonProps?: Partial; /** * Optional additional properties for the MUI {@link Paper} that underlies * the {@link Menu}. */ menuPaperProps?: Partial; } /** * 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, triggerButtonProps, menuPaperProps, 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} {...triggerButtonProps} > {triggerButtonIcon ?? } setAnchorEl(undefined)} MenuListProps={{ // Disable padding at the top and bottom of the menu list. disablePadding: true, "aria-labelledby": ariaID, }} slotProps={{ paper: menuPaperProps }} anchorOrigin={{ vertical: "bottom", horizontal: "right" }} transformOrigin={{ vertical: "top", horizontal: "right" }} > {children} ); }; interface OverflowMenuOptionProps { /** * The color of the text and icons. * * Default: "primary". */ color?: "primary"; /** * 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; /** * Called when the menu option is clicked. */ onClick: () => void; } /** * 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 ( theme.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} ); };