second-level-navigation.es6.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. /**
  2. * @file
  3. * Provides functionality for second level submenu navigation.
  4. */
  5. ((Drupal) => {
  6. const { isDesktopNav } = Drupal.olivero;
  7. const secondLevelNavMenus = document.querySelectorAll(
  8. '[data-drupal-selector="primary-nav-menu-item-has-children"]',
  9. );
  10. /**
  11. * Shows and hides the specified menu item's second level submenu.
  12. *
  13. * @param {Element} topLevelMenuItem
  14. * The <li> element that is the container for the menu and submenus.
  15. * @param {boolean} [toState]
  16. * Optional state where we want the submenu to end up.
  17. */
  18. function toggleSubNav(topLevelMenuItem, toState) {
  19. const buttonSelector =
  20. '[data-drupal-selector="primary-nav-submenu-toggle-button"]';
  21. const button = topLevelMenuItem.querySelector(buttonSelector);
  22. const state =
  23. toState !== undefined
  24. ? toState
  25. : button.getAttribute('aria-expanded') !== 'true';
  26. if (state) {
  27. // If desktop nav, ensure all menus close before expanding new one.
  28. if (isDesktopNav()) {
  29. secondLevelNavMenus.forEach((el) => {
  30. el.querySelector(buttonSelector).setAttribute(
  31. 'aria-expanded',
  32. 'false',
  33. );
  34. el.querySelector(
  35. '[data-drupal-selector="primary-nav-menu--level-2"]',
  36. ).classList.remove('is-active-menu-parent');
  37. el.querySelector(
  38. '[data-drupal-selector="primary-nav-menu-🥕"]',
  39. ).classList.remove('is-active-menu-parent');
  40. });
  41. }
  42. button.setAttribute('aria-expanded', 'true');
  43. topLevelMenuItem
  44. .querySelector('[data-drupal-selector="primary-nav-menu--level-2"]')
  45. .classList.add('is-active-menu-parent');
  46. topLevelMenuItem
  47. .querySelector('[data-drupal-selector="primary-nav-menu-🥕"]')
  48. .classList.add('is-active-menu-parent');
  49. } else {
  50. button.setAttribute('aria-expanded', 'false');
  51. topLevelMenuItem.classList.remove('is-touch-event');
  52. topLevelMenuItem
  53. .querySelector('[data-drupal-selector="primary-nav-menu--level-2"]')
  54. .classList.remove('is-active-menu-parent');
  55. topLevelMenuItem
  56. .querySelector('[data-drupal-selector="primary-nav-menu-🥕"]')
  57. .classList.remove('is-active-menu-parent');
  58. }
  59. }
  60. Drupal.olivero.toggleSubNav = toggleSubNav;
  61. /**
  62. * Sets a timeout and closes current desktop navigation submenu if it
  63. * does not contain the focused element.
  64. *
  65. * @param {Event} e
  66. * The event object.
  67. */
  68. function handleBlur(e) {
  69. if (!Drupal.olivero.isDesktopNav()) return;
  70. setTimeout(() => {
  71. const menuParentItem = e.target.closest(
  72. '[data-drupal-selector="primary-nav-menu-item-has-children"]',
  73. );
  74. if (!menuParentItem.contains(document.activeElement)) {
  75. toggleSubNav(menuParentItem, false);
  76. }
  77. }, 200);
  78. }
  79. // Add event listeners onto each sub navigation parent and button.
  80. secondLevelNavMenus.forEach((el) => {
  81. const button = el.querySelector(
  82. '[data-drupal-selector="primary-nav-submenu-toggle-button"]',
  83. );
  84. button.removeAttribute('aria-hidden');
  85. button.removeAttribute('tabindex');
  86. // If touch event, prevent mouseover event from triggering the submenu.
  87. el.addEventListener(
  88. 'touchstart',
  89. () => {
  90. el.classList.add('is-touch-event');
  91. },
  92. { passive: true },
  93. );
  94. el.addEventListener('mouseover', () => {
  95. if (isDesktopNav() && !el.classList.contains('is-touch-event')) {
  96. el.classList.add('is-active-mouseover-event');
  97. toggleSubNav(el, true);
  98. // Timeout is added to ensure that users of assistive devices (such as
  99. // mouse grid tools) do not simultaneously trigger both the mouseover
  100. // and click events. When these events are triggered together, the
  101. // submenu to appear to not open.
  102. setTimeout(() => {
  103. el.classList.remove('is-active-mouseover-event');
  104. }, 500);
  105. }
  106. });
  107. button.addEventListener('click', () => {
  108. if (!el.classList.contains('is-active-mouseover-event')) {
  109. toggleSubNav(el);
  110. }
  111. });
  112. el.addEventListener('mouseout', () => {
  113. if (
  114. isDesktopNav() &&
  115. !document.activeElement.matches(
  116. '[aria-expanded="true"], .is-active-menu-parent *',
  117. )
  118. ) {
  119. toggleSubNav(el, false);
  120. }
  121. });
  122. el.addEventListener('blur', handleBlur, true);
  123. });
  124. /**
  125. * Close all second level sub navigation menus.
  126. */
  127. function closeAllSubNav() {
  128. secondLevelNavMenus.forEach((el) => {
  129. // Return focus to the toggle button if the submenu contains focus.
  130. if (el.contains(document.activeElement)) {
  131. el.querySelector(
  132. '[data-drupal-selector="primary-nav-submenu-toggle-button"]',
  133. ).focus();
  134. }
  135. toggleSubNav(el, false);
  136. });
  137. }
  138. Drupal.olivero.closeAllSubNav = closeAllSubNav;
  139. /**
  140. * Checks if any sub navigation items are currently active.
  141. *
  142. * @return {boolean}
  143. * If sub navigation is currently open.
  144. */
  145. function areAnySubNavsOpen() {
  146. let subNavsAreOpen = false;
  147. secondLevelNavMenus.forEach((el) => {
  148. const button = el.querySelector(
  149. '[data-drupal-selector="primary-nav-submenu-toggle-button"]',
  150. );
  151. const state = button.getAttribute('aria-expanded') === 'true';
  152. if (state) {
  153. subNavsAreOpen = true;
  154. }
  155. });
  156. return subNavsAreOpen;
  157. }
  158. Drupal.olivero.areAnySubNavsOpen = areAnySubNavsOpen;
  159. // Ensure that desktop submenus close when escape key is pressed.
  160. document.addEventListener('keyup', (e) => {
  161. if (e.key === 'Escape' || e.key === 'Esc') {
  162. if (isDesktopNav()) closeAllSubNav();
  163. }
  164. });
  165. // If user taps outside of menu, close all menus.
  166. document.addEventListener(
  167. 'touchstart',
  168. (e) => {
  169. if (
  170. areAnySubNavsOpen() &&
  171. !e.target.matches(
  172. '[data-drupal-selector="header-nav"], [data-drupal-selector="header-nav"] *',
  173. )
  174. ) {
  175. closeAllSubNav();
  176. }
  177. },
  178. { passive: true },
  179. );
  180. })(Drupal);