hadmean/hadmean

View on GitHub
src/frontend/design-system/components/DropdownMenu/index.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
B
89%
import Dropdown from "react-bootstrap/Dropdown";
import styled from "styled-components";
import { useState, useEffect, useMemo } from "react";
import { Loader, MoreVertical } from "react-feather";
import { USE_ROOT_COLOR } from "frontend/design-system/theme/root";
import { Stack } from "frontend/design-system/primitives/Stack";
import { Typo } from "frontend/design-system/primitives/Typo";
import { Z_INDEXES } from "frontend/design-system/constants/zIndex";
import { SystemIcon } from "frontend/design-system/Icons/System";
import { useRouter } from "next/router";
import { useToggle } from "frontend/hooks/state/useToggleState";
import { useLingui } from "@lingui/react";
import { SoftButtonStyled } from "../Button/Button";
import { BREAKPOINTS } from "../../constants";
import { Spin } from "../_/Spin";
import { SHADOW_CSS } from "../Card";
import { IGroupActionButton } from "../Button/types";
import { useConfirmAlert } from "../ConfirmAlert";

export interface IDropDownMenuItem extends IGroupActionButton {
  description?: string;
}

export interface IProps {
  menuItems: IDropDownMenuItem[];
  ariaLabel: string;
  disabled?: boolean;
  ellipsis?: true;
}

const Label = styled.span`
  text-wrap: nowrap;
  @media (max-width: ${BREAKPOINTS.sm}) {
    display: none;
  }
`;

const DropDownItem = styled.button`
  display: block;
  width: 100%;
  padding: 8px 12px;
  clear: both;
  font-weight: 400;
  cursor: pointer;
  font-size: 16px;
  line-height: 20px;
  color: ${USE_ROOT_COLOR("main-text")};
  text-align: inherit;
  background: ${USE_ROOT_COLOR("base-color")};
  border: 0;
  &:hover {
    background-color: ${USE_ROOT_COLOR("soft-color")};
    color: ${USE_ROOT_COLOR("main-text")};
  }
`;

const DropDownMenuStyled = styled(Dropdown.Menu)`
  ${SHADOW_CSS}
  margin: 0;

  position: absolute;
  top: 100%;
  left: 0;
  z-index: ${Z_INDEXES.dropDown};
  display: none;
  float: left;
  min-width: 10rem;
  margin: 0.125rem 0 0;
  font-size: 0.8125rem;
  color: ${USE_ROOT_COLOR("main-text")};
  text-align: left;
  list-style: none;
  background-color: ${USE_ROOT_COLOR("base-color")};
  background-clip: padding-box;
  border: 1px solid rgba(0, 0, 0, 0.05);
  border-radius: 0.25rem;

  right: 0;
  left: auto;

  &.show {
    display: block;
  }
`;

const SROnly = styled.span`
  border: 0;
  clip: rect(0, 0, 0, 0);
  height: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
  width: 1px;
`;

const EllipsisDropDownToggle = styled(SoftButtonStyled)`
  padding: 2px 4px;
`;

const DropDownToggle = styled(SoftButtonStyled)`
  display: inline-block;
  margin-left: -1px;
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  white-space: nowrap;
  position: relative;
  flex: 1 1 auto;

  &::after {
    display: inline-block;
    vertical-align: middle;
    content: "";
    border-top: 0.3em solid;
    border-right: 0.3em solid transparent;
    border-bottom: 0;
    border-left: 0.3em solid transparent;
  }
`;

const CurrentButton = styled(SoftButtonStyled)`
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
`;
export function DropDownMenu({
  menuItems: menuItems$1,
  disabled,
  ellipsis,
  ariaLabel,
}: IProps) {
  const dropDownMode = useToggle();
  const router = useRouter();

  const { _ } = useLingui();

  const toggleDropDown = () => {
    if (!disabled) {
      dropDownMode.toggle();
    }
  };

  const menuItems = useMemo(() => {
    return [...menuItems$1].sort((a, b) => {
      return (a.order || 0) - (b.order || 0);
    });
  }, [menuItems$1]);

  const [currentMenuItem, setCurrentMenuItem] = useState<IDropDownMenuItem>(
    menuItems[0]
  );

  const confirmAlert = useConfirmAlert();

  const runAction = (actionMenuItem: IDropDownMenuItem) => {
    if (typeof actionMenuItem.action === "string") {
      router.push(actionMenuItem.action);
      return;
    }

    if (actionMenuItem.shouldConfirmAlert) {
      return confirmAlert({
        title: actionMenuItem.shouldConfirmAlert,
        action: actionMenuItem.action,
      });
    }

    actionMenuItem.action();
  };

  useEffect(() => {
    setCurrentMenuItem(menuItems[0]);
  }, [JSON.stringify(menuItems)]);

  if (menuItems.length === 0) {
    return null;
  }

  const onMenuItemClick = (menuIndex: number) => {
    const menuItem = menuItems[menuIndex];
    toggleDropDown();
    runAction(menuItem);
    setCurrentMenuItem(menuItem);
  };

  const { systemIcon, label } = currentMenuItem;

  const currentItem = (
    <Stack $spacing={4} $align="center">
      {currentMenuItem.isMakingRequest ? (
        <Spin as={Loader} size={14} />
      ) : (
        <SystemIcon icon={systemIcon} size={14} />
      )}
      <Label>{_(label)}</Label>
    </Stack>
  );

  if (menuItems.length === 1 && !ellipsis) {
    return (
      <SoftButtonStyled
        size="sm"
        disabled={currentMenuItem.isMakingRequest || disabled}
        onClick={() => runAction(currentMenuItem)}
      >
        {currentItem}
      </SoftButtonStyled>
    );
  }

  return (
    <Dropdown
      as={Stack}
      $spacing={0}
      $width="auto"
      show={dropDownMode.isOn}
      align="end"
      onToggle={toggleDropDown}
    >
      {ellipsis ? (
        <EllipsisDropDownToggle split as={Dropdown.Toggle} size="sm">
          <MoreVertical
            size={16}
            style={{ cursor: "pointer" }}
            aria-label={ariaLabel}
          />
        </EllipsisDropDownToggle>
      ) : (
        <>
          <CurrentButton
            size="sm"
            disabled={disabled || currentMenuItem.isMakingRequest}
            onClick={() => runAction(currentMenuItem)}
            type="button"
          >
            {currentItem}
          </CurrentButton>
          <DropDownToggle split as={Dropdown.Toggle} size="sm">
            <SROnly>Toggle Dropdown</SROnly>
          </DropDownToggle>
        </>
      )}
      <DropDownMenuStyled>
        {menuItems.map((menuItem, index) => (
          <DropDownItem
            key={menuItem.id}
            onClick={() => onMenuItemClick(index)}
            disabled={menuItem.disabled}
            type="button"
          >
            <Stack>
              {currentMenuItem.isMakingRequest ? (
                <Spin as={Loader} size={14} />
              ) : (
                <SystemIcon
                  icon={menuItem.systemIcon}
                  size={14}
                  color={menuItem.disabled ? "muted-text" : "main-text"}
                />
              )}
              <Typo.SM
                as="span"
                $color={menuItem.disabled ? "muted" : undefined}
              >
                {_(menuItem.label)}
              </Typo.SM>
            </Stack>
            {menuItem.description ? (
              <Typo.XS $color="muted" as="span">
                {menuItem.description}
              </Typo.XS>
            ) : null}
          </DropDownItem>
        ))}
      </DropDownMenuStyled>
    </Dropdown>
  );
}

// TODO
// isMakingRequest?: boolean;
// color?: keyof typeof SYSTEM_COLORS;