All files / base/navigation listeners.ts

0% Statements 0/114
0% Branches 0/46
0% Functions 0/25
0% Lines 0/113

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221                                                                                                                                                                                                                                                                                                                                                                                                                                                         
import {
  expandDropdown,
  collapseDropdown,
  setFocusable,
  setActiveDropdown,
  setupAnimationStart,
} from "./dropdownUtils";
 
const ANIMATION_DURATION = 333;
 
export const initNavigationListeners = () => {
  const navigation = document.querySelector(
    ".p-navigation--sliding"
  )! as HTMLElement;
  const menuButton = document.querySelector(
    ".p-navigation__banner .p-navigation__toggle--open"
  )! as HTMLElement;
  // all-canonical-link has it's own toggle listener coming from global-nav
  const toggles = [
    ...document.querySelectorAll(
      ".p-navigation__nav .p-navigation__link[aria-controls]:not(.js-back-button)"
    ),
  ]
    .map((element) => element as HTMLElement)
    .filter((htmlElement) => htmlElement.id !== "all-canonical-link");
  const topNavItemsLists = document.querySelectorAll(
    ".p-navigation__nav > .p-navigation__items"
  );
  const dropdownLinksLists = document.querySelectorAll(
    ".p-navigation__dropdown"
  );
  const allCanonicalLink = document.getElementById("all-canonical-link")!;
 
  // global-nav related elements to fix certain problems for mobile view
  const desktopGlobalNav = document.getElementById("all-canonical-desktop")!;
  const overlayGlobalNav = document.getElementById("all-canonical-overlay")!;
  const togglesMobileGlobalNav = [
    ...document.querySelectorAll(
      "#all-canonical-mobile button.global-nav__header-link-anchor"
    ),
  ].map((element) => element as HTMLElement);
 
  const collapseAllDropdowns = (excludedToggle?: HTMLElement) => {
    toggles.forEach((toggle) => {
      const ariaControls = toggle.getAttribute("aria-controls");
      if (ariaControls) {
        const target = document.getElementById(ariaControls);
        if (target && !(target === excludedToggle)) {
          collapseDropdown(toggle, target);
        }
      }
    });
    // handle the dropdowns from global-nav mobile too
    togglesMobileGlobalNav.forEach((toggle) => {
      const parent = toggle.parentElement;
      const dropdownContents = parent?.querySelector(
        ":scope > .p-navigation__dropdown"
      );
      if (dropdownContents) {
        collapseDropdown(toggle, dropdownContents as HTMLElement);
      }
    });
  };
 
  const closeMenu = () => {
    navigation.classList.add("menu-closing");
 
    const closeMenuHandler = () => {
      navigation.classList.remove("has-menu-open");
      navigation.classList.remove("menu-closing");
    };
 
    // the time is aproximately the time of the sliding animation
    setTimeout(closeMenuHandler, ANIMATION_DURATION);
  };
 
  const resetNavigation = () => {
    collapseAllDropdowns();
    closeMenu();
  };
 
  const unfocusAllLinks = () => {
    // turn off focusability for all dropdown lists in the navigation
    dropdownLinksLists.forEach((list) => {
      const elements = list.querySelectorAll("ul > li > a, ul > li > button");
      elements.forEach((element) => {
        element.setAttribute("tabindex", "-1");
      });
    });
  };
 
  const goBackOneLevel = (e: Event, backButton: HTMLElement) => {
    e.preventDefault();
    const target = backButton.closest(".p-navigation__dropdown");
    if (target && target.parentNode) {
      unfocusAllLinks();
      if (target.parentNode.parentNode) {
        setFocusable(target.parentNode.parentNode as Element);
      }
 
      const links = target.parentNode.querySelector(".p-navigation__link");
      if (links && links instanceof HTMLElement) {
        links.focus();
      }
 
      target.setAttribute("aria-hidden", "true");
      setActiveDropdown(backButton, false);
    }
  };
 
  const handleMenuButtonClick = (e: Event) => {
    e.preventDefault();
 
    if (navigation.classList.contains("has-menu-open")) {
      resetNavigation();
      menuButton.innerHTML = "Menu";
      // remove classes set for desktop global nav
      desktopGlobalNav.classList.remove("show-content");
      overlayGlobalNav.classList.remove("show-overlay");
      // reshow scroll bar
      document.body.style.overflow = "visible";
    } else {
      navigation.classList.add("has-menu-open");
      unfocusAllLinks();
      menuButton.innerHTML = "Close menu";
      for (const topNavItemsList of topNavItemsLists) {
        setFocusable(topNavItemsList);
      }
      // hide scroll bar
      document.body.style.overflow = "hidden";
    }
  };
 
  const handleClickOutsideNavigation = (e: Event) => {
    const target = e.target;
    if (target && target instanceof HTMLElement) {
      // check if the click was outside the navigation
      const topNavigationElement = target.closest(
        ".p-navigation, .p-navigation--sliding, .p-navigation--reduced"
      );
      if (!topNavigationElement || target === allCanonicalLink) {
        // set js-animation-playing on all dropdowns
        setupAnimationStart(toggles, ANIMATION_DURATION);
        resetNavigation();
      }
    }
  };
 
  const handleToggle = (e: Event, toggle: HTMLElement) => {
    e.preventDefault();
 
    setupAnimationStart(toggles, ANIMATION_DURATION);
    const ariaControls = toggle.getAttribute("aria-controls");
    if (ariaControls) {
      const target = document.getElementById(ariaControls);
      if (target && target.parentNode) {
        // check if the toggled dropdown is child of another dropdown
        const isNested = !!(target.parentNode as HTMLElement).closest(
          ".p-navigation__dropdown"
        );
        if (!isNested) {
          collapseAllDropdowns(target);
        }
 
        if (target.getAttribute("aria-hidden") === "true") {
          unfocusAllLinks();
          expandDropdown(toggle, target);
          navigation.classList.add("has-menu-open");
        } else {
          collapseDropdown(toggle, target);
          if (!isNested) {
            closeMenu();
          }
        }
      }
    }
 
    e.stopPropagation();
  };
 
  const handledropdownLinksLists = (e: Event, dropdown: HTMLElement) => {
    if (
      e instanceof KeyboardEvent &&
      e.shiftKey &&
      e.key === "Tab" &&
      window.getComputedStyle(dropdown.children[0], null).display === "none"
    ) {
      const backButton = dropdown.children[1].children[0] as HTMLElement;
      goBackOneLevel(e, backButton);
      const focusElement = dropdown.parentNode?.children[0] as HTMLElement;
      focusElement?.focus({ preventScroll: true });
    }
  };
 
  const handleGoBackOneLevel = (e: Event, backButton: HTMLElement) => {
    goBackOneLevel(e, backButton);
  };
 
  // add listeners to control the navigation
  menuButton.addEventListener("click", handleMenuButtonClick);
  toggles.forEach((toggle) => {
    const handler = (e: Event) => handleToggle(e, toggle);
    toggle.addEventListener("click", handler);
  });
  dropdownLinksLists.forEach((dropdown) => {
    const handler = (e: Event) =>
      handledropdownLinksLists(e, dropdown as HTMLElement);
    dropdown.children[1].addEventListener("keydown", handler);
  });
 
  document.querySelectorAll(".js-back-button").forEach((backButton) => {
    const handler = (e: Event) =>
      handleGoBackOneLevel(e, backButton as HTMLElement);
    backButton.addEventListener("click", handler);
  });
 
  // when clicking outside navigation or in "All Canonical", close all dropdowns
  document.addEventListener("click", handleClickOutsideNavigation);
  allCanonicalLink.addEventListener("click", handleClickOutsideNavigation);
};