SU-SWS/stanford_profile

View on GitHub
themes/stanford_basic/src/js/components/secondary-nav/buttons/SecondarySubNavButtons.js

Summary

Maintainability
B
5 hrs
Test Coverage
import SubNavToggle from './SubNavToggle';
// Events
import EventHandlerDispatch from '../../nav/EventHandlerDispatch';
import {createEvent} from '../../../polyfills/createEvent';

// Keyboard events.
import OnHome from '../common/events/OnHome';
import OnEnd from '../common/events/OnEnd';
import OnEsc from '../common/events/OnEsc';
import OnSpace from '../common/events/OnSpace';
import OnArrowUp from '../common/events/OnArrowUp';
import OnArrowRight from './events/OnArrowRight';
import OnArrowDown from '../common/events/OnArrowDown';
import OnArrowLeft from '../common/events/OnArrowLeft';

/**
 * SecondarySubNavButtons Class
 *
 * A sub menu class for creating a menu with toggle button functionality.
 */
export default class SecondarySubNavButtons {

  /**
   * Initialize.
   *
   * @param {HTMLElement} element     The container wrapper for the nav.
   * @param {Object|Mixed} masterNav  The top most level navigation.
   * @param {Object|Mixed} parentNav  The parent navigation instance if this
   *                                  instance is nested.
   * @param {Object} options          A meta object of information and options.
   */
  constructor(element, masterNav, parentNav = null, options = {}) {
    // Vars.
    this.elem = element;
    this.item = element.parentNode;
    this.masterNav = masterNav;
    this.parentNav = parentNav;
    this.depth = options.depth || 1;
    this.preOpenSubnav = createEvent('preOpenSubnav', {bubbles: true, data: this.item});
    this.postOpenSubnav = createEvent('postOpenSubnav', {bubbles: true, data: this.item});
    this.preCloseSubnav = createEvent('preCloseSubnav', {bubbles: true, data: this.item});
    this.postCloseSubnav = createEvent('postCloseSubnav', {bubbles: true, data: this.item});

    // Merge in defaults.
    this.options = Object.assign({
      itemExpandedClass: 'su-secondary-nav__item--expanded',
      toggleClass: 'su-nav-toggle',
      toggleLabel: 'expand' + this.elem.innerText + ' menu',
      subNavToggleText: '+'
    }, options);

    // Assign the event dispatcher and event registry.
    this.eventRegistry = this.createEventRegistry(options);
    this.dispatch = new EventHandlerDispatch(element, this);

    // Create the toggle buttons.
    this.initToggleButton(options);

    // Add the accessibility meta-information.
    this.initAccessibility();

  }

  /**
   * Initialize the toggle button.
   * @param {Object} options a meta object of information to pass along.
   */
  initToggleButton(options = {}) {
    this.toggleElement = this.createToggleButton();
    this.item.insertBefore(this.toggleElement, this.item.querySelector('ul'));
    this.toggle = new SubNavToggle(this.toggleElement, this, options);
  }

  /**
   * Creates an event registry for handling types of events.
   *
   * This registry is used by the EventHandlerDispatch class to bind and
   * execute the events in the created property key.
   *
   * @param  {Object} options Options to merge in with the defaults.
   *
   * @return {Object} A key/value registry of events and handlers.
   */
  createEventRegistry(options) {

    var registryDefaults = {
      onKeydownSpace: OnSpace,
      onKeydownEnter: OnSpace,
      onKeydownHome: OnHome,
      onKeydownEnd: OnEnd,
      onKeydownEscape: OnEsc,
      onKeydownArrowUp: OnArrowUp,
      onKeydownArrowRight: OnArrowRight,
      onKeydownArrowDown: OnArrowDown,
      onKeydownArrowLeft: OnArrowLeft
    };

    return Object.assign(registryDefaults, options.eventRegistry);
  }

  /**
   * Create and a button for the expand/collapse actions.
   *
   * @return {HTMLElement} The button toggle.
   */
  createToggleButton() {
    let element = document.createElement('button');
    let label = document.createTextNode(this.options.subNavToggleText);

    // Give this instance a unique ID.
    let id = 'toggle-' + Math.random().toString(36).substr(2, 9);

    element.setAttribute('class', this.options.toggleClass);
    element.setAttribute('aria-expanded', 'false');
    element.setAttribute('aria-label', this.options.toggleLabel);
    element.setAttribute('id', id);
    element.appendChild(label);

    return element;
  }

  /**
   * Is this expanded? Can only return TRUE if this is a subnav trigger.
   *
   * @return {Boolean}
   *  Wether or not the item is expanded.
   */
  isExpanded() {
    return this.toggleElement.getAttribute('aria-expanded') === 'true';
  }

  /**
   * Handles the opening of a sub-nav.
   *
   * If this is a subnav trigger, open the corresponding subnav.
   * Optionally force focus on the first element in the subnav
   * (for keyboard nav).
   */
  openSubNav() {
    this.elem.dispatchEvent(this.preOpenSubnav);
    this.toggleElement.setAttribute('aria-expanded', true);
    this.item.classList.add(this.options.itemExpandedClass);
    this.elem.dispatchEvent(this.postOpenSubnav);
  }

  /**
   * Handles the closing of a subnav.
   *
   * If this is a subnav trigger or an item in a subnav, close the
   * corresponding subnav. Optionally force focus on the trigger.
   */
  closeSubNav() {
    this.elem.dispatchEvent(this.preCloseSubnav);
    this.toggleElement.setAttribute('aria-expanded', false);
    this.item.classList.remove(this.options.itemExpandedClass);
    this.elem.dispatchEvent(this.postCloseSubnav);
  }

  /**
   * Get the level of nesting for this nav.
   *
   * @return {Integer} The integer of depth starting at 1.
   */
  getDepth() {
    return this.depth;
  }

  /**
   * Adds ids, labels, and other meta-information.
   */
  initAccessibility() {
    var elementIndex = Array.from(this.item.parentNode.children).indexOf(this.item);
    var elemID = this.toggleElement.getAttribute('id');
    var section = this.item.querySelector(':scope > ul');
    var sectionID = section.getAttribute('id');

    // If there isnt an ID on the element add one.
    if (!elemID) {
      elemID = 'su-acc-' + this.getDepth() + '-' + elementIndex;
      this.toggleElement.setAttribute('id', elemID);
    }

    if (!sectionID) {
      sectionID = 'su-acs-' + this.getDepth() + '-' + elementIndex;
      let uniqueIndex = 0;
      // Make sure the id attribute will be unique.
      while (document.getElementById(sectionID)) {
        sectionID = 'su-acs-' + this.getDepth() + '-' + elementIndex + '-' + uniqueIndex;
        uniqueIndex++;
      }
      // If there isnt an ID on the section add one.
      section.setAttribute('id', sectionID);
    }

    // Add the aria stuff.
    section.setAttribute('aria-labelledby', elemID);
  }

}