qlik-oss/sn-table

View on GitHub
src/table/components/head/HeadCellMenu.tsx

Summary

Maintainability
A
55 mins
Test Coverage
B
89%
import ClearSelections from "@qlik-trial/sprout/icons/react/ClearSelections";
import ColumnSize from "@qlik-trial/sprout/icons/react/ColumnSize";
import More from "@qlik-trial/sprout/icons/react/More";
import Search from "@qlik-trial/sprout/icons/react/Search";
import SelectAll from "@qlik-trial/sprout/icons/react/SelectAll";
import SelectAlternative from "@qlik-trial/sprout/icons/react/SelectAlternative";
import SelectExcluded from "@qlik-trial/sprout/icons/react/SelectExcluded";
import SelectPossible from "@qlik-trial/sprout/icons/react/SelectPossible";
import Selection from "@qlik-trial/sprout/icons/react/Selection";
import { preventDefaultBehavior } from "@qlik/nebula-table-utils/lib/utils";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { TableLayout } from "../../../types";
import { TableContext, useContextSelector } from "../../context";
import useFieldSelection from "../../hooks/use-field-selection";
import { DEFAULT_FONT_SIZE } from "../../styling-defaults";
import { HeadCellMenuProps, MenuItemGroup } from "../../types";
import { setFocusOnColumnAdjuster } from "../../utils/accessibility-utils";
import RecursiveMenuList from "./MenuList/RecursiveMenuList";
import { HeadCellMenuWrapper, StyledMenuIconButton } from "./styles";

const HeadCellMenu = ({ column, tabIndex }: HeadCellMenuProps) => {
  const { isDim, qLibraryId, fieldId, headTextAlign, pageColIdx } = column;
  const { translator, embed, model, interactions, layout } = useContextSelector(
    TableContext,
    (value) => value.baseProps,
  );
  const anchorRef = useRef<HTMLDivElement>(null);
  const listboxRef = useRef<HTMLDivElement>(null);
  const [openMenuDropdown, setOpenMenuDropdown] = useState(false);
  const [openListboxDropdown, setOpenListboxDropdown] = useState(false);
  const {
    fieldInstance,
    selectionActionsEnabledStatus,
    resetSelectionActionsEnabledStatus,
    updateSelectionActionsEnabledStatus,
  } = useFieldSelection(column, openMenuDropdown);
  const isNewHeadCellMenuEnabled = useContextSelector(
    TableContext,
    (value) => value.featureFlags.isNewHeadCellMenuEnabled,
  );

  const embedListbox = useCallback(() => {
    const id = qLibraryId ? { qLibraryId, type: "dimension" } : fieldId;
    // @ts-expect-error TODO: no types for `__DO_NOT_USE__`, it will improve when it becomes stable
    // eslint-disable-next-line
    embed.__DO_NOT_USE__.popover(listboxRef.current, id, {
      anchorOrigin: { vertical: "bottom", horizontal: "left" },
      transformOrigin: { vertical: "top", horizontal: "left" },
      stateName: layout.qStateName,
    });

    // @ts-expect-error TODO: no types for popover related api until it becomes stable
    embed.on("fieldPopoverClose", () => {
      setOpenListboxDropdown(false);
    });
  }, [embed, fieldId, qLibraryId, layout.qStateName]);

  const menuItemGroups = useMemo<MenuItemGroup[]>(
    () => [
      ...(isDim && interactions.select
        ? [
            [
              {
                id: 1,
                itemTitle: translator.get("SNTable.MenuItem.Search"),
                onClick: (evt: React.MouseEvent) => {
                  evt.stopPropagation();
                  setOpenMenuDropdown(false);
                  setOpenListboxDropdown(true);
                  embedListbox();
                },
                icon: <Search />,
                enabled: true,
              },
            ],
            [
              {
                id: 1,
                itemTitle: translator.get("SNTable.MenuItem.Selections"),
                icon: <Selection />,
                enabled: true,
                subMenus: [
                  [
                    {
                      id: 1,
                      itemTitle: translator.get("SNTable.MenuItem.SelectAll"),
                      onClick: async () => {
                        setOpenMenuDropdown(false);
                        await fieldInstance?.selectAll();
                      },
                      icon: <SelectAll />,
                      enabled: selectionActionsEnabledStatus.canSelectAll,
                    },
                    {
                      id: 2,
                      itemTitle: translator.get("SNTable.MenuItem.SelectPossible"),
                      onClick: async () => {
                        setOpenMenuDropdown(false);
                        await fieldInstance?.selectPossible();
                      },
                      icon: <SelectPossible />,
                      enabled: selectionActionsEnabledStatus.canSelectPossible,
                    },
                    {
                      id: 3,
                      itemTitle: translator.get("SNTable.MenuItem.SelectAlternative"),
                      onClick: async () => {
                        setOpenMenuDropdown(false);
                        await fieldInstance?.selectAlternative();
                      },
                      icon: <SelectAlternative />,
                      enabled: selectionActionsEnabledStatus.canSelectAlternative,
                    },
                    {
                      id: 4,
                      itemTitle: translator.get("SNTable.MenuItem.SelectExcluded"),
                      onClick: async () => {
                        setOpenMenuDropdown(false);
                        await fieldInstance?.selectExcluded();
                      },
                      icon: <SelectExcluded />,
                      enabled: selectionActionsEnabledStatus.canSelectExcluded,
                    },
                  ],
                  [
                    {
                      id: 1,
                      itemTitle: translator.get("SNTable.MenuItem.ClearSelections"),
                      onClick: async () => {
                        setOpenMenuDropdown(false);
                        await fieldInstance?.clear();
                      },
                      icon: <ClearSelections />,
                      enabled: selectionActionsEnabledStatus.canClearSelections,
                    },
                  ],
                ],
              },
            ],
          ]
        : []),
      [
        {
          id: 1,
          itemTitle: translator.get("SNTable.MenuItem.AdjustColumnSize"),
          onClick: (evt: React.MouseEvent) => {
            preventDefaultBehavior(evt);
            setOpenMenuDropdown(false);
            setFocusOnColumnAdjuster(anchorRef);
          },
          icon: <ColumnSize />,
          enabled: true,
        },
      ],
    ],
    [isDim, interactions.select, translator, selectionActionsEnabledStatus, embedListbox, fieldInstance],
  );

  const handleOpenDropdown = async () => {
    if (!openMenuDropdown && model) {
      const latestLayout = await model.getLayout();
      updateSelectionActionsEnabledStatus(latestLayout as TableLayout);
    }
    setOpenMenuDropdown(!openMenuDropdown);
  };

  useEffect(() => {
    if (!openMenuDropdown) resetSelectionActionsEnabledStatus();
  }, [openMenuDropdown, resetSelectionActionsEnabledStatus]);

  const chartId = layout.qInfo.qId as string;
  const buttonId = `sn-table-head-menu-button-${pageColIdx}-${chartId}`;
  const menuId = `sn-table-head-menu-${pageColIdx}-${chartId}`;

  return menuItemGroups.length ? (
    <HeadCellMenuWrapper rightAligned={headTextAlign === "right"}>
      <StyledMenuIconButton
        isVisible={openListboxDropdown || openMenuDropdown}
        ref={anchorRef}
        size="small"
        tabIndex={tabIndex}
        className="sn-table-head-menu-button"
        id={buttonId}
        aria-controls={openMenuDropdown ? menuId : undefined}
        aria-expanded={openMenuDropdown ? "true" : undefined}
        aria-haspopup="true"
        onClick={handleOpenDropdown}
        aria-label={translator.get("SNTable.Accessibility.ColumnOptions")}
        isNewHeadCellMenuEnabled={isNewHeadCellMenuEnabled}
      >
        <More height={DEFAULT_FONT_SIZE} />
      </StyledMenuIconButton>

      <div ref={listboxRef} />

      <RecursiveMenuList
        open={openMenuDropdown}
        anchorEl={anchorRef.current}
        onClose={() => setOpenMenuDropdown(false)}
        menuGroups={menuItemGroups}
        buttonId={buttonId}
        menuId={menuId}
      />
    </HeadCellMenuWrapper>
  ) : null;
};

export default HeadCellMenu;