/** * File navigation.js. * * Handles toggling the navigation menu for small screens and enables TAB key * navigation support for dropdown menus. */ ( function() { const brevitySiteNavigation = document.getElementById( 'site-navigation' ); // Return early if the navigation doesn't exist. if ( ! brevitySiteNavigation ) { return; } const brevityButton = brevitySiteNavigation.getElementsByTagName( 'button' )[ 0 ]; // Return early if the button doesn't exist. if ( 'undefined' === typeof brevityButton ) { return; } const brevityMenu = brevitySiteNavigation.getElementsByTagName( 'ul' )[ 0 ]; // Hide menu toggle button if menu is empty and return early. if ( 'undefined' === typeof brevityMenu ) { brevityButton.style.display = 'none'; return; } if ( ! brevityMenu.classList.contains( 'nav-menu' ) ) { brevityMenu.classList.add( 'nav-menu' ); } // Get all the link elements within the menu. const brevityLinks = brevityMenu.getElementsByTagName( 'a' ); // Get all the link elements with children within the menu. const brevityLinksWithChildren = brevityMenu.querySelectorAll( '.menu-item-has-children > a, .page_item_has_children > a' ); // Store the first and last focusable elements for focus trap. let brevityFirstFocusableElement = null; let brevityLastFocusableElement = null; /** * Sets or removes .focus class on an element. * * @param {Event} event The event object. */ function brevityToggleFocus( event ) { if ( event.type === 'focus' || event.type === 'blur' ) { let brevitySelf = this; // Move up through the ancestors of the current link until we hit .nav-menu. while ( ! brevitySelf.classList.contains( 'nav-menu' ) ) { // On li elements toggle the class .focus. if ( 'li' === brevitySelf.tagName.toLowerCase() ) { brevitySelf.classList.toggle( 'focus' ); } brevitySelf = brevitySelf.parentNode; } } if ( event.type === 'touchstart' ) { const brevityMenuItem = this.parentNode; event.preventDefault(); for ( const brevityLink of brevityMenuItem.parentNode.children ) { if ( brevityMenuItem !== brevityLink ) { brevityLink.classList.remove( 'focus' ); } } brevityMenuItem.classList.toggle( 'focus' ); } } /** * Handle keyboard navigation in menu. * * @param {KeyboardEvent} event The keyboard event. */ function brevityHandleMenuKeyboard( event ) { const brevityCurrentLink = event.target; const brevityCurrentItem = brevityCurrentLink.closest( 'li' ); const brevityIsSubmenu = brevityCurrentItem.parentElement !== brevityMenu && brevityCurrentItem.parentElement.closest( '.nav-menu' ) === brevityMenu; const brevityHasChildren = brevityCurrentItem.classList.contains( 'menu-item-has-children' ) || brevityCurrentItem.classList.contains( 'page_item_has_children' ); const brevityIsMobileMenu = brevityIsMobile() && brevitySiteNavigation.classList.contains( 'toggled' ); // Handle Escape key to close menu or submenu. if ( event.key === 'Escape' ) { if ( brevityIsSubmenu ) { // Close submenu and return focus to parent. const brevityParentItem = brevityCurrentItem.parentElement.parentElement; const brevityParentLink = brevityParentItem ? brevityParentItem.querySelector( '> a' ) : null; if ( brevityParentLink ) { event.preventDefault(); brevityCurrentItem.parentElement.classList.remove( 'focus' ); brevityParentItem.classList.remove( 'focus' ); brevityParentLink.focus(); } } else if ( brevityIsMobileMenu ) { // Close main menu on mobile. brevitySiteNavigation.classList.remove( 'toggled' ); brevityButton.setAttribute( 'aria-expanded', 'false' ); brevityButton.focus(); event.preventDefault(); } return; } // Handle arrow keys for submenu navigation (works on both desktop and mobile). if ( brevityHasChildren && ( event.key === 'ArrowDown' || event.key === 'ArrowRight' ) ) { const brevitySubmenu = brevityCurrentItem.querySelector( 'ul' ); if ( brevitySubmenu ) { const brevityFirstSubmenuLink = brevitySubmenu.querySelector( 'a' ); if ( brevityFirstSubmenuLink ) { event.preventDefault(); brevityCurrentItem.classList.add( 'focus' ); // On mobile, ensure submenu is visible. if ( brevityIsMobileMenu ) { brevitySubmenu.style.display = 'block'; } brevityFirstSubmenuLink.focus(); } } } // Handle arrow up/left to go back to parent. if ( brevityIsSubmenu && ( event.key === 'ArrowUp' || event.key === 'ArrowLeft' ) ) { const brevityParentItem = brevityCurrentItem.parentElement.parentElement; const brevityParentLink = brevityParentItem ? brevityParentItem.querySelector( '> a' ) : null; if ( brevityParentLink ) { event.preventDefault(); brevityCurrentItem.parentElement.classList.remove( 'focus' ); brevityParentItem.classList.remove( 'focus' ); // On mobile, hide submenu. if ( brevityIsMobileMenu ) { brevityCurrentItem.parentElement.style.display = 'none'; } brevityParentLink.focus(); } } // Handle arrow keys for vertical navigation in mobile menu (only for top-level items without children). if ( brevityIsMobileMenu && ! brevityHasChildren && ! brevityIsSubmenu ) { const brevityTopLevelItems = Array.from( brevityMenu.querySelectorAll( '> li' ) ); const brevityCurrentIndex = brevityTopLevelItems.indexOf( brevityCurrentItem ); if ( event.key === 'ArrowDown' ) { event.preventDefault(); if ( brevityCurrentIndex < brevityTopLevelItems.length - 1 ) { const brevityNextItem = brevityTopLevelItems[ brevityCurrentIndex + 1 ]; const brevityNextLink = brevityNextItem.querySelector( '> a' ); if ( brevityNextLink ) { brevityNextLink.focus(); } } } else if ( event.key === 'ArrowUp' ) { event.preventDefault(); if ( brevityCurrentIndex > 0 ) { const brevityPrevItem = brevityTopLevelItems[ brevityCurrentIndex - 1 ]; const brevityPrevLink = brevityPrevItem.querySelector( '> a' ); if ( brevityPrevLink ) { brevityPrevLink.focus(); } } else { // If at first item, focus button. brevityButton.focus(); } } } // Handle arrow keys for vertical navigation in submenus on mobile. if ( brevityIsMobileMenu && brevityIsSubmenu ) { const brevitySubmenuItems = Array.from( brevityCurrentItem.parentElement.querySelectorAll( '> li' ) ); const brevityCurrentSubmenuIndex = brevitySubmenuItems.indexOf( brevityCurrentItem ); if ( event.key === 'ArrowDown' ) { event.preventDefault(); if ( brevityCurrentSubmenuIndex < brevitySubmenuItems.length - 1 ) { const brevityNextSubmenuItem = brevitySubmenuItems[ brevityCurrentSubmenuIndex + 1 ]; const brevityNextSubmenuLink = brevityNextSubmenuItem.querySelector( '> a' ); if ( brevityNextSubmenuLink ) { brevityNextSubmenuLink.focus(); } } } else if ( event.key === 'ArrowUp' ) { event.preventDefault(); if ( brevityCurrentSubmenuIndex > 0 ) { const brevityPrevSubmenuItem = brevitySubmenuItems[ brevityCurrentSubmenuIndex - 1 ]; const brevityPrevSubmenuLink = brevityPrevSubmenuItem.querySelector( '> a' ); if ( brevityPrevSubmenuLink ) { brevityPrevSubmenuLink.focus(); } } else { // If at first submenu item, focus parent. const brevityParentItem = brevityCurrentItem.parentElement.parentElement; const brevityParentLink = brevityParentItem ? brevityParentItem.querySelector( '> a' ) : null; if ( brevityParentLink ) { brevityParentLink.focus(); } } } } } /** * Check if we're on mobile viewport. * * @return {boolean} True if mobile, false otherwise. */ function brevityIsMobile() { return window.matchMedia( '(max-width: 37.5em)' ).matches; } /** * Handle focus trap when menu is open on mobile. * * @param {KeyboardEvent} event The keyboard event. */ function brevityHandleFocusTrap( event ) { // Only apply focus trap on mobile. if ( ! brevityIsMobile() ) { return; } if ( ! brevitySiteNavigation.classList.contains( 'toggled' ) ) { return; } // Only handle Tab key. if ( event.key !== 'Tab' ) { return; } // Get all focusable elements in the menu. const brevityFocusableElements = brevitySiteNavigation.querySelectorAll( 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' ); if ( brevityFocusableElements.length === 0 ) { return; } brevityFirstFocusableElement = brevityFocusableElements[ 0 ]; brevityLastFocusableElement = brevityFocusableElements[ brevityFocusableElements.length - 1 ]; // If Shift+Tab on first element, focus last element. if ( event.shiftKey && document.activeElement === brevityFirstFocusableElement ) { event.preventDefault(); brevityLastFocusableElement.focus(); } else if ( ! event.shiftKey && document.activeElement === brevityLastFocusableElement ) { // If Tab on last element, focus first element. event.preventDefault(); brevityFirstFocusableElement.focus(); } } /** * Update focusable elements when menu state changes. */ function brevityUpdateFocusableElements() { if ( brevitySiteNavigation.classList.contains( 'toggled' ) ) { const brevityFocusableElements = brevitySiteNavigation.querySelectorAll( 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' ); brevityFirstFocusableElement = brevityFocusableElements[ 0 ]; brevityLastFocusableElement = brevityFocusableElements[ brevityFocusableElements.length - 1 ]; } } /** * Handle Enter and Space keys on menu toggle button. * * @param {KeyboardEvent} event The keyboard event. */ function brevityHandleToggleKeyboard( event ) { if ( event.key === 'Enter' || event.key === ' ' ) { event.preventDefault(); brevityButton.click(); } } /** * Toggle menu open/closed state. */ function brevityToggleMenu() { const brevityIsExpanded = brevityButton.getAttribute( 'aria-expanded' ) === 'true'; brevitySiteNavigation.classList.toggle( 'toggled' ); if ( brevityIsExpanded ) { brevityButton.setAttribute( 'aria-expanded', 'false' ); // Close all submenus when menu closes. const brevityAllSubmenus = brevityMenu.querySelectorAll( 'ul ul' ); for ( const brevitySubmenu of brevityAllSubmenus ) { brevitySubmenu.style.display = ''; } const brevityAllFocusItems = brevityMenu.querySelectorAll( 'li.focus' ); for ( const brevityFocusItem of brevityAllFocusItems ) { brevityFocusItem.classList.remove( 'focus' ); } } else { brevityButton.setAttribute( 'aria-expanded', 'true' ); // Focus first menu item when menu opens. setTimeout( function() { brevityUpdateFocusableElements(); if ( brevityFirstFocusableElement ) { brevityFirstFocusableElement.focus(); } }, 100 ); } } // Toggle the .toggled class and the aria-expanded value each time the button is clicked. brevityButton.addEventListener( 'click', brevityToggleMenu ); // Handle keyboard on toggle button. brevityButton.addEventListener( 'keydown', brevityHandleToggleKeyboard ); // Remove the .toggled class and set aria-expanded to false when the user clicks outside the navigation. document.addEventListener( 'click', function( event ) { const brevityIsClickInside = brevitySiteNavigation.contains( event.target ); if ( ! brevityIsClickInside && brevitySiteNavigation.classList.contains( 'toggled' ) ) { brevitySiteNavigation.classList.remove( 'toggled' ); brevityButton.setAttribute( 'aria-expanded', 'false' ); brevityButton.focus(); } } ); // Toggle focus each time a menu link is focused or blurred. for ( const brevityLink of brevityLinks ) { brevityLink.addEventListener( 'focus', brevityToggleFocus, true ); brevityLink.addEventListener( 'blur', brevityToggleFocus, true ); brevityLink.addEventListener( 'keydown', brevityHandleMenuKeyboard ); } // Toggle focus each time a menu link with children receive a touch event. for ( const brevityLink of brevityLinksWithChildren ) { brevityLink.addEventListener( 'touchstart', brevityToggleFocus, false ); } // Handle focus trap for keyboard navigation. brevitySiteNavigation.addEventListener( 'keydown', brevityHandleFocusTrap ); }() );