import React, { useRef, forwardRef, useCallback, useState } from 'react'; import { Button, Icon, IconButton, Menu, MenuItem } from './leafygreen'; import { Tooltip } from './tooltip'; import type { TooltipProps } from './tooltip'; import type { ButtonProps } from '@leafygreen-ui/button'; import { spacing } from '@leafygreen-ui/tokens'; import { css, cx } from '@leafygreen-ui/emotion'; export type ItemAction = { action: Action; label: string; icon: React.ReactChild; }; export type GroupedItemAction = ItemAction & { tooltip?: string; tooltipProps?: TooltipProps; }; export type MenuAction = { action: Action; label: string; icon?: React.ReactChild; }; const ItemActionButtonSize = { XSmall: 'xsmall', Small: 'small', Default: 'default', } as const; type ItemActionButtonSize = typeof ItemActionButtonSize[keyof typeof ItemActionButtonSize]; const actionControlsStyle = css({ flex: 'none', marginLeft: 'auto', alignItems: 'center', display: 'flex', }); const actionGroupButtonStyle = css({ '&:not(:first-child)': { marginLeft: spacing[1], }, }); const iconContainerStyle = css({ display: 'block', flex: 'none', fontSize: 0, lineHeight: 0, }); // Using important here because leafygreen / emotion applies styles in the order // that doesn't allow our styles override theirs const buttonSizeStyle: Record = { default: undefined, small: css({ flex: 'none', width: `${spacing[4]}px !important`, height: `${spacing[4]}px !important`, }), xsmall: css({ flex: 'none', // aligns with other xsmall components width: `${20}px !important`, height: `${20}px !important`, }), }; function actionTestId( dataTestId: string | undefined, action: Action ) { return dataTestId ? `${dataTestId}-${action}-action` : undefined; } // As we are using this component to render icon in MenuItem, // and it does cloneElement on glyph, here we are accepting all the // props that are passed during clone process. type IconProps = React.ComponentProps; const ActionGlyph = ({ glyph, size, ...props }: Omit & { glyph?: React.ReactChild; size?: ItemActionButtonSize; }) => { if (typeof glyph === 'string') { return ; } if (React.isValidElement(glyph)) { return glyph; } return null; }; const ItemActionButton = forwardRef< HTMLButtonElement, { glyph: React.ReactChild; label: string; title?: string; size: ItemActionButtonSize; onClick(evt: React.MouseEvent): void; } & Omit, 'size'> >(function IconButtonSmall( { glyph, size, label, onClick, children, title, className, ...rest }, ref ) { return ( {/* Only here to make leafygreen menus work */} {children} ); }); export function ItemActionMenu({ isVisible = true, actions, onAction, className, usePortal, iconClassName, iconStyle, iconSize = ItemActionButtonSize.Default, 'data-testid': dataTestId, }: { actions: MenuAction[]; onAction(actionName: Action): void; className?: string; usePortal?: boolean; iconClassName?: string; iconStyle?: React.CSSProperties; iconSize?: ItemActionButtonSize; isVisible?: boolean; 'data-testid'?: string; }) { // this ref is used by the Menu component to calculate the height and position // of the menu, and by us to give back the focus to the trigger when the menu // is closed (https://jira.mongodb.org/browse/PD-1674). const menuTriggerRef = useRef(null); const [isMenuOpen, setIsMenuOpen] = useState(false); const onClick = useCallback( (evt) => { evt.stopPropagation(); if (evt.currentTarget.dataset.menuitem) { setIsMenuOpen(false); // Workaround for https://jira.mongodb.org/browse/PD-1674 menuTriggerRef.current?.focus(); } onAction(evt.currentTarget.dataset.action); }, [onAction] ); const shouldRender = isMenuOpen || (isVisible && actions.length > 0); if (!shouldRender) { return null; } return (
; children: React.ReactNode; }) => { return ( { evt.stopPropagation(); onClick && onClick(evt); }} className={cx(actionGroupButtonStyle, iconClassName)} style={iconStyle} > {children} ); }} > {actions.map(({ action, label, icon }) => { return ( (dataTestId, action)} data-action={action} data-menuitem={true} glyph={} onClick={onClick} > {label} ); })}
); } export function ItemActionGroup({ actions, onAction, className, iconClassName, iconStyle, iconSize = ItemActionButtonSize.Default, isVisible = true, 'data-testid': dataTestId, }: { actions: GroupedItemAction[]; onAction(actionName: Action): void; className?: string; iconClassName?: string; iconStyle?: React.CSSProperties; iconSize?: ItemActionButtonSize; isVisible?: boolean; 'data-testid'?: string; }) { const onClick = useCallback( (evt) => { evt.stopPropagation(); onAction(evt.currentTarget.dataset.action); }, [onAction] ); const shouldRender = isVisible && actions.length > 0; if (!shouldRender) { return null; } return (
{actions.map(({ action, icon, label, tooltip, tooltipProps }) => { const button = ( (dataTestId, action)} onClick={onClick} className={cx(actionGroupButtonStyle, iconClassName)} style={iconStyle} /> ); if (tooltip) { return ( (
{button} {children}
)} > {tooltip}
); } return button; })}
); } export function ItemActionControls({ isVisible = true, actions, onAction, className, iconClassName, iconStyle, iconSize = ItemActionButtonSize.Default, usePortal, collapseToMenuThreshold = 2, 'data-testid': dataTestId, }: { isVisible?: boolean; actions: ItemAction[]; onAction(actionName: Action): void; className?: string; iconSize?: ItemActionButtonSize; iconClassName?: string; iconStyle?: React.CSSProperties; collapseToMenuThreshold?: number; usePortal?: boolean; 'data-testid'?: string; }) { if (actions.length === 0) { return null; } const shouldShowMenu = actions.length >= collapseToMenuThreshold; if (shouldShowMenu) { return ( ); } return ( ); } export function DropdownMenuButton({ isVisible = true, actions, onAction, usePortal, activeAction, buttonText, buttonProps, iconSize = ItemActionButtonSize.Default, 'data-testid': dataTestId, }: { actions: MenuAction[]; onAction(actionName: Action): void; usePortal?: boolean; iconSize?: ItemActionButtonSize; isVisible?: boolean; activeAction?: Action; 'data-testid'?: string; buttonText: string; buttonProps: ButtonProps; }) { // this ref is used by the Menu component to calculate the height and position // of the menu, and by us to give back the focus to the trigger when the menu // is closed (https://jira.mongodb.org/browse/PD-1674). const menuTriggerRef = useRef(null); const [isMenuOpen, setIsMenuOpen] = useState(false); const onClick = useCallback( (evt) => { evt.stopPropagation(); if (evt.currentTarget.dataset.menuitem) { setIsMenuOpen(false); // Workaround for https://jira.mongodb.org/browse/PD-1674 menuTriggerRef.current?.focus(); } onAction(evt.currentTarget.dataset.action); }, [onAction] ); const shouldRender = isMenuOpen || (isVisible && actions.length > 0); if (!shouldRender) { return null; } return ( ; children: React.ReactNode; }) => { return ( ); }} > {actions.map(({ action, label, icon }) => { return ( (dataTestId, action)} data-action={action} data-menuitem={true} glyph={} onClick={onClick} > {label} ); })} ); }