ifmeorg/ifme

View on GitHub
client/app/components/Header/index.jsx

Summary

Maintainability
A
0 mins
Test Coverage
// @flow
import React, { useState, useRef, type Node } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faBars, faTimes } from '@fortawesome/free-solid-svg-icons';
import renderHTML from 'react-render-html';
import { I18n } from 'libs/i18n';
import { Logo } from 'components/Logo';
import { HeaderProfile } from 'components/Header/HeaderProfile';
import type { Profile, Link } from './types';
import css from './Header.scss';
import { useFocusTrap } from '../../hooks';

export type Props = {
  home: Link,
  links: Link[],
  mobileOnly?: any,
  profile?: Profile,
};

export type State = {
  mobileNavOpen: boolean,
};

export const Header = ({
  home, links, mobileOnly, profile,
}: Props): Node => {
  const [mobileNavOpen, setMobileNavOpen] = useState(false);
  const navigationRef = useRef(null);

  useFocusTrap(navigationRef, mobileNavOpen);

  const toggle = (): void => {
    setMobileNavOpen((currentNavValue) => !currentNavValue);
  };

  const handleHamburgerKeyDown = (
    event: SyntheticKeyboardEvent<HTMLElement>,
  ): void => {
    // Only toggle the menu if the user presses the Enter key or the space bar
    if (['Enter', ' '].includes(event.key)) {
      /**
       * Prevent the default action to stop scrolling when space is pressed
       */
      event.preventDefault();
      toggle();
    }
  };

  const displayToggle = (): Node => {
    const body = ((document.body: any): HTMLBodyElement);
    if (mobileNavOpen) {
      body.classList.add('bodyHeaderOpen');
      return <FontAwesomeIcon icon={faTimes} />;
    }
    body.classList.remove('bodyHeaderOpen');
    return <FontAwesomeIcon icon={faBars} />;
  };

  const displayLinks = (): Node[] => links.map((link: Link) => (
    <div className={css.headerLink} key={link.name}>
      <a
        href={link.url}
        className={`${link.active ? css.headerActiveLink : ''} ${
          link.hideInMobile ? css.headerHideInMobile : ''
        }`}
        data-method={`${link.dataMethod || ''}`}
        rel={`${link.dataMethod ? 'nofollow' : ''}`}
      >
        {link.name}
      </a>
    </div>
  ));

  const displayDesktop = (): Node => (
    <div
      className={css.headerDesktop}
      aria-label={I18n.t('navigation.main_menu')}
      role="navigation"
    >
      <div className={css.headerDesktopHome}>
        <Logo sm link={home.url} />
      </div>
      <div className={css.headerDesktopNav}>
        <div
          id="headerHamburger"
          className={css.headerHamburger}
          onClick={toggle}
          onKeyDown={handleHamburgerKeyDown}
          role="button"
          tabIndex="0"
          aria-label={mobileNavOpen ? I18n.t('close') : I18n.t('expand_menu')}
        >
          {displayToggle()}
        </div>
        {!mobileNavOpen && (
          <div className={css.headerDesktopNavLinks}>{displayLinks()}</div>
        )}
      </div>
    </div>
  );

  const displayMobile = (): Node => (
    <div id="headerMobile" className={css.headerMobileNav}>
      <div>
        {profile ? <HeaderProfile profile={profile} /> : null}
        {mobileOnly ? renderHTML(mobileOnly) : null}
        {displayLinks()}
      </div>
    </div>
  );

  return (
    <header
      id="header"
      className={`${css.header} ${mobileNavOpen ? css.headerMobile : ''}`}
    >
      <div
        ref={navigationRef}
        className={`${mobileNavOpen ? css.headerMobileBg : ''}`}
        role="menu"
        tabIndex="-1"
      >
        {displayDesktop()}
        {mobileNavOpen ? displayMobile() : null}
      </div>
    </header>
  );
};

export default ({
  home, links, mobileOnly, profile,
}: Props): Node => (
  <Header home={home} links={links} mobileOnly={mobileOnly} profile={profile} />
);