WordPress/wp-content/themes/twentytwentyone/assets/js/primary-navigation.js
audrasjb 096e903010 Twenty Twenty-One: Check if anchor exists before triggering in-page navigation.
This prevents JavaScript errors when using hash in URLs without matching them to any existing anchor in the page.

Props rixeo, sabernhardt, pbearne.
Fixes #53619.

Built from https://develop.svn.wordpress.org/trunk@52213


git-svn-id: http://core.svn.wordpress.org/trunk@51805 1a063a9b-81f0-0310-95a4-ce76da25c4cd
2021-11-19 13:03:01 +00:00

205 lines
5.9 KiB
JavaScript

/**
* File primary-navigation.js.
*
* Required to open and close the mobile navigation.
*/
/**
* Toggle an attribute's value
*
* @since Twenty Twenty-One 1.0
*
* @param {Element} el - The element.
* @param {boolean} withListeners - Whether we want to add/remove listeners or not.
*/
function twentytwentyoneToggleAriaExpanded( el, withListeners ) {
if ( 'true' !== el.getAttribute( 'aria-expanded' ) ) {
el.setAttribute( 'aria-expanded', 'true' );
twentytwentyoneSubmenuPosition( el.parentElement );
if ( withListeners ) {
document.addEventListener( 'click', twentytwentyoneCollapseMenuOnClickOutside );
}
} else {
el.setAttribute( 'aria-expanded', 'false' );
if ( withListeners ) {
document.removeEventListener( 'click', twentytwentyoneCollapseMenuOnClickOutside );
}
}
}
function twentytwentyoneCollapseMenuOnClickOutside( event ) {
if ( ! document.getElementById( 'site-navigation' ).contains( event.target ) ) {
document.getElementById( 'site-navigation' ).querySelectorAll( '.sub-menu-toggle' ).forEach( function( button ) {
button.setAttribute( 'aria-expanded', 'false' );
} );
}
}
/**
* Changes the position of submenus so they always fit the screen horizontally.
*
* @since Twenty Twenty-One 1.0
*
* @param {Element} li - The li element.
*/
function twentytwentyoneSubmenuPosition( li ) {
var subMenu = li.querySelector( 'ul.sub-menu' ),
rect,
right,
left,
windowWidth;
if ( ! subMenu ) {
return;
}
rect = subMenu.getBoundingClientRect();
right = Math.round( rect.right );
left = Math.round( rect.left );
windowWidth = Math.round( window.innerWidth );
if ( right > windowWidth ) {
subMenu.classList.add( 'submenu-reposition-right' );
} else if ( document.body.classList.contains( 'rtl' ) && left < 0 ) {
subMenu.classList.add( 'submenu-reposition-left' );
}
}
/**
* Handle clicks on submenu toggles.
*
* @since Twenty Twenty-One 1.0
*
* @param {Element} el - The element.
*/
function twentytwentyoneExpandSubMenu( el ) { // jshint ignore:line
// Close other expanded items.
el.closest( 'nav' ).querySelectorAll( '.sub-menu-toggle' ).forEach( function( button ) {
if ( button !== el ) {
button.setAttribute( 'aria-expanded', 'false' );
}
} );
// Toggle aria-expanded on the button.
twentytwentyoneToggleAriaExpanded( el, true );
// On tab-away collapse the menu.
el.parentNode.querySelectorAll( 'ul > li:last-child > a' ).forEach( function( linkEl ) {
linkEl.addEventListener( 'blur', function( event ) {
if ( ! el.parentNode.contains( event.relatedTarget ) ) {
el.setAttribute( 'aria-expanded', 'false' );
}
} );
} );
}
( function() {
/**
* Menu Toggle Behaviors
*
* @since Twenty Twenty-One 1.0
*
* @param {string} id - The ID.
*/
var navMenu = function( id ) {
var wrapper = document.body, // this is the element to which a CSS class is added when a mobile nav menu is open
mobileButton = document.getElementById( id + '-mobile-menu' ),
navMenuEl = document.getElementById( 'site-navigation' );
// If there's no nav menu, none of this is necessary.
if ( ! navMenuEl ) {
return;
}
if ( mobileButton ) {
mobileButton.onclick = function() {
wrapper.classList.toggle( id + '-navigation-open' );
wrapper.classList.toggle( 'lock-scrolling' );
twentytwentyoneToggleAriaExpanded( mobileButton );
mobileButton.focus();
};
}
/**
* Trap keyboard navigation in the menu modal.
* Adapted from Twenty Twenty.
*
* @since Twenty Twenty-One 1.0
*/
document.addEventListener( 'keydown', function( event ) {
var modal, elements, selectors, lastEl, firstEl, activeEl, tabKey, shiftKey, escKey;
if ( ! wrapper.classList.contains( id + '-navigation-open' ) ) {
return;
}
modal = document.querySelector( '.' + id + '-navigation' );
selectors = 'input, a, button';
elements = modal.querySelectorAll( selectors );
elements = Array.prototype.slice.call( elements );
tabKey = event.keyCode === 9;
shiftKey = event.shiftKey;
escKey = event.keyCode === 27;
activeEl = document.activeElement; // eslint-disable-line @wordpress/no-global-active-element
lastEl = elements[ elements.length - 1 ];
firstEl = elements[0];
if ( escKey ) {
event.preventDefault();
wrapper.classList.remove( id + '-navigation-open', 'lock-scrolling' );
twentytwentyoneToggleAriaExpanded( mobileButton );
mobileButton.focus();
}
if ( ! shiftKey && tabKey && lastEl === activeEl ) {
event.preventDefault();
firstEl.focus();
}
if ( shiftKey && tabKey && firstEl === activeEl ) {
event.preventDefault();
lastEl.focus();
}
// If there are no elements in the menu, don't move the focus
if ( tabKey && firstEl === lastEl ) {
event.preventDefault();
}
} );
/**
* Close menu and scroll to anchor when an anchor link is clicked.
* Adapted from Twenty Twenty.
*
* @since Twenty Twenty-One 1.1
*/
document.addEventListener( 'click', function( event ) {
// If target onclick is <a> with # within the href attribute
if ( event.target.hash && event.target.hash.includes( '#' ) ) {
wrapper.classList.remove( id + '-navigation-open', 'lock-scrolling' );
twentytwentyoneToggleAriaExpanded( mobileButton );
// Wait 550 and scroll to the anchor.
setTimeout(function () {
var anchor = document.getElementById(event.target.hash.slice(1));
if ( anchor ) {
anchor.scrollIntoView();
}
}, 550);
}
} );
navMenuEl.querySelectorAll( '.menu-wrapper > .menu-item-has-children' ).forEach( function( li ) {
li.addEventListener( 'mouseenter', function() {
this.querySelector( '.sub-menu-toggle' ).setAttribute( 'aria-expanded', 'true' );
twentytwentyoneSubmenuPosition( li );
} );
li.addEventListener( 'mouseleave', function() {
this.querySelector( '.sub-menu-toggle' ).setAttribute( 'aria-expanded', 'false' );
} );
} );
};
window.addEventListener( 'load', function() {
new navMenu( 'primary' );
} );
}() );