161 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			161 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * @file
 | 
						|
 * Customization of navigation.
 | 
						|
 */
 | 
						|
 | 
						|
((Drupal, once, tabbable) => {
 | 
						|
  /**
 | 
						|
   * Checks if navWrapper contains "is-active" class.
 | 
						|
   *
 | 
						|
   * @param {Element} navWrapper
 | 
						|
   *   Header navigation.
 | 
						|
   *
 | 
						|
   * @return {boolean}
 | 
						|
   *   True if navWrapper contains "is-active" class, false if not.
 | 
						|
   */
 | 
						|
  function isNavOpen(navWrapper) {
 | 
						|
    return navWrapper.classList.contains('is-active');
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Opens or closes the header navigation.
 | 
						|
   *
 | 
						|
   * @param {object} props
 | 
						|
   *   Navigation props.
 | 
						|
   * @param {boolean} state
 | 
						|
   *   State which to transition the header navigation menu into.
 | 
						|
   */
 | 
						|
  function toggleNav(props, state) {
 | 
						|
    const value = !!state;
 | 
						|
    props.navButton.setAttribute('aria-expanded', value);
 | 
						|
    props.body.classList.toggle('is-overlay-active', value);
 | 
						|
    props.body.classList.toggle('is-fixed', value);
 | 
						|
    props.navWrapper.classList.toggle('is-active', value);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Initialize the header navigation.
 | 
						|
   *
 | 
						|
   * @param {object} props
 | 
						|
   *   Navigation props.
 | 
						|
   */
 | 
						|
  function init(props) {
 | 
						|
    props.navButton.setAttribute('aria-controls', props.navWrapperId);
 | 
						|
    props.navButton.setAttribute('aria-expanded', 'false');
 | 
						|
 | 
						|
    props.navButton.addEventListener('click', () => {
 | 
						|
      toggleNav(props, !isNavOpen(props.navWrapper));
 | 
						|
    });
 | 
						|
 | 
						|
    // Close any open sub-navigation first, then close the header navigation.
 | 
						|
    document.addEventListener('keyup', (e) => {
 | 
						|
      if (e.key === 'Escape') {
 | 
						|
        if (props.olivero.areAnySubNavsOpen()) {
 | 
						|
          props.olivero.closeAllSubNav();
 | 
						|
        } else {
 | 
						|
          toggleNav(props, false);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    });
 | 
						|
 | 
						|
    props.overlay.addEventListener('click', () => {
 | 
						|
      toggleNav(props, false);
 | 
						|
    });
 | 
						|
 | 
						|
    props.overlay.addEventListener('touchstart', () => {
 | 
						|
      toggleNav(props, false);
 | 
						|
    });
 | 
						|
 | 
						|
    // Focus trap. This is added to the header element because the navButton
 | 
						|
    // element is not a child element of the navWrapper element, and the keydown
 | 
						|
    // event would not fire if focus is on the navButton element.
 | 
						|
    props.header.addEventListener('keydown', (e) => {
 | 
						|
      if (e.key === 'Tab' && isNavOpen(props.navWrapper)) {
 | 
						|
        const tabbableNavElements = tabbable.tabbable(props.navWrapper);
 | 
						|
        tabbableNavElements.unshift(props.navButton);
 | 
						|
        const firstTabbableEl = tabbableNavElements[0];
 | 
						|
        const lastTabbableEl =
 | 
						|
          tabbableNavElements[tabbableNavElements.length - 1];
 | 
						|
 | 
						|
        if (e.shiftKey) {
 | 
						|
          if (
 | 
						|
            document.activeElement === firstTabbableEl &&
 | 
						|
            !props.olivero.isDesktopNav()
 | 
						|
          ) {
 | 
						|
            lastTabbableEl.focus();
 | 
						|
            e.preventDefault();
 | 
						|
          }
 | 
						|
        } else if (
 | 
						|
          document.activeElement === lastTabbableEl &&
 | 
						|
          !props.olivero.isDesktopNav()
 | 
						|
        ) {
 | 
						|
          firstTabbableEl.focus();
 | 
						|
          e.preventDefault();
 | 
						|
        }
 | 
						|
      }
 | 
						|
    });
 | 
						|
 | 
						|
    // Remove overlays when browser is resized and desktop nav appears.
 | 
						|
    window.addEventListener('resize', () => {
 | 
						|
      if (props.olivero.isDesktopNav()) {
 | 
						|
        toggleNav(props, false);
 | 
						|
        props.body.classList.remove('is-overlay-active');
 | 
						|
        props.body.classList.remove('is-fixed');
 | 
						|
      }
 | 
						|
 | 
						|
      // Ensure that all sub-navigation menus close when the browser is resized.
 | 
						|
      Drupal.olivero.closeAllSubNav();
 | 
						|
    });
 | 
						|
 | 
						|
    // If hyperlink links to an anchor in the current page, close the
 | 
						|
    // mobile menu after the click.
 | 
						|
    props.navWrapper.addEventListener('click', (e) => {
 | 
						|
      if (
 | 
						|
        e.target.matches(
 | 
						|
          `[href*="${window.location.pathname}#"], [href*="${window.location.pathname}#"] *, [href^="#"], [href^="#"] *`,
 | 
						|
        )
 | 
						|
      ) {
 | 
						|
        toggleNav(props, false);
 | 
						|
      }
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Initialize the navigation.
 | 
						|
   *
 | 
						|
   * @type {Drupal~behavior}
 | 
						|
   *
 | 
						|
   * @prop {Drupal~behaviorAttach} attach
 | 
						|
   *   Attach context and settings for navigation.
 | 
						|
   */
 | 
						|
  Drupal.behaviors.oliveroNavigation = {
 | 
						|
    attach(context) {
 | 
						|
      const headerId = 'header';
 | 
						|
      const header = once('navigation', `#${headerId}`, context).shift();
 | 
						|
      const navWrapperId = 'header-nav';
 | 
						|
 | 
						|
      if (header) {
 | 
						|
        const navWrapper = header.querySelector(`#${navWrapperId}`);
 | 
						|
        const { olivero } = Drupal;
 | 
						|
        const navButton = context.querySelector(
 | 
						|
          '[data-drupal-selector="mobile-nav-button"]',
 | 
						|
        );
 | 
						|
        const body = document.body;
 | 
						|
        const overlay = context.querySelector(
 | 
						|
          '[data-drupal-selector="header-nav-overlay"]',
 | 
						|
        );
 | 
						|
 | 
						|
        init({
 | 
						|
          olivero,
 | 
						|
          header,
 | 
						|
          navWrapperId,
 | 
						|
          navWrapper,
 | 
						|
          navButton,
 | 
						|
          body,
 | 
						|
          overlay,
 | 
						|
        });
 | 
						|
      }
 | 
						|
    },
 | 
						|
  };
 | 
						|
})(Drupal, once, tabbable);
 |