views:

728

answers:

3

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?

+1  A: 

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).

Keltex
@Keltex 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.
DA
A: 

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'); ...
aefxx
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.
DA
+1  A: 

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.

emmychan
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.
DA
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.
emmychan