navigation.es6.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. /**
  2. * @file
  3. * Customization of navigation.
  4. */
  5. ((Drupal, once, tabbable) => {
  6. /**
  7. * Checks if navWrapper contains "is-active" class.
  8. *
  9. * @param {Element} navWrapper
  10. * Header navigation.
  11. *
  12. * @return {boolean}
  13. * True if navWrapper contains "is-active" class, false if not.
  14. */
  15. function isNavOpen(navWrapper) {
  16. return navWrapper.classList.contains('is-active');
  17. }
  18. /**
  19. * Opens or closes the header navigation.
  20. *
  21. * @param {object} props
  22. * Navigation props.
  23. * @param {boolean} state
  24. * State which to transition the header navigation menu into.
  25. */
  26. function toggleNav(props, state) {
  27. const value = !!state;
  28. props.navButton.setAttribute('aria-expanded', value);
  29. if (value) {
  30. props.body.classList.add('is-overlay-active');
  31. props.body.classList.add('is-fixed');
  32. props.navWrapper.classList.add('is-active');
  33. } else {
  34. props.body.classList.remove('is-overlay-active');
  35. props.body.classList.remove('is-fixed');
  36. props.navWrapper.classList.remove('is-active');
  37. }
  38. }
  39. /**
  40. * Initialize the header navigation.
  41. *
  42. * @param {object} props
  43. * Navigation props.
  44. */
  45. function init(props) {
  46. props.navButton.setAttribute('aria-controls', props.navWrapperId);
  47. props.navButton.setAttribute('aria-expanded', 'false');
  48. props.navButton.addEventListener('click', () => {
  49. toggleNav(props, !isNavOpen(props.navWrapper));
  50. });
  51. // Close any open sub-navigation first, then close the header navigation.
  52. document.addEventListener('keyup', (e) => {
  53. if (e.key === 'Escape' || e.key === 'Esc') {
  54. if (props.olivero.areAnySubNavsOpen()) {
  55. props.olivero.closeAllSubNav();
  56. } else {
  57. toggleNav(props, false);
  58. }
  59. }
  60. });
  61. props.overlay.addEventListener('click', () => {
  62. toggleNav(props, false);
  63. });
  64. props.overlay.addEventListener('touchstart', () => {
  65. toggleNav(props, false);
  66. });
  67. // Focus trap. This is added to the header element because the navButton
  68. // element is not a child element of the navWrapper element, and the keydown
  69. // event would not fire if focus is on the navButton element.
  70. props.header.addEventListener('keydown', (e) => {
  71. if (e.key === 'Tab' && isNavOpen(props.navWrapper)) {
  72. const tabbableNavElements = tabbable.tabbable(props.navWrapper);
  73. tabbableNavElements.unshift(props.navButton);
  74. const firstTabbableEl = tabbableNavElements[0];
  75. const lastTabbableEl =
  76. tabbableNavElements[tabbableNavElements.length - 1];
  77. if (e.shiftKey) {
  78. if (
  79. document.activeElement === firstTabbableEl &&
  80. !props.olivero.isDesktopNav()
  81. ) {
  82. lastTabbableEl.focus();
  83. e.preventDefault();
  84. }
  85. } else if (
  86. document.activeElement === lastTabbableEl &&
  87. !props.olivero.isDesktopNav()
  88. ) {
  89. firstTabbableEl.focus();
  90. e.preventDefault();
  91. }
  92. }
  93. });
  94. // Remove overlays when browser is resized and desktop nav appears.
  95. window.addEventListener('resize', () => {
  96. if (props.olivero.isDesktopNav()) {
  97. toggleNav(props, false);
  98. props.body.classList.remove('is-overlay-active');
  99. props.body.classList.remove('is-fixed');
  100. }
  101. // Ensure that all sub-navigation menus close when the browser is resized.
  102. Drupal.olivero.closeAllSubNav();
  103. });
  104. // If hyperlink links to an anchor in the current page, close the
  105. // mobile menu after the click.
  106. props.navWrapper.addEventListener('click', (e) => {
  107. if (
  108. e.target.matches(
  109. `[href*="${window.location.pathname}#"], [href*="${window.location.pathname}#"] *, [href^="#"], [href^="#"] *`,
  110. )
  111. ) {
  112. toggleNav(props, false);
  113. }
  114. });
  115. }
  116. /**
  117. * Initialize the navigation.
  118. *
  119. * @type {Drupal~behavior}
  120. *
  121. * @prop {Drupal~behaviorAttach} attach
  122. * Attach context and settings for navigation.
  123. */
  124. Drupal.behaviors.oliveroNavigation = {
  125. attach(context) {
  126. const headerId = 'header';
  127. const header = once('navigation', `#${headerId}`, context).shift();
  128. const navWrapperId = 'header-nav';
  129. if (header) {
  130. const navWrapper = header.querySelector(`#${navWrapperId}`);
  131. const { olivero } = Drupal;
  132. const navButton = context.querySelector(
  133. '[data-drupal-selector="mobile-nav-button"]',
  134. );
  135. const body = document.body;
  136. const overlay = context.querySelector(
  137. '[data-drupal-selector="header-nav-overlay"]',
  138. );
  139. init({
  140. olivero,
  141. header,
  142. navWrapperId,
  143. navWrapper,
  144. navButton,
  145. body,
  146. overlay,
  147. });
  148. }
  149. },
  150. };
  151. })(Drupal, once, tabbable);