1import React, { ReactElement, useCallback, useMemo, useState, useRef, useImperativeHandle } from 'react';
2import { css, cx } from '@emotion/css';
3import { GrafanaTheme2, LinkTarget } from '@grafana/data';
4import { useStyles2 } from '../../themes';
5import { Icon } from '../Icon/Icon';
6import { IconName } from '../../types';
7import { SubMenu } from './SubMenu';
8import { getFocusStyles } from '../../themes/mixins';
9
10/** @internal */
11export type MenuItemElement = HTMLAnchorElement & HTMLButtonElement & HTMLDivElement;
12
13/** @internal */
14export interface MenuItemProps<T = any> {
15  /** Label of the menu item */
16  label: string;
17  /** Aria label for accessibility support */
18  ariaLabel?: string;
19  /** Aria checked for accessibility support */
20  ariaChecked?: boolean;
21  /** Target of the menu item (i.e. new window)  */
22  target?: LinkTarget;
23  /** Icon of the menu item */
24  icon?: IconName;
25  /** Role of the menu item */
26  role?: string;
27  /** Url of the menu item */
28  url?: string;
29  /** Handler for the click behaviour */
30  onClick?: (event?: React.SyntheticEvent<HTMLElement>, payload?: T) => void;
31  /** Custom MenuItem styles*/
32  className?: string;
33  /** Active */
34  active?: boolean;
35
36  tabIndex?: number;
37
38  /** List of menu items for the subMenu */
39  childItems?: Array<ReactElement<MenuItemProps>>;
40}
41
42/** @internal */
43export const MenuItem = React.memo(
44  React.forwardRef<MenuItemElement, MenuItemProps>((props, ref) => {
45    const {
46      url,
47      icon,
48      label,
49      ariaLabel,
50      ariaChecked,
51      target,
52      onClick,
53      className,
54      active,
55      childItems,
56      role = 'menuitem',
57      tabIndex = -1,
58    } = props;
59    const styles = useStyles2(getStyles);
60    const [isActive, setIsActive] = useState(active);
61    const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
62    const [openedWithArrow, setOpenedWithArrow] = useState(false);
63    const onMouseEnter = useCallback(() => {
64      setIsSubMenuOpen(true);
65      setIsActive(true);
66    }, []);
67    const onMouseLeave = useCallback(() => {
68      setIsSubMenuOpen(false);
69      setIsActive(false);
70    }, []);
71    const hasSubMenu = useMemo(() => childItems && childItems.length > 0, [childItems]);
72    const Wrapper = hasSubMenu ? 'div' : url === undefined ? 'button' : 'a';
73    const itemStyle = cx(
74      {
75        [styles.item]: true,
76        [styles.activeItem]: isActive,
77      },
78      className
79    );
80
81    const localRef = useRef<MenuItemElement>(null);
82    useImperativeHandle(ref, () => localRef.current!);
83
84    const handleKeys = (event: React.KeyboardEvent) => {
85      switch (event.key) {
86        case 'ArrowRight':
87          event.preventDefault();
88          event.stopPropagation();
89          if (hasSubMenu) {
90            setIsSubMenuOpen(true);
91            setOpenedWithArrow(true);
92            setIsActive(true);
93          }
94          break;
95        default:
96          break;
97      }
98    };
99
100    const closeSubMenu = () => {
101      setIsSubMenuOpen(false);
102      setIsActive(false);
103      localRef?.current?.focus();
104    };
105
106    return (
107      <Wrapper
108        target={target}
109        className={itemStyle}
110        rel={target === '_blank' ? 'noopener noreferrer' : undefined}
111        href={url}
112        onClick={
113          onClick
114            ? (event) => {
115                if (!(event.ctrlKey || event.metaKey || event.shiftKey) && onClick) {
116                  event.preventDefault();
117                  event.stopPropagation();
118                  onClick(event);
119                }
120              }
121            : undefined
122        }
123        onMouseEnter={onMouseEnter}
124        onMouseLeave={onMouseLeave}
125        onKeyDown={handleKeys}
126        role={url === undefined ? role : undefined}
127        data-role="menuitem" // used to identify menuitem in Menu.tsx
128        ref={localRef}
129        aria-label={ariaLabel}
130        aria-checked={ariaChecked}
131        tabIndex={tabIndex}
132      >
133        {icon && <Icon name={icon} className={styles.icon} aria-hidden />}
134        {label}
135        {hasSubMenu && (
136          <SubMenu
137            items={childItems}
138            isOpen={isSubMenuOpen}
139            openedWithArrow={openedWithArrow}
140            setOpenedWithArrow={setOpenedWithArrow}
141            close={closeSubMenu}
142          />
143        )}
144      </Wrapper>
145    );
146  })
147);
148MenuItem.displayName = 'MenuItem';
149
150/** @internal */
151const getStyles = (theme: GrafanaTheme2) => {
152  return {
153    item: css`
154      background: none;
155      cursor: pointer;
156      white-space: nowrap;
157      color: ${theme.colors.text.primary};
158      display: flex;
159      padding: 5px 12px 5px 10px;
160      margin: 0;
161      border: none;
162      width: 100%;
163      position: relative;
164
165      &:hover,
166      &:focus,
167      &:focus-visible {
168        background: ${theme.colors.action.hover};
169        color: ${theme.colors.text.primary};
170        text-decoration: none;
171      }
172
173      &:focus-visible {
174        ${getFocusStyles(theme)}
175      }
176    `,
177    activeItem: css`
178      background: ${theme.colors.action.selected};
179    `,
180    icon: css`
181      opacity: 0.7;
182      margin-right: 10px;
183      color: ${theme.colors.text.secondary};
184    `,
185  };
186};
187