I'm adding keyboard navigation to the main menu on my site, and I want any open submenus to close when the user tabs outside of the main menu. So I have the following function:
var $nav = $(".navigation nav.main"),
$navItems = $nav.find("a");
$navItems.on("blur", function() {
if ($nav.find("a:focus").length === 0) {
closeMenu();
}
});
The way that I would expect this to work is like this:
But what actually happens is $(nav.find("a:focus").length)
always evaluates to 0
, even when I can see directly on screen that there is indeed a link with focus within the menu.
However, if I wrap a setTimeout()
around the conditional, then it works how I would expect:
var $nav = $(".navigation nav.main"),
$navItems = $nav.find("a");
$navItems.on("blur", function() {
setTimeout(function() {
if ($nav.find("a:focus").length === 0) {
closeMenu();
}
});
});
Now, $nav.find("a:focus").length
evalulates to 1
every time I hit Tab, until I tab outside of the menu, at which point it evaluates to 0
and closes it.
This is exactly how I want it to work, but why is the setTimeout()
necessary?
Why doesn't it work?
As epascarello points out in the comments, your blur
is happening before the next element has focus.
Why does
setTimeout()
, even with a0ms
delay, work?
I believe this has to do with the rendering engine. When you do a setTimeout()
, the function gets "queued" behind the rendering engine's UI update (which focuses the next element). Despite there being no difference in time, the setTimeout()
ensures the check happens after the UI update.
Order of events:
1. Fire blur event
2. Update UI
Order of events with a setTimeout()
:
1. Fire blur event (queue new function) ──┐
2. Update UI |
3. Run the queued function <───────────┘
Is there an alternative?
The blur
event's relatedTarget
indicates which element will be receiving focus.
By knowing the element that's about to receive focus, you could use jQuery's .filter()
to determine if it's a nav item or not.
var $nav = $(".navigation nav.main"),
$navItems = $nav.find("a");
$navItems.on("blur", function(e) {
var $focusedElement = $(e.relatedTarget);
var isNavItem = !!$navItems.filter($focusedElement).length;
if (!isNavItem)
console.log("The focused element is not a nav item.");
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="navigation">
<nav class="main">
<a href="#">Link 1</a>
<a href="#">Link 2</a>
</nav>
</div>
<a href="#">Non-nav link</a>