import { css, cx } from '@emotion/css'; import React, { ReactElement, useCallback, useState, useRef, useImperativeHandle } from 'react'; import { GrafanaTheme2, LinkTarget } from '@grafana/data'; import { useStyles2 } from '../../themes'; import { getFocusStyles } from '../../themes/mixins'; import { IconName } from '../../types'; import { Icon } from '../Icon/Icon'; import { SubMenu } from './SubMenu'; /** @internal */ export type MenuItemElement = HTMLAnchorElement & HTMLButtonElement & HTMLDivElement; /** @internal */ export interface MenuItemProps { /** Label of the menu item */ label: string; /** Aria label for accessibility support */ ariaLabel?: string; /** Aria checked for accessibility support */ ariaChecked?: boolean; /** Target of the menu item (i.e. new window) */ target?: LinkTarget; /** Icon of the menu item */ icon?: IconName; /** Role of the menu item */ role?: string; /** Url of the menu item */ url?: string; /** Handler for the click behaviour */ onClick?: (event?: React.MouseEvent, payload?: T) => void; /** Custom MenuItem styles*/ className?: string; /** Active */ active?: boolean; /** Show in destructive style (error color) */ destructive?: boolean; tabIndex?: number; /** List of menu items for the subMenu */ childItems?: Array>; } /** @internal */ export const MenuItem = React.memo( React.forwardRef((props, ref) => { const { url, icon, label, ariaLabel, ariaChecked, target, onClick, className, active, destructive, childItems, role = 'menuitem', tabIndex = -1, } = props; const styles = useStyles2(getStyles); const [isActive, setIsActive] = useState(active); const [isSubMenuOpen, setIsSubMenuOpen] = useState(false); const [openedWithArrow, setOpenedWithArrow] = useState(false); const onMouseEnter = useCallback(() => { setIsSubMenuOpen(true); setIsActive(true); }, []); const onMouseLeave = useCallback(() => { setIsSubMenuOpen(false); setIsActive(false); }, []); const hasSubMenu = childItems && childItems.length > 0; const ItemElement = hasSubMenu ? 'div' : url === undefined ? 'button' : 'a'; const itemStyle = cx( { [styles.item]: true, [styles.active]: isActive, [styles.destructive]: destructive, }, className ); const localRef = useRef(null); useImperativeHandle(ref, () => localRef.current!); const handleKeys = (event: React.KeyboardEvent) => { switch (event.key) { case 'ArrowRight': event.preventDefault(); event.stopPropagation(); if (hasSubMenu) { setIsSubMenuOpen(true); setOpenedWithArrow(true); setIsActive(true); } break; default: break; } }; const closeSubMenu = () => { setIsSubMenuOpen(false); setIsActive(false); localRef?.current?.focus(); }; return ( <> {icon && } {label} {hasSubMenu && ( )} ); }) ); MenuItem.displayName = 'MenuItem'; const getStyles = (theme: GrafanaTheme2) => { return { item: css` background: none; cursor: pointer; white-space: nowrap; color: ${theme.colors.text.primary}; display: flex; align-items: center; padding: ${theme.spacing(0.5, 2)}; min-height: ${theme.spacing(4)}; margin: 0; border: none; width: 100%; position: relative; &:hover, &:focus, &:focus-visible { background: ${theme.colors.action.hover}; color: ${theme.colors.text.primary}; text-decoration: none; } &:focus-visible { ${getFocusStyles(theme)} } `, active: css` background: ${theme.colors.action.hover}; `, destructive: css` color: ${theme.colors.error.text}; svg { color: ${theme.colors.error.text}; } &:hover, &:focus, &:focus-visible { background: ${theme.colors.error.main}; color: ${theme.colors.error.contrastText}; svg { color: ${theme.colors.error.contrastText}; } } `, icon: css` opacity: 0.7; margin-right: 10px; margin-left: -4px; color: ${theme.colors.text.secondary}; `, }; };