angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

touch-keyboard-navigation.js (9465B)


      1 /**
      2  * Touch & Keyboard navigation.
      3  *
      4  * Contains handlers for touch devices and keyboard navigation.
      5  */
      6 
      7 (function() {
      8 
      9 	/**
     10 	 * Debounce.
     11 	 *
     12 	 * @param {Function} func
     13 	 * @param {number} wait
     14 	 * @param {boolean} immediate
     15 	 */
     16 	function debounce(func, wait, immediate) {
     17 		'use strict';
     18 
     19 		var timeout;
     20 		wait      = (typeof wait !== 'undefined') ? wait : 20;
     21 		immediate = (typeof immediate !== 'undefined') ? immediate : true;
     22 
     23 		return function() {
     24 
     25 			var context = this, args = arguments;
     26 			var later = function() {
     27 				timeout = null;
     28 
     29 				if (!immediate) {
     30 					func.apply(context, args);
     31 				}
     32 			};
     33 
     34 			var callNow = immediate && !timeout;
     35 
     36 			clearTimeout(timeout);
     37 			timeout = setTimeout(later, wait);
     38 
     39 			if (callNow) {
     40 				func.apply(context, args);
     41 			}
     42 		};
     43 	}
     44 
     45 	/**
     46 	 * Add class.
     47 	 *
     48 	 * @param {Object} el
     49 	 * @param {string} cls
     50 	 */
     51 	function addClass(el, cls) {
     52 		if ( ! el.className.match( '(?:^|\\s)' + cls + '(?!\\S)') ) {
     53 			el.className += ' ' + cls;
     54 		}
     55 	}
     56 
     57 	/**
     58 	 * Delete class.
     59 	 *
     60 	 * @param {Object} el
     61 	 * @param {string} cls
     62 	 */
     63 	function deleteClass(el, cls) {
     64 		el.className = el.className.replace( new RegExp( '(?:^|\\s)' + cls + '(?!\\S)' ),'' );
     65 	}
     66 
     67 	/**
     68 	 * Has class?
     69 	 *
     70 	 * @param {Object} el
     71 	 * @param {string} cls
     72 	 *
     73 	 * @returns {boolean} Has class
     74 	 */
     75 	function hasClass(el, cls) {
     76 
     77 		if ( el.className.match( '(?:^|\\s)' + cls + '(?!\\S)' ) ) {
     78 			return true;
     79 		}
     80 	}
     81 
     82 	/**
     83 	 * Toggle Aria Expanded state for screenreaders.
     84 	 *
     85 	 * @param {Object} ariaItem
     86 	 */
     87 	function toggleAriaExpandedState( ariaItem ) {
     88 		'use strict';
     89 
     90 		var ariaState = ariaItem.getAttribute('aria-expanded');
     91 
     92 		if ( ariaState === 'true' ) {
     93 			ariaState = 'false';
     94 		} else {
     95 			ariaState = 'true';
     96 		}
     97 
     98 		ariaItem.setAttribute('aria-expanded', ariaState);
     99 	}
    100 
    101 	/**
    102 	 * Open sub-menu.
    103 	 *
    104 	 * @param {Object} currentSubMenu
    105 	 */
    106 	function openSubMenu( currentSubMenu ) {
    107 		'use strict';
    108 
    109 		// Update classes.
    110 		// classList.add is not supported in IE11.
    111 		currentSubMenu.parentElement.className += ' off-canvas';
    112 		currentSubMenu.parentElement.lastElementChild.className += ' expanded-true';
    113 
    114 		// Update aria-expanded state.
    115 		toggleAriaExpandedState( currentSubMenu );
    116 	}
    117 
    118 	/**
    119 	 * Close sub-menu.
    120 	 *
    121 	 * @param {Object} currentSubMenu
    122 	 */
    123 	function closeSubMenu( currentSubMenu ) {
    124 		'use strict';
    125 
    126 		var menuItem     = getCurrentParent( currentSubMenu, '.menu-item' ); // this.parentNode
    127 		var menuItemAria = menuItem.querySelector('a[aria-expanded]');
    128 		var subMenu      = currentSubMenu.closest('.sub-menu');
    129 
    130 		// If this is in a sub-sub-menu, go back to parent sub-menu.
    131 		if ( getCurrentParent( currentSubMenu, 'ul' ).classList.contains( 'sub-menu' ) ) {
    132 
    133 			// Update classes.
    134 			// classList.remove is not supported in IE11.
    135 			menuItem.className = menuItem.className.replace( 'off-canvas', '' );
    136 			subMenu.className  = subMenu.className.replace( 'expanded-true', '' );
    137 
    138 			// Update aria-expanded and :focus states.
    139 			toggleAriaExpandedState( menuItemAria );
    140 
    141 		// Or else close all sub-menus.
    142 		} else {
    143 
    144 			// Update classes.
    145 			// classList.remove is not supported in IE11.
    146 			menuItem.className = menuItem.className.replace( 'off-canvas', '' );
    147 			menuItem.lastElementChild.className = menuItem.lastElementChild.className.replace( 'expanded-true', '' );
    148 
    149 			// Update aria-expanded and :focus states.
    150 			toggleAriaExpandedState( menuItemAria );
    151 		}
    152 	}
    153 
    154 	/**
    155 	 * Find first ancestor of an element by selector.
    156 	 *
    157 	 * @param {Object} child
    158 	 * @param {String} selector
    159 	 * @param {String} stopSelector
    160 	 */
    161 	function getCurrentParent( child, selector, stopSelector ) {
    162 
    163 		var currentParent = null;
    164 
    165 		while ( child ) {
    166 
    167 			if ( child.matches(selector) ) {
    168 
    169 				currentParent = child;
    170 				break;
    171 
    172 			} else if ( stopSelector && child.matches(stopSelector) ) {
    173 
    174 				break;
    175 			}
    176 
    177 			child = child.parentElement;
    178 		}
    179 
    180 		return currentParent;
    181 	}
    182 
    183 	/**
    184 	 * Remove all off-canvas states.
    185 	 */
    186 	function removeAllFocusStates() {
    187 		'use strict';
    188 
    189 		var siteBranding            = document.getElementsByClassName( 'site-branding' )[0];
    190 		var getFocusedElements      = siteBranding.querySelectorAll(':hover, :focus, :focus-within');
    191 		var getFocusedClassElements = siteBranding.querySelectorAll('.is-focused');
    192 		var i;
    193 		var o;
    194 
    195 		for ( i = 0; i < getFocusedElements.length; i++) {
    196 			getFocusedElements[i].blur();
    197 		}
    198 
    199 		for ( o = 0; o < getFocusedClassElements.length; o++) {
    200 			deleteClass( getFocusedClassElements[o], 'is-focused' );
    201 		}
    202 	}
    203 
    204 	/**
    205 	 * Matches polyfill for IE11.
    206 	 */
    207 	if (!Element.prototype.matches) {
    208 		Element.prototype.matches = Element.prototype.msMatchesSelector;
    209 	}
    210 
    211 	/**
    212 	 * Toggle `focus` class to allow sub-menu access on touch screens.
    213 	 */
    214 	function toggleSubmenuDisplay() {
    215 
    216 		document.addEventListener('touchstart', function(event) {
    217 
    218 			if ( event.target.matches('a') ) {
    219 
    220 				var url = event.target.getAttribute( 'href' ) ? event.target.getAttribute( 'href' ) : '';
    221 
    222 				// Open submenu if URL is #.
    223 				if ( '#' === url && event.target.nextSibling.matches('.submenu-expand') ) {
    224 					openSubMenu( event.target );
    225 				}
    226 			}
    227 
    228 			// Check if .submenu-expand is touched.
    229 			if ( event.target.matches('.submenu-expand') ) {
    230 				openSubMenu(event.target);
    231 
    232 			// Check if child of .submenu-expand is touched.
    233 			} else if ( null != getCurrentParent( event.target, '.submenu-expand' ) &&
    234 								getCurrentParent( event.target, '.submenu-expand' ).matches( '.submenu-expand' ) ) {
    235 				openSubMenu( getCurrentParent( event.target, '.submenu-expand' ) );
    236 
    237 			// Check if .menu-item-link-return is touched.
    238 			} else if ( event.target.matches('.menu-item-link-return') ) {
    239 				closeSubMenu( event.target );
    240 
    241 			// Check if child of .menu-item-link-return is touched.
    242 			} else if ( null != getCurrentParent( event.target, '.menu-item-link-return' ) && getCurrentParent( event.target, '.menu-item-link-return' ).matches( '.menu-item-link-return' ) ) {
    243 				closeSubMenu( event.target );
    244 			}
    245 
    246 			// Prevent default mouse/focus events.
    247 			removeAllFocusStates();
    248 
    249 		}, false);
    250 
    251 		document.addEventListener('touchend', function(event) {
    252 
    253 			var mainNav = getCurrentParent( event.target, '.main-navigation' );
    254 
    255 			if ( null != mainNav && hasClass( mainNav, '.main-navigation' ) ) {
    256 				// Prevent default mouse events.
    257 				event.preventDefault();
    258 
    259 			} else if (
    260 				event.target.matches('.submenu-expand') ||
    261 				null != getCurrentParent( event.target, '.submenu-expand' ) &&
    262 				getCurrentParent( event.target, '.submenu-expand' ).matches( '.submenu-expand' ) ||
    263 				event.target.matches('.menu-item-link-return') ||
    264 				null != getCurrentParent( event.target, '.menu-item-link-return' ) &&
    265 				getCurrentParent( event.target, '.menu-item-link-return' ).matches( '.menu-item-link-return' ) ) {
    266 					// Prevent default mouse events.
    267 					event.preventDefault();
    268 			}
    269 
    270 			// Prevent default mouse/focus events.
    271 			removeAllFocusStates();
    272 
    273 		}, false);
    274 
    275 		document.addEventListener('focus', function(event) {
    276 
    277 			if ( event.target.matches('.main-navigation > div > ul > li a') ) {
    278 
    279 				// Remove Focused elements in sibling div.
    280 				var currentDiv        = getCurrentParent( event.target, 'div', '.main-navigation' );
    281 				var currentDivSibling = currentDiv.previousElementSibling === null ? currentDiv.nextElementSibling : currentDiv.previousElementSibling;
    282 				var focusedElement    = currentDivSibling.querySelector( '.is-focused' );
    283 				var focusedClass      = 'is-focused';
    284 				var prevLi            = getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ).previousElementSibling;
    285 				var nextLi            = getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ).nextElementSibling;
    286 
    287 				if ( null !== focusedElement && null !== hasClass( focusedElement, focusedClass ) ) {
    288 					deleteClass( focusedElement, focusedClass );
    289 				}
    290 
    291 				// Add .is-focused class to top-level li.
    292 				if ( getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ) ) {
    293 					addClass( getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ), focusedClass );
    294 				}
    295 
    296 				// Check for previous li.
    297 				if ( prevLi && hasClass( prevLi, focusedClass ) ) {
    298 					deleteClass( prevLi, focusedClass );
    299 				}
    300 
    301 				// Check for next li.
    302 				if ( nextLi && hasClass( nextLi, focusedClass ) ) {
    303 					deleteClass( nextLi, focusedClass );
    304 				}
    305 			}
    306 
    307 		}, true);
    308 
    309 		document.addEventListener('click', function(event) {
    310 
    311 			// Remove all focused menu states when clicking outside site branding.
    312 			if ( event.target !== document.getElementsByClassName( 'site-branding' )[0] ) {
    313 				removeAllFocusStates();
    314 			} else {
    315 				// Nothing.
    316 			}
    317 
    318 		}, false);
    319 	}
    320 
    321 	/**
    322 	 * Run our sub-menu function as soon as the document is `ready`.
    323 	 */
    324 	document.addEventListener( 'DOMContentLoaded', function() {
    325 		toggleSubmenuDisplay();
    326 	});
    327 
    328 	/**
    329 	 * Run our sub-menu function on selective refresh in the customizer.
    330 	 */
    331 	document.addEventListener( 'customize-preview-menu-refreshed', function( e, params ) {
    332 		if ( 'menu-1' === params.wpNavMenuArgs.theme_location ) {
    333 			toggleSubmenuDisplay();
    334 		}
    335 	});
    336 
    337 	/**
    338 	 * Run our sub-menu function every time the window resizes.
    339 	 */
    340 	var isResizing = false;
    341 	window.addEventListener( 'resize', function() {
    342 		isResizing = true;
    343 		debounce( function() {
    344 			if ( isResizing ) {
    345 				return;
    346 			}
    347 
    348 			toggleSubmenuDisplay();
    349 			isResizing = false;
    350 
    351 		}, 150 );
    352 	} );
    353 
    354 })();