jQuery figuring out if parent has lost 'focus'
Asked Answered
A

7

6

I'm stuck on figuring out the logic to make a drop down menu keyboard accessible.

The HTML is structured as such (extra class names used for clarity):

<ul>
    <li class="primaryMenuItem">
        <a href="">Link 1</a>
        <ul class="popUpMenu">
            <li><a href="">Sub Link 1</a></li>
            <li><a href="">Sub Link 2</a></li>
        </ul>
    </li>
    <li class="primaryMenuItem">
        <a href="">Link 2</a>
        <ul class="popUpMenu">
            <li><a href="">Sub Link 1</a></li>
            <li><a href="">Sub Link 2</a></li>
        </ul>
    </li>    
</ul>

Link 1 and Link 2, when hovered, will show the sub-menu lists (pull down menu). I have this working just fine with some jQuery and the jQuery hoverIntent plugin.

The catch is that this only works with the mouse at the moment.

Next challenge is to get this to work via the keyboard.

I can easily add a focus event to the top level links that then trigger the secondary menus:

$('ul.primaryMenuItem a:first').focus([call showMenu function]) 

That works fine.

To close the menu, one option is to, when opening another menu, check to see if there is another open already and, if so, close it.

That also works fine.

Where that fails, however, is if you have the last menu open, and tab out of it. Since you haven't tabbed into another menu, this one stays open.

The challenge is to figure out how/when to close the menu and the logic needed (jQuery) to figure it out. Ideally, I'd close the menu when the focus is on an element on the page OTHER than any of the menu's child elements.

Logically, I'm looking for this:

$('li.primaryMenuItem').blur([close $(this).find('ul.popUpMenu'))

However, you can't do that, since the LI doesn't actually have focus, but rather the anchor tag within it.

Any suggestions?

UPDATE:

perhaps a better/simpler way to ask the question:

Via jQuery, is there a way to 'watch' to see if focus has moved outside of all children of a particular object?

Anacardiaceous answered 3/2, 2010 at 22:12 Comment(1)
Is there a typo? $('ul.primaryMenuItem a:first').focus([call showMenu function]) --> $('li.primaryMenuItem a:first').focus...Roy
I
6

You can use event bubbling to check what has focus on the focusin event. I had success with the following code:


$("li:has(ul.popUpMenu)").focusin(function(e) {
    $(this).children().fadeIn('slow');
  });
  $('body').focusin(function(e) {
    if (!$(e.target).parent().is('ul.popUpMenu li')) {
      $('ul.popUpMenu').fadeOut('slow');
    }
  });

You could(should) probably make it more optimized, but it works.

Interceptor answered 4/2, 2010 at 0:6 Comment(3)
Interesting! I feel odd, however, attaching an event handler to the body and all child elements of it. Is there any sort of performance issue doing that? Ultimately, your solution is 'on every focus, see if it's in the menu. If not, close it'. Which certainly makes sense.Anacardiaceous
Well, it will trigger the focusin event anytime a focus event occurs within the body, but focus doesn't usually change very quickly, and there'll be a limited number of elements that can be the target of a focus event (links/form elements) so personally I don't think calling this comparison on every focus event will impact performance too much. You could try to optimize the comparison (I'm not sure which is faster $(e.target).is('ul.popUpMenu li a') or example), and you should cache the element query. If performance is a real issue, you need to run some benchmarks to check the impact.Interceptor
focusin won't fire in some browsers for the body tag (or many other tags). Setting the tabindex to -1 seems to fix that and makes this solution a good fit - $("body").attr("tabindex", -1);Pless
H
2

Use the new jquery 1.4 functions: focusin and focusout instead of blur and focus. Here's how focusout differs:

The focusout event is sent to an element when it, or any element inside of it, loses focus. This is distinct from the blur event in that it supports detecting the loss of focus from parent elements (in other words, it supports events bubbling).

Harlanharland answered 3/2, 2010 at 22:35 Comment(1)
@Harlanharland I looked at that. However, that's not what I need. I want to know if the parent container has lost focus. focusout will trigger if ANY of the child elements lose focus. Which means that tabbing from sub-menu to sub-menu would trigger that event. I need something along the lines of 'has the person tabbed outside the container' type event.Anacardiaceous
C
2

How about if you do the following:

$('#link_A_id, #link_A_id > *').focusout(function () {
    if ($(document.activeElement).closest('#link_A_id').length == 0)
        //focus is out of link A and it's children
});
Cellar answered 12/1, 2012 at 3:2 Comment(1)
+1 This got me going in the right direction. I don't see why the second selector (#link_A_id > *) would be needed and I didn't use it. I also had to wrap the if statement in a timeout because the body element steals the focus before the next element gains focus.Quadratics
W
1

I came up with this recipe (vanilla JS) which does exactly what the OP is asking for (provide a way to listen to "focus moves outside a container element") and is generic enough to work for any use case.

In OP's example, it would be used like this:

for (const primaryMenu of Array.from($('.primaryMenuItem'))) {
  onFocusOutsideOf(primaryMenu, () => closeMenu(primaryMenu));
}

Here is the code for onFocusOutsideOf:

/**
 * Invokes `callback` when focus moves outside the given element's DOM
 * subtree.
 *
 * Returns a function that can optionally be called to remove the
 * event listener.
 */
function onFocusOutsideOf(container, callback) {
  const eventListener = () => {
    // setTimeout is used to allow `document.activeElement` to
    // be updated to the newly focused element. This is needed
    // since the 'focusout' event within the container happens
    // before the 'focus' event on the new element.
    setTimeout(() => {
      if (!isDescendantOf(container, document.activeElement)) {
        callback();
      }
    })
  };
  container.addEventListener('focusout', eventListener);

  return function unsubscribe() {
    container.removeEventListener('focusout', eventListener);
  }
}

/**
 * Utility function which returns whether a given DOM element
 * has another DOM element as a descendant.
 */
function isDescendantOf(ancestor: Element, potentialDescendant: Element) {
  let parent = potentialDescendant.parentNode;
  while (parent) {
    if (parent === ancestor) return true;
    parent = parent.parentElement;
  }
  return false;
}
Wiggler answered 8/6, 2020 at 19:32 Comment(0)
T
0

Try this

$('li.primaryMenuItem:last li a:last').blur([do whatever you need to do])

Logically, if your user tabs out he must have been focusing the last anchor.

You could even set up your own event handler like so:

$('li.primaryMenuItem:last').bind('myblur', function() ...);

and call it within the last anchors blur event:

...blur(function() {
    $(this).parents('li.primaryMenuItem').trigger('myblur'); ...
Tuppence answered 3/2, 2010 at 23:0 Comment(1)
The problem with that is there are many ways to leave the menu before tabbing out of the last item. For instance, you could be tabbing backwards, which means you could tab out of the last item, but still be in the same menu. In addition, one could use a keyboard command or mouse click to blur while halfway through the menu.Anacardiaceous
J
0

This helped me... http://plugins.jquery.com/project/focus

It will detect if you're still within the parent automatically. It basically changes jQuery focusout to work this way instead, which I feel is how it should work.

<div class="parent">
   <input type="text" />
   <input type="text" />
</div>

$('#parent').focusout(function () {
    console.log('focusout of parent');
});

I don't see why pressing tab to move textfield between the child elements should trigger focusout on the parent because you're still within that parent. Something must be happening that takes you out of it for a moment and I suspect it's a bug... anyone with me on this? Well anyway the plugin above fixes it. Just include it before your code to 'fix' this. Would love someone to explain why this isn't a bug if it isn't.

Thanks, Dom

Justification answered 12/9, 2011 at 9:40 Comment(0)
A
0

I had a similar issue... I created a jsfiddle to determine when a parent fieldset loses focus and then calling a function. It could certainly be optimized, but it's a start.

http://jsfiddle.net/EKhLc/10/

function saveFields() {
  $.each($('fieldset.save'),function(index, value) {
    // WHERE THE POST WOULD GO
    alert('saving fieldset with id '+ value.id);
    $(value).removeClass('save');
  });

}
$('.control-group').focusin(function(){
  var thefield = $(this).parent('fieldset');
  if (!thefield.hasClass('active')) {
    if($('fieldset.active').length > 0){

      $('fieldset.active').removeClass('active').addClass('save');
      saveFields();
      }
    thefield.addClass('active');
    } else {
        console.log('already active');
    }
});
Auriculate answered 10/4, 2013 at 22:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.