/* Copyright 2023 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ import classnames from "classnames"; import React, { type ComponentPropsWithoutRef, type ComponentType, type ElementType, isValidElement, type ReactElement, type SVGAttributes, useCallback, useContext, type MouseEventHandler, } from "react"; import styles from "./MenuItem.module.css"; import { Text } from "../Typography/Text"; import ChevronRightIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-right"; import { MenuContext } from "./MenuContext"; import { Slot } from "@radix-ui/react-slot"; type MenuItemElement = "button" | "a" | "div"; type Props = { /** * The element type of this menu item. * @default button */ as?: C; /** * The CSS class name. */ className?: string; /** * The icon to show on this menu item. * When `Icon` is a ReactElement, it should spread the props */ Icon?: ComponentType> | ReactElement; /** * The label to show on this menu item. */ // This prop is required because it's rare to not want a label label: string | null; /** * Additional properties to pass to the Text label component. */ labelProps?: ComponentPropsWithoutRef; /** * Event callback for when the item is selected via mouse, touch, or keyboard. * Calling event.preventDefault in this handler will prevent the menu from * being dismissed. */ // This prop is required because it's rare to not want a selection handler onSelect: ((e: Event) => void) | null; /** * Event callback for when the item is clicked. * @param e */ onClick?: MouseEventHandler; /** * The color variant of the menu item. * @default primary */ kind?: "primary" | "critical"; disabled?: boolean; /** * Whether to hide the chevron navigation hint. */ hideChevron?: boolean; } & Omit, "onSelect" | "onClick">; /** * An item within a menu, acting either as a navigation button, or simply a * container for other interactive elements. * Must be used within a compound Menu or other `menu` or `menubar` aria role subtree. */ export const MenuItem = ({ as, className, Icon, label, labelProps, onSelect, kind = "primary", children, onClick: onClickProp, disabled, hideChevron, ...props }: Props): React.ReactElement => { const Component = as ?? ("button" as ElementType); const context = useContext(MenuContext); const onClick = useCallback( (e: Parameters>[0]) => { (onClickProp as ((e_: typeof e) => void) | undefined)?.(e); // If there is no wrapper component to automatically handle onSelect, we // need to handle it manually, dismissing the menu as the default action if (onSelect !== null && context?.MenuItemWrapper == null) { const selectEvent = new CustomEvent("menu.itemSelect", { bubbles: true, cancelable: true, }); onSelect(selectEvent); if (!selectEvent.defaultPrevented) context?.onOpenChange(false); } }, [context, onSelect], ); const iconIsReactElement = isValidElement(Icon); const componentIcon = Icon as ReactElement; const SvgIcon = Icon as ComponentType>; const content = ( {Icon && (iconIsReactElement ? ( {componentIcon} ) : ( ))} {label !== null && ( {label} )} {/* We use CSS to swap between this navigation hint and the provided children on hover - see the styles module. */} {!hideChevron && (Component === "button" || Component === "a") && ( )} {children} ); return context?.MenuItemWrapper == null || onSelect === null ? ( content ) : ( {content} ); };