Advertisement
Horlaes

Handles toggling the navigation menu for small screens and e

Aug 18th, 2019
189
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.36 KB | None | 0 0
  1. /* global chickenbrainyScreenReaderText */
  2. /**
  3. * File navigation.js.
  4. *
  5. * Handles toggling the navigation menu for small screens and enables TAB key
  6. * navigation support for dropdown menus.
  7. */
  8.  
  9. const KEYMAP = {
  10. TAB: 9,
  11. };
  12.  
  13. if ( 'loading' === document.readyState ) {
  14. // The DOM has not yet been loaded.
  15. document.addEventListener( 'DOMContentLoaded', initNavigation );
  16. } else {
  17. // The DOM has already been loaded.
  18. initNavigation();
  19. }
  20.  
  21. // Initiate the menus when the DOM loads.
  22. function initNavigation() {
  23. initNavToggleSubmenus();
  24. initNavToggleSmall();
  25. }
  26.  
  27. /**
  28. * Initiate the script to process all
  29. * navigation menus with submenu toggle enabled.
  30. */
  31. function initNavToggleSubmenus() {
  32. const navTOGGLE = document.querySelectorAll( '.nav--toggle-sub' );
  33.  
  34. // No point if no navs.
  35. if ( ! navTOGGLE.length ) {
  36. return;
  37. }
  38.  
  39. for ( let i = 0; i < navTOGGLE.length; i++ ) {
  40. initEachNavToggleSubmenu( navTOGGLE[ i ] );
  41. }
  42. }
  43.  
  44. /**
  45. * Initiate the script to process submenu
  46. * navigation toggle for a specific navigation menu.
  47. * @param {Object} nav Navigation element.
  48. */
  49. function initEachNavToggleSubmenu( nav ) {
  50. // Get the submenus.
  51. const SUBMENUS = nav.querySelectorAll( '.menu ul' );
  52.  
  53. // No point if no submenus.
  54. if ( ! SUBMENUS.length ) {
  55. return;
  56. }
  57.  
  58. // Create the dropdown button.
  59. const dropdownButton = getDropdownButton();
  60.  
  61. for ( let i = 0; i < SUBMENUS.length; i++ ) {
  62. const parentMenuItem = SUBMENUS[ i ].parentNode;
  63. let dropdown = parentMenuItem.querySelector( '.dropdown' );
  64.  
  65. // If no dropdown, create one.
  66. if ( ! dropdown ) {
  67. // Create dropdown.
  68. dropdown = document.createElement( 'span' );
  69. dropdown.classList.add( 'dropdown' );
  70.  
  71. const dropdownSymbol = document.createElement( 'i' );
  72. dropdownSymbol.classList.add( 'dropdown-symbol' );
  73. dropdown.appendChild( dropdownSymbol );
  74.  
  75. // Add before submenu.
  76. SUBMENUS[ i ].parentNode.insertBefore( dropdown, SUBMENUS[ i ] );
  77. }
  78.  
  79. // Convert dropdown to button.
  80. const thisDropdownButton = dropdownButton.cloneNode( true );
  81.  
  82. // Copy contents of dropdown into button.
  83. thisDropdownButton.innerHTML = dropdown.innerHTML;
  84.  
  85. // Replace dropdown with toggle button.
  86. dropdown.parentNode.replaceChild( thisDropdownButton, dropdown );
  87.  
  88. // Toggle the submenu when we click the dropdown button.
  89. thisDropdownButton.addEventListener( 'click', ( e ) => {
  90. toggleSubMenu( e.target.parentNode );
  91. } );
  92.  
  93. // Clean up the toggle if a mouse takes over from keyboard.
  94. parentMenuItem.addEventListener( 'mouseleave', ( e ) => {
  95. toggleSubMenu( e.target, false );
  96. } );
  97.  
  98. // When we focus on a menu link, make sure all siblings are closed.
  99. parentMenuItem.querySelector( 'a' ).addEventListener( 'focus', ( e ) => {
  100. const parentMenuItemsToggled = e.target.parentNode.parentNode.querySelectorAll( 'li.menu-item--toggled-on' );
  101. for ( let j = 0; j < parentMenuItemsToggled.length; j++ ) {
  102. toggleSubMenu( parentMenuItemsToggled[ j ], false );
  103. }
  104. } );
  105.  
  106. // Handle keyboard accessibility for traversing menu.
  107. SUBMENUS[ i ].addEventListener( 'keydown', ( e ) => {
  108. // These specific selectors help us only select items that are visible.
  109. const focusSelector = 'ul.toggle-show > li > a, ul.toggle-show > li > button';
  110.  
  111. if ( KEYMAP.TAB === e.keyCode ) {
  112. if ( e.shiftKey ) {
  113. // Means we're tabbing out of the beginning of the submenu.
  114. if ( isfirstFocusableElement( e.target, document.activeElement, focusSelector ) ) {
  115. toggleSubMenu( e.target.parentNode, false );
  116. }
  117. // Means we're tabbing out of the end of the submenu.
  118. } else if ( islastFocusableElement( e.target, document.activeElement, focusSelector ) ) {
  119. toggleSubMenu( e.target.parentNode, false );
  120. }
  121. }
  122. } );
  123.  
  124. SUBMENUS[ i ].parentNode.classList.add( 'menu-item--has-toggle' );
  125. }
  126. }
  127.  
  128. /**
  129. * Initiate the script to process all
  130. * navigation menus with small toggle enabled.
  131. */
  132. function initNavToggleSmall() {
  133. const navTOGGLE = document.querySelectorAll( '.nav--toggle-small' );
  134.  
  135. // No point if no navs.
  136. if ( ! navTOGGLE.length ) {
  137. return;
  138. }
  139.  
  140. for ( let i = 0; i < navTOGGLE.length; i++ ) {
  141. initEachNavToggleSmall( navTOGGLE[ i ] );
  142. }
  143. }
  144.  
  145. /**
  146. * Initiate the script to process small
  147. * navigation toggle for a specific navigation menu.
  148. * @param {Object} nav Navigation element.
  149. */
  150. function initEachNavToggleSmall( nav ) {
  151. const menuTOGGLE = nav.querySelector( '.menu-toggle' );
  152.  
  153. // Return early if MENUTOGGLE is missing.
  154. if ( ! menuTOGGLE ) {
  155. return;
  156. }
  157.  
  158. // Add an initial values for the attribute.
  159. menuTOGGLE.setAttribute( 'aria-expanded', 'false' );
  160.  
  161. menuTOGGLE.addEventListener( 'click', ( e ) => {
  162. nav.classList.toggle( 'nav--toggled-on' );
  163. e.target.setAttribute( 'aria-expanded', 'false' === e.target.getAttribute( 'aria-expanded' ) ? 'true' : 'false' );
  164. }, false );
  165. }
  166.  
  167. /**
  168. * Toggle submenus open and closed, and tell screen readers what's going on.
  169. * @param {Object} parentMenuItem Parent menu element.
  170. * @param {boolean} forceToggle Force the menu toggle.
  171. * @return {void}
  172. */
  173. function toggleSubMenu( parentMenuItem, forceToggle ) {
  174. const toggleButton = parentMenuItem.querySelector( '.dropdown-toggle' ),
  175. subMenu = parentMenuItem.querySelector( 'ul' );
  176. let parentMenuItemToggled = parentMenuItem.classList.contains( 'menu-item--toggled-on' );
  177.  
  178. // Will be true if we want to force the toggle on, false if force toggle close.
  179. if ( undefined !== forceToggle && 'boolean' === ( typeof forceToggle ) ) {
  180. parentMenuItemToggled = ! forceToggle;
  181. }
  182.  
  183. // Toggle aria-expanded status.
  184. toggleButton.setAttribute( 'aria-expanded', ( ! parentMenuItemToggled ).toString() );
  185.  
  186. /*
  187. * Steps to handle during toggle:
  188. * - Let the parent menu item know we're toggled on/off.
  189. * - Toggle the ARIA label to let screen readers know will expand or collapse.
  190. */
  191. if ( parentMenuItemToggled ) {
  192. // Toggle "off" the submenu.
  193. parentMenuItem.classList.remove( 'menu-item--toggled-on' );
  194. subMenu.classList.remove( 'toggle-show' );
  195. toggleButton.setAttribute( 'aria-label', chickenbrainyScreenReaderText.expand );
  196.  
  197. // Make sure all children are closed.
  198. const subMenuItemsToggled = parentMenuItem.querySelectorAll( '.menu-item--toggled-on' );
  199. for ( let i = 0; i < subMenuItemsToggled.length; i++ ) {
  200. toggleSubMenu( subMenuItemsToggled[ i ], false );
  201. }
  202. } else {
  203. // Make sure siblings are closed.
  204. const parentMenuItemsToggled = parentMenuItem.parentNode.querySelectorAll( 'li.menu-item--toggled-on' );
  205. for ( let i = 0; i < parentMenuItemsToggled.length; i++ ) {
  206. toggleSubMenu( parentMenuItemsToggled[ i ], false );
  207. }
  208.  
  209. // Toggle "on" the submenu.
  210. parentMenuItem.classList.add( 'menu-item--toggled-on' );
  211. subMenu.classList.add( 'toggle-show' );
  212. toggleButton.setAttribute( 'aria-label', chickenbrainyScreenReaderText.collapse );
  213. }
  214. }
  215.  
  216. /**
  217. * Returns the dropdown button
  218. * element needed for the menu.
  219. * @return {Object} drop-down button element
  220. */
  221. function getDropdownButton() {
  222. const dropdownButton = document.createElement( 'button' );
  223. dropdownButton.classList.add( 'dropdown-toggle' );
  224. dropdownButton.setAttribute( 'aria-expanded', 'false' );
  225. dropdownButton.setAttribute( 'aria-label', chickenbrainyScreenReaderText.expand );
  226. return dropdownButton;
  227. }
  228.  
  229. /**
  230. * Returns true if element is the
  231. * first focusable element in the container.
  232. * @param {Object} container
  233. * @param {Object} element
  234. * @param {string} focusSelector
  235. * @return {boolean} whether or not the element is the first focusable element in the container
  236. */
  237. function isfirstFocusableElement( container, element, focusSelector ) {
  238. const focusableElements = container.querySelectorAll( focusSelector );
  239. if ( 0 < focusableElements.length ) {
  240. return element === focusableElements[ 0 ];
  241. }
  242. return false;
  243. }
  244.  
  245. /**
  246. * Returns true if element is the
  247. * last focusable element in the container.
  248. * @param {Object} container
  249. * @param {Object} element
  250. * @param {string} focusSelector
  251. * @return {boolean} whether or not the element is the last focusable element in the container
  252. */
  253. function islastFocusableElement( container, element, focusSelector ) {
  254. const focusableElements = container.querySelectorAll( focusSelector );
  255. if ( 0 < focusableElements.length ) {
  256. return element === focusableElements[ focusableElements.length - 1 ];
  257. }
  258. return false;
  259. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement