Rev Rev - 5 months ago 59
jQuery Question

When an element or any of it's descendants lose focus, remove a class

I'm trying to make my navigation system more accessible, and I'm having a hard time figuring out one part of it. Here's a list of the features I'd like


  • Navigable by keyboard. This is working fine.

  • Click/press enter on a button before a sub-menu to toggle it's active state. This is working fine.

  • Close a sub-menu when it or any of it's descendants lose focus. Here's where I'm having trouble.



The issue is that I can't seem to accurately figure out if the element or it's children have focus. It works okay for the first press of "tab" on the "keyboard," but after it highlights the second element in the child list, the menu closes.

Here's all my JS along with some example CSS and HTML.

JS:

// toggle the is-active class and aria values on button click
jQuery(".menu-list_toggle").bind("click", function(e) {
e.preventDefault();

if (jQuery(this).next(".menu-list[aria-hidden]").attr("aria-hidden") === "true") {
// mark all sibling menus as inactive
jQuery(this).closest(".menu-list_item").siblings().removeClass("is-active");

// mark menu list as active
jQuery(this).closest(".menu-list_item").addClass("is-active");
jQuery(this).next(".menu-list[aria-hidden]").attr("aria-hidden", "false");
} else {
// mark menu list as inactive
jQuery(this).closest(".menu-list_item").removeClass("is-active");
jQuery(this).next(".menu-list[aria-hidden]").attr("aria-hidden", "true");
}
});

// remove is-active and aria values on blur - HAVING TROUBLE HERE!
jQuery(".menu-list").bind("focusout", function() {
if (jQuery(this).has(document.activeElement).length === 0) {
jQuery(this).closest(".menu-list_item").removeClass("is-active");
jQuery(this).attr("aria-hidden", "true");
}
});


CSS:

[aria-hidden=true] {
display: none;
}

.is-active {
background: teal;
}


HTML:

<ul class="menu-list">
<li class="menu-list_item">
<a class="menu-list_link" href="#">Some Link</a>
<button class="menu-list_toggle">Toggle Menu</button>
<ul class="menu-list" aria-hidden="true">
<li class="menu-list_item">
<a class="menu-list_link" href="#">Some Link</a>
</li>
<li class="menu-list_item">
<a class="menu-list_link" href="#">Some Link</a>
<button class="menu-list_toggle">Toggle Menu</button>
<ul class="menu-list" aria-hidden="true">
<li class="menu-list_item">
<a class="menu-list_link" href="#">Some Link</a>
</li>
<li class="menu-list_item">
<a class="menu-list_link" href="#">Some Link</a>
</li>
<li class="menu-list_item">
<a class="menu-list_link" href="#">Some Link</a>
</li>
</ul>
</li>
<li class="menu-list_item">
<a class="menu-list_link" href="#">Some Link</a>
</li>
<li class="menu-list_item">
<a class="menu-list_link" href="#">Some Link</a>
</li>
</ul>
</li>
<li class="menu-list_item">
<a class="menu-list_link" href="#">Some Link</a>
</li>
<li class="menu-list_item">
<a class="menu-list_link" href="#">Some Link</a>
</li>
</ul>


JS Fiddle.



Ideally, I'd like to do this all in vanilla JavaScript instead of jQuery, but I figure I can go back and refactor later once I get the core concept working.

Answer

It's very simple, this is how you can do it:

Also, attach the event listener to .menu-list and delegate the blur events from .menu-list_link to it so you won't end up with many event listeners.

jQuery(".menu-list").on("blur", ".menu-list_link", function(e) {
  var tThis = jQuery(this);
  var tThisParent = tThis.closest('.menu-list_item');
  var tThisGrandParent = tThisParent.closest('.menu-list');
  var tThisGreatGrandParent = tThisGrandParent.closest('.menu-list_item');
  if (tThisParent.is(':last-child')) {
    tThisGrandParent.attr("aria-hidden", "true");
    tThisGreatGrandParent.removeClass("is-active");
  }
});

It closes the sub-menu menu only if the item losing focus is the last item on its corresponding sub-menu.

Demo: https://jsfiddle.net/r0kz0g69/2/


Moreover if you put the whole menu inside a container you can attach the event listener to it instead of .menu-lists, e.g. a .menu-container, so you will only have one event listener on a DOM element for this.

jQuery(".menu-container").on("blur", ".menu-list_link", function() {
  var tThis = jQuery(this);
  var tThisParent = tThis.closest('.menu-list_item');
  var tThisGrandParent = tThisParent.closest('.menu-list');
  var tThisGreatGrandParent = tThisGrandParent.closest('.menu-list_item');
  if (tThisParent.is(':last-child')) {
    tThisGrandParent.attr("aria-hidden", "true");
    tThisGreatGrandParent.removeClass("is-active");
  }
});

Demo 2: https://jsfiddle.net/r0kz0g69/3/


And a small improvement, don't hide the menu list if the item losing focus is the outermost sub-menu, so the outermost menu stays visible.

jQuery(".menu-container").on("blur", ".menu-list_link", function() {
  var tThis = jQuery(this);
  var tThisParent = tThis.closest('.menu-list_item');
  var tThisGrandParent = tThisParent.closest('.menu-list');
  var tThisGreatGrandParent = tThisGrandParent.closest('.menu-list_item');
  if (tThisParent.is(':last-child')) {
    if(!tThisGrandParent.is('.menu-container > .menu-list')) {
        tThisGrandParent.attr("aria-hidden", "true");
    }
    tThisGreatGrandParent.removeClass("is-active");
  }
});

Demo 3: https://jsfiddle.net/r0kz0g69/4/

Comments