ManageIQ/manageiq-ui-classic

View on GitHub
app/javascript/menu/main-menu.jsx

Summary

Maintainability
B
4 hrs
Test Coverage
import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { SideNav } from 'carbon-components-react/es/components/UIShell';

import FirstLevel from './first-level';
import GroupSwitcher from './group-switcher';
import MenuCollapse from './menu-collapse';
import MenuSearch from './search';
import MiqLogo from './miq-logo';
import SearchResults from './search-results';
import SecondLevel from './second-level';
import Username from './username';
import { updateActiveItem } from './history';

const initialExpanded = window.localStorage.getItem('patternfly-navigation-primary') !== 'collapsed';

export const MainMenu = ({
  applianceName,
  brandUrl,
  currentGroup,
  currentUser,
  customBrand,
  logoLarge,
  logoSmall,
  menu: initialMenu,
  miqGroups,
  showLogo,
  showMenuCollapse,
  showUser,
}) => {
  const [expanded, setExpanded] = useState(initialExpanded);
  const [menu, setMenu] = useState(initialMenu);
  const [searchResults, setSearch] = useState(null);
  const [activeSection, setSection] = useState(null);
  const [openMenu, setOpen] = useState(false);
  // code to override navbar in plugins
  const Navbar = ManageIQ.component.getReact('menu.Navbar');

  const appearExpanded = expanded || !!activeSection || !!searchResults;
  const hideSecondary = () => setSection(null);
  const hideSecondaryEscape = e => e.keyCode === 27 && hideSecondary();

  const secondLevelFirst = useRef(undefined);
  const firstLevelNext = useRef(undefined);
  const firstLevelPrev = useRef(undefined);

  useEffect(() => {
    // persist expanded state
    window.localStorage.setItem('patternfly-navigation-primary', expanded ? 'expanded' : 'collapsed');
  }, [expanded]);

  useEffect(() => {
    // set body class - for content offset
    const classNames = {
      true: 'miq-main-menu-expanded',
      false: 'miq-main-menu-collapsed',
    };
    document.body.classList.remove(classNames[!appearExpanded]);
    document.body.classList.add(classNames[appearExpanded]);
  }, [appearExpanded]);

  useEffect(() => {
    // cypress, debugging
    window.ManageIQ.menu = menu;
  }, [menu]);

  useEffect(() => {
    // react router support - allow history changes to update the menu .. and try on load
    updateActiveItem.setMenu = setMenu;
    updateActiveItem(ManageIQ.redux.store.getState().router.location);
  }, []);

  const showMenu = (event) => {
    // when focus/tab is in leftnav, if menu is not expanded, open menu
    if (!expanded) {
      setExpanded(true);
      // To understand if we are opening it manually on tab
      setOpen(true);
    }
    if (event.keyCode === 27) hideSecondary();
  };
  const hideMenu = (event) => {
    // if we open it manually, collpase menu on blur
    if (!event.currentTarget.contains(event.relatedTarget) && openMenu) {
      setExpanded(false);
      setOpen(false);
    }
  };
  const toggleMenu = () => {
    // if it is already open on tabbing, keep it open
    if (expanded && openMenu) {
      setOpen(false);
    } else {
      setExpanded(!expanded);
    }
  };

  const onSelect = (item) => {
    if (activeSection && item.id === activeSection.id) {
      hideSecondary();
    } else {
      setSection(item);
    }
    // The first menu item in the second level can be focused only after second level is actually displayed
    if (item) {
      setTimeout(() => {
        if (secondLevelFirst.current) {
          secondLevelFirst.current.focus();
        }
      });
    }
  };

  const unFocusSecondary = (forward) => () => {
    hideSecondary();

    const { current } = forward ? firstLevelNext : firstLevelPrev;
    // Focus the prev/next element in the first level if available
    if (current) {
      current.focus();
      if (!expanded) {
        setExpanded(true);
        setOpen(true);
      }
    }
  };

  return (
    <>
      <Navbar
        isSideNavExpanded={expanded}
        open={openMenu}
        onClickSideNavExpand={() => {
          if (expanded) setSection(null);
          setExpanded(!expanded);
        }}
        applianceName={applianceName}
        currentUser={currentUser}
        brandUrl={brandUrl}
      />
      <div
        onClick={hideSecondary}
        onKeyDown={showMenu}
        onBlur={hideMenu}
        role="presentation"
        id="main-menu-primary"
      >
        <SideNav
          aria-label={__('Main Menu')}
          className="primary"
          expanded={appearExpanded}
          addFocusListeners={false}
          isChildOfHeader={false}
        >
          {showLogo && (
            <MiqLogo
              expanded={appearExpanded}
              customBrand={customBrand}
              logoLarge={logoLarge}
              logoSmall={logoSmall}
            />
          )}

          {showUser && (
            <Username
              applianceName={applianceName}
              currentUser={currentUser}
              expanded={appearExpanded}
            />
          )}

          <GroupSwitcher
            currentGroup={currentGroup}
            expanded={appearExpanded}
            miqGroups={miqGroups}
          />

          <MenuSearch
            menu={menu}
            expanded={appearExpanded}
            onSearch={setSearch}
            toggle={() => setExpanded(!expanded)}
          />

          <hr className="bx--side-nav__hr" />

          {searchResults && <SearchResults results={searchResults} />}
          {!searchResults && (
            <FirstLevel
              menu={menu}
              onSelect={onSelect}
              activeSection={activeSection && activeSection.id}
              expanded={appearExpanded}
              ref={{
                prevRef: firstLevelPrev,
                nextRef: firstLevelNext,
              }}
            />
          )}

          {showMenuCollapse && (
            <MenuCollapse
              expanded={expanded/* not appearExpanded */}
              toggle={toggleMenu}
              onFocus={hideSecondary}
              open={openMenu}
            />
          )}
        </SideNav>
      </div>
      { activeSection && (
        <>
          <SideNav aria-label={__('Secondary Menu')} className="secondary" isChildOfHeader={false} expanded>
            <div onKeyDown={hideSecondaryEscape} role="presentation">
              <span onFocus={unFocusSecondary(false)} role="presentation" tabIndex="0" />
              <SecondLevel menu={activeSection.items} hideSecondary={hideSecondary} ref={secondLevelFirst} />
              <span onFocus={unFocusSecondary(true)} role="presentation" tabIndex="0" />
            </div>
          </SideNav>
          <div
            className="miq-main-menu-overlay"
            role="presentation"
            onClick={hideSecondary}
            onFocus={hideSecondary}
            onKeyDown={hideSecondary}
          />
        </>
      )}
    </>
  );
};

const propGroup = PropTypes.shape({
  description: PropTypes.string.isRequired,
  id: PropTypes.string.isRequired,
});

const propUser = PropTypes.shape({
  name: PropTypes.string.isRequired,
  userid: PropTypes.string.isRequired,
});

MainMenu.propTypes = {
  applianceName: PropTypes.string.isRequired,
  currentGroup: propGroup.isRequired,
  currentUser: propUser.isRequired,
  customBrand: PropTypes.bool,
  logoLarge: PropTypes.string,
  logoSmall: PropTypes.string,
  menu: PropTypes.arrayOf(PropTypes.any).isRequired,
  miqGroups: PropTypes.arrayOf(propGroup).isRequired,
  showLogo: PropTypes.bool,
  showMenuCollapse: PropTypes.bool,
  showUser: PropTypes.bool,
};

MainMenu.defaultProps = {
  showLogo: true,
  showMenuCollapse: true,
  showUser: true,
};