Utility function to toggle ARIA and data attributes, and manage CSS classes on HTML dropdowns

Viewed 145

I have a dropdown utility function that is mostly working. The issues are with properly handling the toggling of the data-text and aria attributes for the buttons when clicking on the non-target button or outside the target element completely.

I have included my window.addEventListener code, but have commented it out as it is affecting the display of the ul sub menu elements.

I am learning vanilla JavaScript in bits and pieces, so please provide tips and solutions that are simple for a novice JS developer. Thanks.

const navUtility = (() => {
  const buttons = document.querySelectorAll("[data-nav-toggle]");
  const containers = document.querySelectorAll("[data-nav-container]");

  if (!buttons) {
    return;
  }

  function dropdownToggleHandler() {
    buttons.forEach((button) => {
      button.addEventListener("click", toggleContainer);
    });
  }

  function toggleContainer(e) {
    toggleButtonClass(e.target);
    toggleButtonAria(e.target);
    toggleButtonDataText(e.target);
    toggleContainers(e.target);
  }

  function toggleButtonClass(button) {
    if (button.classList.contains("is-active")) {
      button.classList.remove("is-active");
    } else {
      button.classList.add("is-active");
    }
  }

  function toggleButtonAria(button) {
    const expandedValue = button.getAttribute("aria-expanded");
    const setValue = expandedValue === "true" ? "false" : "true";
    button.setAttribute("aria-expanded", setValue);
  }

  function toggleButtonDataText(button) {
    const originalValue = button.getAttribute("data-text-original");
    const swapValue = button.getAttribute("data-text-swap");
    if (swapValue === button.textContent) {
      button.textContent = button.getAttribute("data-text-original");
    } else {
      button.setAttribute("data-text-original", button.textContent);
      button.textContent = button.getAttribute("data-text-swap");
    }
  }

  function toggleContainers(button) {
    const parent = button.closest("[data-nav-container]");
    parent.classList.toggle("is-expanded");
    document.querySelectorAll(".is-expanded").forEach(function(parentExpanded) {
      if (parentExpanded !== parent) {
        parentExpanded.classList.remove("is-expanded");
      }
    });
  }

  dropdownToggleHandler();

  /* window.addEventListener('click', function(e) {
    containers.forEach((container) => {
      if (e.target !== container) {
        container.classList.remove("is-expanded");
      }
    });
  }); */
})();
nav {
  position: relative;
}

nav>ul {
  width: 21rem;
  max-height: 0;
  position: absolute;
  right: auto;
  left: 0;
  z-index: 5;
  visibility: hidden;
  background-color: grey;
  transition: max-height 200ms ease-out;
}

nav.is-expanded>ul {
  max-height: 4000px;
  visibility: visible;
}
<nav class="" data-nav-container="want-to-navigation" aria-label="Site tools navigation">
  <button aria-expanded="false" class="" data-nav-toggle="want-to-navigation" data-text-swap="Close" data-text-original="I Want To...">Close</button>
  <ul id="want-to-navigation" class="menu">
    <li>
      <a>Apply For</a>
      <ul class="sub-menu">
        <li>
          <a href="#">Development Permits</a>
        </li>
        <li>
          <a href="#">Dust Suppressant</a>
        </li>
        <li>
          <a href="#">Fire Permit</a>
        </li>
        <li>
          <a href="#">Employment</a>
        </li>
        <li>
          <a href="#">Subdivision</a>
        </li>
      </ul>
    </li>
  </ul>
</nav>
<nav class="" data-nav-container="quick-links-navigation" aria-label="Site quick links navigation">
  <button aria-expanded="false" class="" data-nav-toggle="quick-links-navigation" data-text-swap="Close" data-text-original="Quicklinks">Close</button>
  <ul id="quick-links-navigation">
    <li>
      <a href="#">Community Guide</a>
    </li>
    <li>
      <a href="#">Notice of Development</a>
    </li>
    <li>
      <a href="#">Workshops &amp; Courses</a>
    </li>
    <li>
      <a href="#">Council Business</a>
    </li>
  </ul>
</nav>

1 Answers

Condition you have added in event listener of window not helping you. Instead of comparing container with event target element, you have to check does container contains event target element. If event is within nav just ignore else toggle container class list for .is-expanded.

Replacing

if (e.target !== container) {
    container.classList.remove("is-expanded");
}

with

if (!container.contains(e.target)) {
    container.classList.remove("is-expanded");
}

would solve your problem.


With quick fix

window.addEventListener('DOMContentLoaded', function(){
  const navUtility = (() => {
    const buttons = document.querySelectorAll("[data-nav-toggle]");
    const containers = document.querySelectorAll("[data-nav-container]");
    
    if (!buttons) {
      return;
    }
    
    function dropdownToggleHandler() {
      buttons.forEach((button) => {
        button.addEventListener("click", toggleContainer);
      });
    }
    
    function toggleContainer(e) {
      toggleButtonClass(e.target);
      toggleButtonAria(e.target);
      toggleButtonDataText(e.target);
      
      
      toggleContainers(e.target);
    }
    
    function toggleButtonClass(button) {
      if (button.classList.contains("is-active")) {
        button.classList.remove("is-active");
      } else {
        button.classList.add("is-active");
      }
    }
    
    function toggleButtonAria(button) {
      const expandedValue = button.getAttribute("aria-expanded");
      const setValue = expandedValue === "true" ? "false" : "true";
      button.setAttribute("aria-expanded", setValue);
    }
    
    function toggleButtonDataText(button) {
      const originalValue = button.getAttribute("data-text-original");
      const swapValue = button.getAttribute("data-text-swap");
      if (swapValue === button.textContent) {
        button.textContent = button.getAttribute("data-text-original");
      } else {
        button.setAttribute("data-text-original", button.textContent);
        button.textContent = button.getAttribute("data-text-swap");
      }
    }
    
    function toggleContainers(button) {
      const parent = button.closest("[data-nav-container]");
      parent.classList.toggle("is-expanded");
      document.querySelectorAll(".is-expanded").forEach(function(parentExpanded) {
        if (parentExpanded !== parent) {
          parentExpanded.classList.remove("is-expanded");
        }
      });
    }
    
    dropdownToggleHandler();
    
    window.addEventListener('click', function(e) {
      containers.forEach((container) => {
        if (!container.contains(e.target)) {
          container.classList.remove("is-expanded");
        }
      });
    });
  })();
})
nav {
  position: relative;
}

nav>ul {
  width: 21rem;
  max-height: 0;
  position: absolute;
  right: auto;
  left: 0;
  z-index: 99;
  visibility: hidden;
  background-color: grey;
  transition: max-height 200ms ease-out;
}

nav.is-expanded>ul {
  max-height: 4000px;
  visibility: visible;
}
<nav tabindex="-1"  class="" data-nav-container="want-to-navigation" aria-label="Site tools navigation">
  <button aria-expanded="false" class="" data-nav-toggle="want-to-navigation" data-text-swap="Close" data-text-original="I Want To...">Close</button>
  <ul id="want-to-navigation" class="menu">
    <li>
      <a>Apply For</a>
      <ul class="sub-menu">
        <li>
          <a href="#">Development Permits</a>
        </li>
        <li>
          <a href="#">Dust Suppressant</a>
        </li>
        <li>
          <a href="#">Fire Permit</a>
        </li>
        <li>
          <a href="#">Employment</a>
        </li>
        <li>
          <a href="#">Subdivision</a>
        </li>
      </ul>
    </li>
  </ul>
</nav>
<nav tabindex="-1" class="" data-nav-container="quick-links-navigation" aria-label="Site quick links navigation">
  <button aria-expanded="false" class="" data-nav-toggle="quick-links-navigation" data-text-swap="Close" data-text-original="Quicklinks">Close</button>
  <ul id="quick-links-navigation">
    <li>
      <a href="#">Community Guide</a>
    </li>
    <li>
      <a href="#">Notice of Development</a>
    </li>
    <li>
      <a href="#">Workshops &amp; Courses</a>
    </li>
    <li>
      <a href="#">Council Business</a>
    </li>
  </ul>
</nav>

This will fix the hiding issue, you need to change the button caption accordingly.

[update]

code minimised to avoid lengthy post

Though this works fine you must try onBlur event to handle such activity rather than checking click on the whole document for the target element.

You can use a label instead of a button since a label allows you to associate with other elements with for attribute.

window.addEventListener("DOMContentLoaded",handleDomOnLoad);function handleDomOnLoad(a){(()=>{let a=document.querySelectorAll("[data-nav-toggle]"),b=document.querySelectorAll("[data-nav-container]");function c(a){d(a.target),e(a.target),f(a.target),g(a.target)}function d(a){a.classList.contains("is-active")?a.classList.remove("is-active"):a.classList.add("is-active")}function e(a){let b=a.getAttribute("aria-expanded");a.setAttribute("aria-expanded","true"===b?"false":"true")}function f(a){a.getAttribute("data-text-original");let b=a.getAttribute("data-text-swap");b===a.textContent?a.textContent=a.getAttribute("data-text-original"):(a.setAttribute("data-text-original",a.textContent),a.textContent=a.getAttribute("data-text-swap"))}function g(a){let b=a.closest("[data-nav-container]");b.classList.toggle("is-expanded"),document.querySelectorAll(".is-expanded").forEach(function(a){a!==b&&a.classList.remove("is-expanded")})}

function handleOnBlur(e) {
  if (!e.currentTarget.contains(e.relatedTarget))
    e.target.classList.remove("is-expanded");
};

document.querySelectorAll('li a[href]').forEach(element => element.addEventListener('click', function(e){
  console.log(e.target.textContent);
  document.querySelectorAll('nav').forEach(n=>n.classList.remove("is-expanded")); //can fetch specific parent by traversing
}))

,a&&(a.forEach(a=>{a.addEventListener("click",c)}),

document.querySelectorAll("[data-nav-container]").forEach(
  a=>{
    a.addEventListener("blur",handleOnBlur)
  }
)

)})()}
*{ font-family: sans-serif; }
nav { position: relative; background-color: #cfcfcf; margin: 5px; padding: 8px 16px; border-radius: 5px; display: inline-block; }
nav>ul { padding: 8px 16px; width: 21rem; position: absolute; right: auto; left: 0; z-index: 5; visibility: hidden; background-color: #cfcfcf78; transition: max-height 200ms ease-out; border-radius: 5px; }
nav.is-expanded>ul { height: auto; visibility: visible; }
nav ul li { margin: 0px; list-style-type: none; }
nav li ul li { padding:10px; }
<nav tabindex="-1" id="nav1" data-nav-container="want-to-navigation" aria-label="Site tools navigation">
  <label aria-expanded="false" for='nav1' class="" data-nav-toggle="want-to-navigation" data-text-swap="Close" data-text-original="I Want To...">Close</label>
  <ul id="want-to-navigation" class="menu">
    <li>
      <a>Apply For</a>
      <ul class="sub-menu">
        <li><a href="#">Development Permits</a></li>
        <li><a href="#">Dust Suppressant</a></li>
        <li><a href="#">Fire Permit</a></li>
        <li><a href="#">Employment</a></li>
        <li><a href="#">Subdivision</a></li>
      </ul>
    </li>
  </ul>
</nav>

Related