OperationCode/front-end

View on GitHub
components/Nav/NavListItem/NavListItem.tsx

Summary

Maintainability
A
35 mins
Test Coverage
A
100%
import { useState } from 'react';
import classNames from 'classnames';
import Link from 'next/link';
import PlusIcon from 'static/images/icons/plus.svg';
import MinusIcon from 'static/images/icons/minus.svg';
import styles from './NavListItem.module.css';

type SublinkType = {
  name: string;
  href: string;
};

export type NavListItemPropsType = {
  /**
   * Text used for the label.
   */
  name: string;
  /**
   * Url to be passed to the base anchor element.
   */
  href: string;
  /**
   * List of child links containing the `name` and `href`
   */
  sublinks?: SublinkType[];
  /**
   * Includes an optional icon.
   */
  icon?: React.ReactElement | null;
};

function NavListItem({ sublinks, href, name, icon = null }: NavListItemPropsType) {
  const [areSublinksVisible, setSublinksVisible] = useState(false);

  const handleKeyDown = (event: React.KeyboardEvent, indexKeyedOn: number) => {
    const lastSublinkIndex = sublinks && sublinks.length - 1;
    const isLastSublink = indexKeyedOn === lastSublinkIndex;
    const isFirstSublink = indexKeyedOn === 0;

    const didHitTab = event.key === 'Tab';
    const didTabForward = didHitTab && !event.shiftKey;
    const didTabBackward = didHitTab && event.shiftKey;

    const shouldHideSublinks =
      (isLastSublink && didTabForward) || (isFirstSublink && didTabBackward);

    if (shouldHideSublinks) {
      setSublinksVisible(false);
    }
  };

  const hasSublinks = sublinks && sublinks.length > 0;
  const exposeSublinks = () => setSublinksVisible(true);
  const hideSublinks = () => setSublinksVisible(false);
  const invertSublinkVisibility = () => setSublinksVisible(previousState => !previousState);

  return (
    <li className={styles.NavListItem}>
      <Link href={href}>
        <a
          className={classNames(styles.link, styles.navItemLink)}
          onMouseEnter={exposeSublinks}
          onMouseLeave={hideSublinks}
          role="link"
          tabIndex={0}
          data-testid={`Nav Item ${name}`}
        >
          <span className={styles.linkContent}>{name}</span>
          {icon && icon}
        </a>
      </Link>

      {hasSublinks && (
        <>
          <button
            aria-expanded={areSublinksVisible}
            aria-haspopup={hasSublinks}
            aria-label="submenu"
            className={styles.sublinkToggleButton}
            onClick={invertSublinkVisibility}
            onMouseEnter={exposeSublinks}
            onMouseLeave={hideSublinks}
            type="button"
          >
            {areSublinksVisible ? (
              <MinusIcon className={styles.icon} data-testid="minus-icon" />
            ) : (
              <PlusIcon className={styles.icon} data-testid="plus-icon" />
            )}
          </button>

          <ul
            className={classNames(styles.sublinksList, {
              [styles.invisible]: !areSublinksVisible,
            })}
            onMouseEnter={exposeSublinks}
            onMouseLeave={hideSublinks}
          >
            {sublinks.map((sublink, index) => (
              <li className={styles.sublinkListItem} key={sublink.name}>
                {/* 😞 next/link fought being mocked, so `prefetch` has test-specific code */}
                <Link href={sublink.href} prefetch={process.env.NODE_ENV === 'production'}>
                  <a
                    className={styles.link}
                    key={sublink.name}
                    role="link"
                    tabIndex={0}
                    data-testid={`Nav Item ${sublink.name}`}
                    onKeyDown={event => handleKeyDown(event, index)}
                  >
                    <span className={styles.linkContent}>{sublink.name}</span>
                  </a>
                </Link>
              </li>
            ))}
          </ul>
        </>
      )}
    </li>
  );
}

export default NavListItem;