Skip to content
Snippets Groups Projects
TableDropDown.js 7.31 KiB
Newer Older
chris's avatar
chris committed
/* eslint react/prop-types: 0 */
chris's avatar
chris committed
import React, {
  useMemo,
  useContext,
  useState,
  useEffect,
  useRef,
  createRef,
} from 'react';
chris's avatar
chris committed
import styled from 'styled-components';
chris's avatar
chris committed
import { useTranslation } from 'react-i18next';
chris's avatar
chris committed
import { isEmpty } from 'lodash';
chris's avatar
chris committed
import { WaxContext, Icon, useOnClickOutside } from 'wax-prosemirror-core';
chris's avatar
chris committed
import * as tablesFn from '../tableSrc';
chris's avatar
chris committed

chris's avatar
chris committed
const Wrapper = styled.div`
chris's avatar
chris committed
  opacity: ${props => (props.disabled ? '0.4' : '1')};
chris's avatar
chris committed
  display: flex;
`;

const ButtonWrapper = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
chris's avatar
chris committed
`;
chris's avatar
chris committed

chris's avatar
chris committed
const DropDownButton = styled.button`
chris's avatar
chris committed
  background: #fff;
  border: none;
chris's avatar
chris committed
  color: #000;
chris's avatar
chris committed
  cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
chris's avatar
chris committed
  display: flex;
chris's avatar
chris committed
  width: 160px;
chris's avatar
chris committed

  span {
    position: relative;
    top: 2px;
  }
chris's avatar
chris committed
`;

const DropDownMenu = styled.div`
chris's avatar
chris committed
  visibility: ${props => (props.isOpen ? 'visible' : 'hidden')};
chris's avatar
chris committed
  background: #fff;
  display: flex;
  flex-direction: column;
  border: 1px solid #ddd;
  border-radius: 0.25rem;
  box-shadow: 0 0.2rem 0.4rem rgb(0 0 0 / 10%);
chris's avatar
chris committed
  margin: 29px auto auto;
chris's avatar
chris committed
  position: absolute;
chris's avatar
chris committed
  width: 170px;
chris's avatar
chris committed
  max-height: 180px;
chris's avatar
chris committed
  overflow-y: scroll;
chris's avatar
chris committed
  z-index: 2;

  span {
    cursor: pointer;
chris's avatar
chris committed
    padding: 8px 10px;
chris's avatar
chris committed
  }

chris's avatar
chris committed
  span:focus,
  span:hover {
chris's avatar
chris committed
    background: #f2f9fc;
    outline: 2px solid #f2f9fc;
  }
chris's avatar
chris committed
`;

chris's avatar
chris committed
const StyledIcon = styled(Icon)`
  height: 18px;
  width: 18px;
  margin-left: auto;
`;

chris's avatar
chris committed
const TableDropDown = ({ item }) => {
chris's avatar
chris committed
  const { t, i18n } = useTranslation();
chris's avatar
chris committed
  const Translation = ({ label, defaultTrans }) => {
    return (
      <>{!isEmpty(i18n) && i18n.exists(label) ? t(label) : defaultTrans}</>
    );
  };
chris's avatar
chris committed

chris's avatar
chris committed
  const dropDownOptions = [
chris's avatar
chris committed
    {
      label: (
        <Translation
          defaultTrans="Delete table"
          label="Wax.Tables.Delete table"
        />
      ),
      value: 'deleteTable',
    },
    {
      label: (
        <Translation
          defaultTrans="Add Caption"
          label="Wax.Tables.Add Caption"
        />
      ),
      value: 'addCaption',
    },
    {
      label: (
        <Translation
          defaultTrans="Delete Caption"
          label="Wax.Tables.Delete Caption"
        />
      ),
      value: 'deleteCaption',
    },
    {
      label: (
        <Translation
          defaultTrans="Add column before"
          label="Wax.Tables.Add column before"
        />
      ),
      value: 'addColumnBefore',
    },
    {
      label: (
        <Translation
          defaultTrans="Add column after"
          label="Wax.Tables.Add column after"
        />
      ),
      value: 'addColumnAfter',
    },
    {
      label: (
        <Translation
          defaultTrans="Delete column"
          label="Wax.Tables.Delete column"
        />
      ),
      value: 'deleteColumn',
    },
    {
      label: (
        <Translation
          defaultTrans="Insert row before"
          label="Wax.Tables.Insert row before"
        />
      ),
      value: 'addRowBefore',
    },
    {
      label: (
        <Translation
          defaultTrans="Insert row after"
          label="Wax.Tables.Insert row after"
        />
      ),
      value: 'addRowAfter',
    },
    {
      label: (
        <Translation defaultTrans="Delete row" label="Wax.Tables.Delete row" />
      ),
      value: 'deleteRow',
    },
    {
      label: (
        <Translation
          defaultTrans="Merge cells"
          label="Wax.Tables.Merge cells"
        />
      ),
      value: 'mergeCells',
    },
    {
      label: (
        <Translation defaultTrans="Split cell" label="Wax.Tables.Split cell" />
      ),
      value: 'splitCell',
    },
    {
      label: (
        <Translation
          defaultTrans="Toggle header column"
          label="Wax.Tables.Toggle header column"
        />
      ),
      value: 'toggleHeaderColumn',
    },
    {
      label: (
        <Translation
          defaultTrans="Toggle header row"
          label="Wax.Tables.Toggle header row"
        />
      ),
      value: 'toggleHeaderRow',
    },
    {
      label: (
        <Translation
          defaultTrans="Toggle header cells"
          label="Wax.Tables.Toggle header cells"
        />
      ),
      value: 'toggleHeaderCell',
    },
chris's avatar
chris committed
  ];

chris's avatar
chris committed
  const { activeView } = useContext(WaxContext);
chris's avatar
chris committed
  const itemRefs = useRef([]);
chris's avatar
chris committed
  const wrapperRef = useRef();
chris's avatar
chris committed
  const [isOpen, setIsOpen] = useState(false);
  const isDisabled = !item.select(activeView.state);
chris's avatar
chris committed

chris's avatar
chris committed
  useOnClickOutside(wrapperRef, () => setIsOpen(false));

chris's avatar
chris committed
  useEffect(() => {
    if (isDisabled) setIsOpen(false);
  }, [isDisabled]);
chris's avatar
chris committed

chris's avatar
chris committed
  const openCloseMenu = () => {
chris's avatar
chris committed
    if (!isDisabled) setIsOpen(!isOpen);
chris's avatar
chris committed
    if (isOpen)
      setTimeout(() => {
        activeView.focus();
      });
chris's avatar
chris committed
  };

chris's avatar
chris committed
  const onKeyDown = (e, index) => {
chris's avatar
chris committed
    e.preventDefault();
chris's avatar
chris committed
    // arrow down
    if (e.keyCode === 40) {
      if (index === itemRefs.current.length - 1) {
        itemRefs.current[0].current.focus();
      } else {
        itemRefs.current[index + 1].current.focus();
      }
    }

    // arrow up
    if (e.keyCode === 38) {
      if (index === 0) {
        itemRefs.current[itemRefs.current.length - 1].current.focus();
      } else {
        itemRefs.current[index - 1].current.focus();
      }
    }

    // enter
    if (e.keyCode === 13) {
      itemRefs.current[index].current.click();
    }

    // ESC
    if (e.keyCode === 27) {
chris's avatar
chris committed
      setIsOpen(false);
chris's avatar
chris committed
    }
  };
chris's avatar
chris committed

chris's avatar
chris committed
  const TableDropDownComponent = useMemo(
    () => (
chris's avatar
chris committed
      <Wrapper disabled={isDisabled} ref={wrapperRef}>
chris's avatar
chris committed
        <ButtonWrapper>
          <DropDownButton
            aria-controls="table-options"
            aria-expanded={isOpen}
            aria-haspopup
            disabled={isDisabled}
            onKeyDown={e => {
              if (e.keyCode === 40) {
                itemRefs.current[0].current.focus();
              }
              if (e.keyCode === 27) {
                setIsOpen(false);
              }
              if (e.keyCode === 13 || e.keyCode === 32) {
                setIsOpen(true);
              }
            }}
            onMouseDown={openCloseMenu}
            type="button"
          >
            <span>
              {!isEmpty(i18n) && i18n.exists('Wax.Tables.Table Options')
                ? t('Wax.Tables.Table Options')
                : 'Table Options'}
            </span>
            <StyledIcon name="expand" />
          </DropDownButton>
        </ButtonWrapper>
chris's avatar
chris committed
        <DropDownMenu
          aria-label="Choose a table action"
          id="table-options"
          isOpen={isOpen}
          role="menu"
        >
chris's avatar
chris committed
          {dropDownOptions.map((option, index) => {
chris's avatar
chris committed
            itemRefs.current[index] = itemRefs.current[index] || createRef();
chris's avatar
chris committed
            return (
              <span
                key={option.value}
chris's avatar
chris committed
                onClick={() => {
chris's avatar
chris committed
                  item.run(
                    activeView.state,
                    activeView.dispatch,
                    tablesFn[option.value],
                  );

chris's avatar
chris committed
                  openCloseMenu();
chris's avatar
chris committed
                }}
                onKeyDown={e => onKeyDown(e, index)}
                ref={itemRefs.current[index]}
                role="menuitem"
                tabIndex="-1"
              >
                {option.label}
              </span>
            );
          })}
        </DropDownMenu>
chris's avatar
chris committed
      </Wrapper>
chris's avatar
chris committed
    ),
chris's avatar
chris committed
    [isDisabled, isOpen, t('Wax.Tables.Table Options')],
chris's avatar
chris committed

  return TableDropDownComponent;
chris's avatar
chris committed

chris's avatar
chris committed
export default TableDropDown;