main/MainMenu.js

import debounce from 'lodash/debounce';
import isVisible from '../utils/dom/isVisible';
import customEvent from '../utils/customEvent';
import delegateEvent from '../utils/delegateEvent';
import dynamicElement from '../utils/dynamicElement';

/**
 * Sets up main menu navigation dropdowns
 *
 * @module MainMenu
 * @prop {Element} $navBar - DOM reference to the navigation bar
 * @prop {Element} $longformHeader
 * - DOM reference to short header ( appears only on custom templates )
 * @prop {Element} $toggles - DOM reference to all dropdown menu toggles
 * @prop {Element} $close - DOM reference to the dropdown menu close button
 * @prop {Element} $moreMenu - DOM reference to the more menu
 * @prop {String} currentMenuId - DOM selector for active menu dropdown
 * @prop {Element} $currentMenu - DOM reference to the current menu
 * @prop {String} navClass - DOM selector to capture the navigation bar
 * @prop {String} longformHeaderContrastClass
 * - Modifier class applied to short header when menu opens
 * @prop {String} longformHeaderPositionClass
 * - Modifier class applied to short header when user scrolled past hero section
 * @prop {String} navMenuOpenClass - Modifier class to apply when a menu is open
 * @prop {String} navMenuStuckClass - Modifier class to apply when menu is stuck
 * @prop {String} submenusCss - css class of element wrapping all submenus
 * @prop {String} panelAboveNav - css class added to the panel when it's scrolls above the nav
 * @prop {String} sidebarSelector - DOM selector for sidebar
 * @prop {Array} menuIds - List of menu dropdown selectors
 * @prop {String} focusableSelector - DOM selector to capture focusable elements
 * @prop {String} disabledAttribute
 * - data attribute for controlling element disabled behavior on menu open and close
 * @prop {String} disabledFlagAlways
 *	- DOM selector to identify elements that should always be disabled when any menu is open
 * @prop {String} disabledFlagSelf - DOM selector to capture focusable elements
 *	- DOM selector to identify elements that should be disabled when its own menu is open
 */
const MainMenu = {
	$navBar: null,
	$longformHeader: null,
	$interactiveHeader: null,
	$toggles: {},
	$close: null,
	$moreMenu: null,
	currentMenuId: null,
	$currentMenu: null,
	heightResetTimeout: false,
	navClass: 'l-navbar',
	longformHeaderContrastClass: 'l-longform-header--active',
	longformHeaderPositionClass: 'l-longform-header--narrow',
	interactiveHeaderContrastClass: 'l-interactive-header--active',
	interactiveHeaderPositionClass: 'l-interactive-header--narrow',
	navMenuOpenClass: 'l-navWrapper--menuOpen',
	navMenuStuckClass: 'l-navbar--stuck',
	submenusCss: 'l-navbar__submenus',
	panelAboveNav: 'l-panel--aboveNav',
	sidebarSelector: '#sidebar',
	menuIds: ['#menuMore', '#menuLocal', '#menuSearch', '#menuSocialShareNav', '#menuSocialShareArticle', '#menuComments', '#eventsCalendarNav', '#eventsTypesNav'],
	focusableSelector: 'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])',
	disabledAttribute: 'data-disabled',
	disabledFlagAlways: '[data-disabled="all"]',
	disabledFlagSelf: '[data-disabled="self"]',
	breakPointDesktop: 1312,
	breakpointTabletPortrait: 768,

	/**
	 * Binds event listeners to main menu toggles.
	 * @method init
	 */
	init() {
		// define elements
		this.$toggles = document.querySelectorAll( '[data-toggle-menu]' );
		this.$hoverToggles = document.querySelectorAll( '[data-hover-toggle-menu]' );
		this.$navBar = document.querySelector( '.l-navbar' );
		this.$wrapper = document.querySelector( '.l-navWrapper' );
		this.$longformHeader = document.querySelector( '.l-longform-header' );
		this.$interactiveHeader = document.querySelector( '.l-interactive-header' );
		this.$menuMore = document.querySelector( '#menuMore' );

		// bind menu click listeners
		[].forEach.call( this.$toggles, ( $toggle ) => {
			$toggle.addEventListener( 'click', ( event ) => {
				event.preventDefault();
				event.stopPropagation();
				const selectedMenuId = $toggle.getAttribute( 'data-toggle-menu' );
				this.handleMenuToggle( $toggle, selectedMenuId, 'click' );
			});
		});

		// watch for toggle buttons dynamically added
		delegateEvent( dynamicElement.selector, 'click', '[data-toggle-menu]', ( event, $toggle ) => {
			event.preventDefault();
			event.stopPropagation();
			const selectedMenuId = $toggle.getAttribute( 'data-toggle-menu' );
			this.handleMenuToggle( $toggle, selectedMenuId, 'click' );
		});

		// bind menu hover state listeners
		[].forEach.call( this.$hoverToggles, ( $toggle ) => {
			$toggle.addEventListener( 'mouseenter', ( event ) => {
				let mouseWithin = true;

				// delay required so it fires after the click event
				setTimeout( () => {
					event.preventDefault();
					event.stopPropagation();

					// don't show menu if mouse has already left the toggle
					$toggle.removeEventListener( 'mouseleave', this.boundCheckMouseLeave );
					if ( false === mouseWithin ) {
						return;
					}

					const selectedMenuId = $toggle.getAttribute( 'data-hover-toggle-menu' );
					this.handleMenuToggle( $toggle, selectedMenuId, 'mouseenter' );
				}, 200 );

				// check if mouse leaves the toggle in less than 200ms
				this.boundCheckMouseLeave = () => { mouseWithin = false; };
				$toggle.addEventListener( 'mouseleave', this.boundCheckMouseLeave );
			});

			// on touch-enabled devices we need to be able to toggle the menu with a click
			$toggle.addEventListener( 'click', ( event ) => {
				event.preventDefault();
				event.stopPropagation();
				const selectedMenuId = $toggle.getAttribute( 'data-hover-toggle-menu' );
				this.handleMenuToggle( $toggle, selectedMenuId, 'click' );
			});
		});

		// bind menu close buttons
		const $closeButtons = document.querySelectorAll( '[data-close-menu]' );
		[].forEach.call( $closeButtons, ( $closeButton ) => {
			$closeButton.addEventListener( 'click', () => {
				this.closeCurrentMenu();
			});
		});

		// bind focus event to menu icon so we can reset nav if tabbing
		const $menuIcon = document.querySelector( '[data-toggle-menu="#menuMore"]' );
		if ( $menuIcon ) {
			$menuIcon.addEventListener( 'focus', () => {
				const $topicsNav = document.querySelector( '#menuMain-topics' );
				if ( $topicsNav ) {
					$topicsNav.scrollTop = 0;
				}
			});
		}

		window.addEventListener( customEvent.NAV_TITLE_AND_SOCIAL, ( event ) => {
			if ( true === event.detail.in_view ) {
				// Add SocialShare panel offset when nav transitions
				// to menu to title and social share
				this.setSocialShareMenuMargin();
			} else {
				// Add Local Menu panel offset when nav transitions
				// from menu to title and social share
				this.setLocalMenuMargin();
			}
		});

		if ( this.$navBar ) {
			// bind event to capture resize and orientation change
			this.resizeHandler = debounce( () => {
				// Add SocialShare panel offset when page is resized
				this.setSocialShareMenuMargin();

				// Add Local menu panel offset when page is resized
				this.setLocalMenuMargin();
			}, 200 );
			window.addEventListener( 'orientationchange', this.resizeHandler );
			window.addEventListener( 'resize', this.resizeHandler );
			window.addEventListener( customEvent.NAV_TRANSITIONED, this.resizeHandler );

			this.resizeHandler();
		}
	},

	initClickToggles( $toggles ) {
		[].forEach.call( $toggles, ( $toggle ) => {
			$toggle.addEventListener( 'click', ( event ) => {
				event.preventDefault();
				event.stopPropagation();
				const selectedMenuId = $toggle.getAttribute( 'data-toggle-menu' );
				this.handleMenuToggle( $toggle, selectedMenuId, 'click' );
			});
		});
	},

	/**
	 * Handles click events on main menu toggles.
	 *
	 * @method handleMenuToggle
	 * @param {Element} $toggle - DOM reference to the clicked toggle element
	 * @param {string} $eventType - Specifies whether menu is opened via click or mouseenter
	 */
	handleMenuToggle( $toggle, selectedMenuId, $eventType = 'click' ) {
		// don't run this code if user is simply hovering back onto a mouseenter toggle
		// while the menu is still open (e.g. hovering from local menu onto local)
		if ( 'mouseenter' === $eventType && this.currentMenuId ) {
			if ( selectedMenuId === this.currentMenuId ) {
				return;
			}
		}

		// also don't run this code if the menu is already open via hover
		// and the user just clicked on the toggle (to avoid accidental closing by click)
		if ( 'click' === $eventType && this.currentMenuId ) {
			// Fix - inability to close the menu in iPad Mini (iOS 12)
			// Bail on the first click after the menu is already opened,
			// carry on with toggling the menu on subqequent clicks.
			if ( selectedMenuId === this.currentMenuId && 'mouseenter' === this.eventType && 'true' !== this.$currentMenu.dataset.clicked ) {
				this.$currentMenu.dataset.clicked = 'true';
				return;
			}
		}

		// Reset the 'clicked' flag for identifying subsequent clicks.
		if ( this.$currentMenu ) {
			this.$currentMenu.dataset.clicked = '';
		}

		// close any open menus, if the user is just closing the menu don't do anything else
		const openMenuEvent = this.closeOpenMenus( $toggle );
		if ( openMenuEvent ) {
			this.currentMenuId = selectedMenuId;
			this.$currentMenu = document.querySelector( this.currentMenuId );

			if ( ! this.$currentMenu ) {
				return;
			}

			this.eventType = $eventType;

			// init current menu
			this.$currentMenu.classList.add( 'l-panel--init' );

			// fade in current menu
			this.$currentMenu.classList.remove( 'is-slid-and-faded-out' );
			this.$currentMenu.classList.add( 'is-slid-and-faded-in' );

			const skipParent = $toggle.getAttribute( 'data-skip-parent' );
			if ( skipParent ) {
				this.$currentMenu.classList.add( 'l-panel--single' );
			}

			// toggle menu button (need to toggle all buttons tied to this menu)
			const $menuToggles = document.querySelectorAll( `[data-toggle-menu="${this.currentMenuId}"], [data-hover-toggle-menu="${this.currentMenuId}"]` );
			[].forEach.call( $menuToggles, ( $menuToggle ) => {
				$menuToggle.classList.add( 'is-toggled' );

				// add active modifier if the user is clicking on a c-nav__item toggle
				if ( $menuToggle.classList.contains( 'c-nav__item' ) ) {
					$menuToggle.classList.add( 'c-nav__item--active' );
				}

				// add active modifier if the user is clicking on a c-nav__link toggle
				if ( $menuToggle.classList.contains( 'c-nav__link' ) ) {
					$menuToggle.classList.add( 'c-nav__link--active' );
				}
			});

			// set aria-expaneded
			$toggle.setAttribute( 'aria-expanded', true );

			// gray out other nav items and update focus
			this.focusOnMenu( $toggle );

			// set proper menu sizing and update on resize
			this.menuSizing( $toggle, selectedMenuId );

			// setup close handlers
			this.menuClosing( $eventType, $toggle, selectedMenuId );

			// add navbar modifier class (ensures dropdown appears over everything)
			if ( ! this.isOrphan() ) {
				this.$wrapper.classList.add( this.navMenuOpenClass );
				// if the page has a short header, add contrast class to it when menu opens
				if ( this.$longformHeader ) {
					this.$longformHeader.classList.add( this.longformHeaderContrastClass );
				}

				if ( this.$interactiveHeader ) {
					this.$interactiveHeader.classList.add( this.interactiveHeaderContrastClass );
				}
			}
		}
	},

	/**
	 * Closes any open menus. Checks to see if user is attempting to open another menu.
	 *
	 * @method closeOpenMenus
	 * @param {Element} $toggle - DOM reference to the clicked toggle element
	 * @returns {bool} - True if the user is attempting to open another menu
	 */
	closeOpenMenus( $toggle ) {
		// check if user is toggling off the menu
		if ( $toggle.classList.contains( 'is-toggled' ) ) {
			this.closeCurrentMenu();
			return false;
		}

		// fade out any open menu
		if ( this.currentMenuId ) {
			this.closeCurrentMenu();
		}

		return true;
	},

	/**
	 * Grays out non-active menu items.
	 *
	 * @method focusOnMenu
	 * @param {Element} $toggle - DOM reference to the clicked toggle element
	 */
	focusOnMenu( $toggle ) {
		// if menu is an orphan, or the local menu, don't bother graying out other menu elements
		if ( ! this.isOrphan() || '#menuLocal' === this.currentMenuId ) {
			// gray out all menu items with the data-disabled="all" flag
			const $alwaysHide = document.querySelectorAll( this.disabledFlagAlways );
			[].forEach.call( $alwaysHide, elem => elem.classList.add( 'is-disabled' ) );
		}

		// gray out menu items in the currently selected menu is data-disabled="self" flag is present
		// also gray out if data-disabled="all" flag is present ( like comment button in social share )
		const $siblings = $toggle.parentNode.children;
		[].forEach.call( $siblings, ( elem ) => {
			const disabledFlag = elem.getAttribute( this.disabledAttribute );

			if ( 'self' === disabledFlag || 'all' === disabledFlag ) {
				elem.classList.add( 'is-disabled' );
			}
		});

		// setup tab focus and event handlers
		this.updateTabFocus( this.$currentMenu );
	},

	/**
	 * Sets up handlers so that menu size is adjusted to fit screen height.
	 * This allows for vertical scroll on overflow, as well as proper margins for offset menus.
	 *
	 * @method menuSizing
	 * @param {Element} $toggle - DOM reference to the clicked toggle element
	 */
	menuSizing( $toggle, selectedMenuId ) {
		// Bail when there's no associated nav bar
		if ( ! this.$navBar ) {
			return;
		}

		// set menu max height to allow scrolling
		this.setMenuHeight( this.$currentMenu, true );

		// set menu width on Comments menu
		let menuId = selectedMenuId;
		menuId = menuId.replace( '#', '' );
		if ( 'menuComments' === menuId ) {
			this.setMenuWidth( this.$currentMenu );
		}

		// bind event to capture resize and orientation change
		this.resizeHandler = debounce( () => {
			this.setMenuHeight( this.$currentMenu );
		}, 200 );
		window.addEventListener( 'orientationchange', this.resizeHandler );
		window.addEventListener( 'resize', this.resizeHandler );
		window.addEventListener( customEvent.NAV_TRANSITIONED, this.resizeHandler );
	},

	/**
	 * Binds close events so that the menu dropdown will close if the user clicks outside the menu.
	 *
	 * @method menuClosing
	 */
	menuClosing( $eventType = 'click', $toggle, selectedMenuId ) {
		if ( 'mouseenter' === $eventType ) {
			// bind event to capture mouse leaving menu
			const $menu = document.querySelector( selectedMenuId );
			this.boundMouseLeaveMenu = e => this.handleMouseLeaveMenu( e, $toggle );
			$menu.addEventListener( 'mouseleave', this.boundMouseLeaveMenu );

			// bind event to capture mouse leaving toggle
			this.boundMouseLeaveToggle = e => this.handleMouseLeaveMenu( e, $menu );
			$toggle.addEventListener( 'mouseleave', this.boundMouseLeaveToggle );
		} else {
			// bind event to capture away clicks (and first ensure that "this" is correctly bound)
			this.boundClickOutsideMenu = e => this.handleClickOutsideMenu( e );
			document.addEventListener( $eventType, this.boundClickOutsideMenu );
		}
	},

	/**
	 * Redirects tab focus to current active dropdown menu and binds keyboard event listeners.
	 *
	 * @method updateTabFocus
	 */
	updateTabFocus() {
		// save current active element
		this.menuTrigger = document.activeElement;

		// get first and last focusable Elements in dropdown and filter out non-visible
		const focusBounds = this.getFocusBounds();

		if ( focusBounds && focusBounds.firstFocusable && focusBounds.lastFocusable ) {
			this.firstFocusable = focusBounds.firstFocusable;
			this.lastFocusable = focusBounds.lastFocusable;
			this.boundKeydown = e => this.handleKeydownEvent( e );
			document.addEventListener( 'keydown', this.boundKeydown );
		}
	},

	/**
	 * Gets the first and last focusable elements of the menu.
	 *
	 * @method getFocusBounds
	 */
	getFocusBounds() {
		let focusableEls = this.$currentMenu.querySelectorAll( this.focusableSelector );
		focusableEls = [].filter.call( focusableEls, elem => isVisible( elem ) );

		if ( focusableEls.length > 0 ) {
			const { 0: firstFocusable, [focusableEls.length - 1]: lastFocusable } = focusableEls;
			return { firstFocusable, lastFocusable };
		}

		return null;
	},

	/**
	 * Handles keydown event and redirects to the appropriate handler.
	 *
	 * @method handleKeydownEvent
	 * @param {object} event - Native JS event object
	 */
	handleKeydownEvent( event ) {
		const keycodeTab = 9;
		const keycodeEsc = 27;

		if ( 'Tab' === event.key || keycodeTab === event.keyCode ) {
			this.handleTabKey( event );
		}

		if ( keycodeEsc === event.keyCode ) {
			this.handleEscKey();
		}
	},

	/**
	 * Handles tabbing through dropdown menu.
	 * If user `tabs` on menu trigger after opening menu, it brings them to the first element.
	 * If user `shift` + `tabs` before the start of the menu.
	 * If user `tabs` past the end of the dropdown, close menu and redirect focus back to `$toggle`.
	 * close menu and redirect focus back to `$toggle`.
	 *
	 * @method handleTabKey
	 * @param {object} event - Native JS event object
	 */
	handleTabKey( event ) {
		if ( ! event.shiftKey && document.activeElement === this.menuTrigger ) {
			// tab into the first menu element
			event.preventDefault();
			this.firstFocusable.focus();
		} else if ( event.shiftKey && document.activeElement === this.firstFocusable ) {
			// close the menu as the user is tabbing back out of it
			event.preventDefault();
			this.closeCurrentMenu();
			this.menuTrigger.focus();
		} else if ( ! event.shiftKey && document.activeElement === this.lastFocusable ) {
			// check if this is really the last focus element or if things have changed
			if ( ! this.newLastFocusable() ) {
				// close the menu as the user has tabbed through it
				event.preventDefault();
				this.closeCurrentMenu();
				this.menuTrigger.focus();
			}
		}
	},

	/**
	 * Checks to see if the last focusable element has changed (for example after an ajax request)
	 *
	 * @method newLastFocusable
	 */
	newLastFocusable() {
		const focusBounds = this.getFocusBounds();
		if ( focusBounds.firstFocusable && focusBounds.lastFocusable ) {
			if ( focusBounds.lastFocusable !== this.lastFocusable ) {
				this.firstFocusable = focusBounds.firstFocusable;
				this.lastFocusable = focusBounds.lastFocusable;
				return true;
			}
		}

		return false;
	},

	/**
	 * Closes menu if user presses the `esc` key. Also redirect focus back to `$toggle`.
	 *
	 * @method handleEscKey
	 */
	handleEscKey() {
		this.closeCurrentMenu();
		this.menuTrigger.focus();
	},

	/**
	 * Closes menu if user clicks outside the menu dropdown.
	 *
	 * @method handleClickOutsideMenu
	 * @param {object} event - Native JS event object
	 */
	handleClickOutsideMenu( event ) {
		// Do not close current menu if user is interacting with date picker within the opened menu.
		if ( event.target.classList.contains( 'ui-datepicker-next' )
			|| event.target.classList.contains( 'ui-datepicker-prev' )
			|| event.target.classList.contains( 'ui-datepicker-title' ) ) {
			return;
		}

		// check if click is coming from outside of menu
		if ( ! event.target.closest( this.currentMenuId ) ) {
			this.closeCurrentMenu();
		}
	},

	/**
	 * Closes menu if user's mouse moves outside the menu and not into the toggle
	 * or vice versa
	 *
	 * @method handleMouseLeaveMenu
	 * @param {object} event - Native JS event object
	 * @param {element} $alt - Toggle or menu div which the mouse may move into
	 */
	handleMouseLeaveMenu( event, $alt ) {
		if ( ! this.isMouseWithinElem( event, $alt ) ) {
			this.closeCurrentMenu();
		}
	},

	/**
	 * Checks if mouse position is within element
	 * @param {object} event - Native JS event object
	 * @param {element} $elem - DOM element to check
	 * @return {bool} - True / False depending on whether mouse position is within element.
	 */
	isMouseWithinElem( event, $elem ) {
		const bounds = $elem.getBoundingClientRect();
		if ( event.pageX >= bounds.left + window.pageXOffset
			&& event.pageX <= bounds.right + window.pageXOffset
			&& event.pageY >= bounds.top + window.pageYOffset
			&& event.pageY <= bounds.bottom + window.pageYOffset ) {
			return true;
		}

		return false;
	},

	/**
	 * Fades out active dropdown menu, resets toggle states, and unbinds event listeners.
	 *
	 * @method closeCurrentMenu
	 */
	closeCurrentMenu() {
		// close menu
		const $currentMenu = document.querySelector( this.currentMenuId );

		if ( ! $currentMenu ) {
			return;
		}

		const menuId = this.currentMenuId.replace( '#', '' );
		$currentMenu.classList.remove( 'is-slid-and-faded-in' );
		$currentMenu.classList.add( 'is-slid-and-faded-out' );

		// clear timeout for reseting submenu height
		if ( this.heightResetTimeout ) {
			clearTimeout( this.heightResetTimeout );
		}

		// Delay reseting the state till after being faded out
		this.heightResetTimeout = setTimeout( () => {
			$currentMenu.classList.remove( 'l-panel--single' );
			$currentMenu.classList.remove( 'l-panel--init' );

			// unset height of submenus wrapper
			const $parent = $currentMenu.parentNode;
			if ( $parent && $parent.classList.contains( this.submenusCss ) ) {
				$parent.removeAttribute( 'style' );
				if ( $parent.classList.contains( `${this.submenusCss}--${menuId}` ) ) {
					$parent.classList.remove( `${this.submenusCss}--${menuId}` );
				}
			}
		}, 500 );

		// reset nav toggle state (there may be multiple toggles for a given menu)
		const $menuToggles = document.querySelectorAll( `[data-toggle-menu="${this.currentMenuId}"], [data-hover-toggle-menu="${this.currentMenuId}"]` );
		[].forEach.call( $menuToggles, $menuToggle => $menuToggle.classList.remove( 'is-toggled' ) );
		[].forEach.call( $menuToggles, $menuToggle => $menuToggle.classList.remove( 'c-nav__item--active' ) );
		[].forEach.call( $menuToggles, $menuToggle => $menuToggle.classList.remove( 'c-nav__link--active' ) );

		// reset aria-expanded
		[].forEach.call( $menuToggles, $menuToggle => $menuToggle.setAttribute( 'aria-expanded', false ) );

		// turn off grayed out menu items
		const $disabledMenuItems = document.querySelectorAll( `${this.disabledFlagAlways}, ${this.disabledFlagSelf}` );
		[].forEach.call( $disabledMenuItems, elem => elem.classList.remove( 'is-disabled' ) );

		this.currentMenuId = null;
		this.$currentMenu = null;

		// remove navbar modifier class
		if ( this.$wrapper ) {
			this.$wrapper.classList.remove( this.navMenuOpenClass );
		}

		// if the page has a short header, remove contrast class from it when menu closes
		if ( this.$longformHeader ) {
			if ( ! this.$longformHeader.classList.contains( this.longformHeaderPositionClass ) ) {
				this.$longformHeader.classList.remove( this.longformHeaderContrastClass );
			}
		}

		if ( this.$interactiveHeader ) {
			if ( ! this.$interactiveHeader.classList.contains( this.interactiveHeaderPositionClass ) ) {
				this.$interactiveHeader.classList.remove( this.interactiveHeaderContrastClass );
			}
		}

		// unbind listeners
		window.removeEventListener( 'orientationchange', this.resizeHandler );
		window.removeEventListener( 'resize', this.resizeHandler );
		window.removeEventListener( customEvent.NAV_TRANSITIONED, this.resizeHandler );
		document.removeEventListener( 'keydown', this.boundKeydown );

		// remove click outside listener if it's a click toggle
		if ( this.boundClickOutsideMenu ) {
			document.removeEventListener( 'click', this.boundClickOutsideMenu );
		}

		// remove mouseleave listeners if it's a hover toggle
		if ( this.boundMouseLeaveMenu ) {
			$currentMenu.removeEventListener( 'mouseleave', this.boundMouseLeaveMenu );
			[].forEach.call( $menuToggles, ( $menuToggle ) => {
				$menuToggle.removeEventListener( 'mouseleave', this.boundMouseLeaveToggle );
			});
		}
	},

	/**
	 * Sets menu height to maximum available space in the `viewport`. Vertical scroll if it overflows.
	 *
	 * @method setMenuHeight
	 * @param {Element} $menu - DOM reference to the current active dropdown menu
	 * @param {Boolean} opening - Indicates whether to account for the y-translation when the menu
	 * is initially opened.
	 */
	setMenuHeight( $menu, opening = false ) {
		if ( this.$navBar.classList.contains( this.navMenuStuckClass ) ) {
			$menu.classList.add( this.panelAboveNav );
		} else {
			$menu.classList.remove( this.panelAboveNav );
		}

		if ( this.isOrphan() && 'force' !== $menu.dataset.menuFullHeight ) {
			return;
		}

		// clear timeout for reseting submenu height
		if ( this.heightResetTimeout ) {
			clearTimeout( this.heightResetTimeout );
		}

		const menuId  = $menu.getAttribute( 'id' );
		const $parent = $menu.parentNode;
		const viewport      = window.innerHeight;
		const bounds        = $menu.getBoundingClientRect();
		const $menuInner    = $menu.firstElementChild;
		const boundsInner   = $menuInner.getBoundingClientRect();
		const paddingOffset = bounds.height - boundsInner.height;
		let maxHeight       = viewport - bounds.top - ( opening ? 20 : 0 );

		// Comments panel should open up full height
		// or below the header if it's visible
		if ( 'menuComments' === menuId ) {
			// let topOffset = 0;
			maxHeight = viewport;
			// Regular article template
			if ( ! this.$interactiveHeader && ! this.$longformHeader ) {
				const topOffset = window.innerWidth < this.breakpointTabletPortrait
					? this.$navBar.getBoundingClientRect().top : 20;

				if ( this.$navBar.classList.contains( this.navMenuStuckClass ) ) {
					$parent.style.top = `${topOffset}px`;
				} else {
					maxHeight -= document.querySelector( '.l-header' ).getBoundingClientRect().height;
					$parent.style.top = 0;
				}
			} else if ( this.$longformHeader ) { // Longform
				let topOffset = 0;
				if ( window.innerWidth > this.breakpointTabletPortrait ) {
					topOffset = this.$longformHeader.classList
						.contains( this.longformHeaderPositionClass )
						? 54 : 78;
				}

				$parent.style.top = `-${topOffset}px`;
			} else if ( this.$interactiveHeader ) { // Interactive
				let topOffset = 0;
				if ( window.innerWidth > this.breakpointTabletPortrait ) {
					topOffset = 80;
				}

				$parent.style.top = `-${topOffset}px`;
			}
		}

		$menu.style.maxHeight = `${maxHeight}px`; // eslint-disable-line no-param-reassign

		if ( 'true' === $menu.dataset.menuFullHeight || 'force' === $menu.dataset.menuFullHeight ) {
			$menuInner.style.height = `${maxHeight - paddingOffset}px`;
		}

		if ( 'undefined' !== typeof ( $menu.style.webkitOverflowScrolling ) ) {
			$menu.style.webkitOverflowScrolling = 'touch'; // eslint-disable-line no-param-reassign
		}

		// remove overflow scroll-y if menu fits in viewport to avoid grayed-out scrollbar
		if ( bounds.bottom < viewport ) {
			$menu.style.overflow = 'auto'; // eslint-disable-line no-param-reassign
		} else {
			$menu.style.overflow = ''; // eslint-disable-line no-param-reassign
		}

		// workaround for bottom-positioned mobile menus
		if ( 'menuSocialShareNav' === menuId ) {
			if ( ! this.$interactiveHeader && ! this.$longformHeader ) {
				if ( window.innerWidth < this.breakpointTabletPortrait ) {
					if ( ! this.$navBar.classList.contains( this.navMenuStuckClass ) ) {
						$parent.style.top = '-48px';
					} else {
						$parent.style.top = 0;
					}
				}
			} else {
				$parent.style.top = 0;
			}
			$parent.style.height = `${viewport}px`;
		// set height of submenus wrapper, which sets overflow to hidden
		// ensuring that submenus appear to slide in from behind main nav
		} else if ( $parent && $parent.classList.contains( this.submenusCss ) ) {
			$parent.style.height = `${maxHeight}px`;
		}
	},

	/**
	 * Sets menu minimum width
	 * Calculate width of the comments menu so it always covers the sidebar
	 *
	 * @method setMenuWidth
	 * @param {Element} $menu - DOM reference to the current active dropdown menu
	 */
	setMenuWidth( $menu ) {
		const $sidebar = document.querySelector( this.sidebarSelector );
		// Longform and Issues templates does not have a sidebar
		// Skip calculation - width is set with CSS
		if ( ! $sidebar ) {
			return;
		}

		const $thisMenu = $menu;
		const menuBounds = $menu.getBoundingClientRect();
		const sidebarBounds = $sidebar.getBoundingClientRect();
		const commentsMenuWidth = menuBounds.right - sidebarBounds.left;
		$thisMenu.style.width = `${commentsMenuWidth}px`;
	},

	/**
	 * Offsets the menu's margin so it aligns with its `$toggle` trigger.
	 *
	 * @method setMenuMargin
	 * @param {Element} $menu - DOM reference to the current active dropdown menu
	 * @param {Element} $toggle - DOM reference to the clicked toggle element
	 * @param {string} marginSide - The side of the menu to apply the offset
	 * @param {number} padding - Additional horizontal offset between the menu and its toggle
	 */
	setMenuMargin( $menu, $toggle, marginSide = 'left', padding = 16 ) {
		const margin = $toggle.getBoundingClientRect()[marginSide];
		const offset = this.$navBar.getBoundingClientRect()[marginSide];
		if ( 'left' === marginSide ) {
			$menu.style.marginLeft = `${margin - offset - padding}px`; // eslint-disable-line no-param-reassign
		} else if ( 'right' === marginSide ) {
			$menu.style.marginRight = `${offset - margin - padding}px`; // eslint-disable-line no-param-reassign
		}
	},

	setLocalMenuMargin() {
		if ( window.innerWidth > this.breakPointDesktop ) {
			// Local Nav panel offset when nav transitions
			// from menu to title and social share
			const $localButton = document.querySelector( '[data-hover-toggle-menu="#menuLocal"]' );
			const $localMenu = document.querySelector( '#menuLocal' );
			if ( $localButton && $localMenu ) {
				this.setMenuMargin( $localMenu, $localButton );
			}
		}
	},

	setSocialShareMenuMargin() {
		const $socialButton = document.querySelector( '[data-toggle-menu="#menuSocialShareNav"]' );
		const $socialMenu = document.querySelector( '#menuSocialShareNav' );
		if ( $socialButton && $socialMenu ) {
			this.setMenuMargin( $socialMenu, $socialButton, 'right', 0 );
		}
	},

	/**
	 * Checks if menu is separate from the main nav
	 *
	 * @method isOrphan
	 * @return {Boolean} - True / False whether or not it is a child of the main nav
	 */
	isOrphan() {
		return ( null === this.$currentMenu.closest( `.${this.navClass}` ) );
	},
};

export default MainMenu;