为什么$(“ a:focus”)。length除非包装在setTimeout()中,否则求值为0?

时间:2018-12-07 16:10:39

标签: javascript jquery settimeout onblur onfocus

我正在将键盘导航添加到站点的主菜单中,并且当用户在主菜单之外单击时,我希望任何打开的子菜单都关闭。所以我有以下功能:

var $nav = $(".navigation nav.main"),
    $navItems = $nav.find("a");

$navItems.on("blur", function() {
    if ($nav.find("a:focus").length === 0) {
        closeMenu();
    }
});

我希望它起作用的方式是这样的:

  1. 只要菜单中的任何链接失去焦点...
  2. 如果菜单中没有焦点的没有链接,请关闭菜单

但是实际上发生的是$(nav.find("a:focus").length) 始终的计算结果为0,即使我直接在屏幕上看到菜单中确实存在一个具有焦点的链接。 / p>

但是,如果我将setTimeout()包装在条件中,那么它将按我期望的那样工作:

var $nav = $(".navigation nav.main"),
    $navItems = $nav.find("a");

$navItems.on("blur", function() {
    setTimeout(function() {
        if ($nav.find("a:focus").length === 0) {
            closeMenu();
        }
    });
});

现在,每次我按下 Tab 时,$nav.find("a:focus").length都将评估为1,直到我在菜单外进行标签切换,此时它的值为0,然后关闭它。

这正是我希望它起作用的方式,但是为什么setTimeout()是必需的?

1 个答案:

答案 0 :(得分:2)

  

为什么不起作用?

正如epascarello在评论中指出的那样,您的blur发生在下一个元素被关注之前。

  

为什么setTimeout()即使有0ms的延迟也能工作?

相信这与渲染引擎有关。当您执行setTimeout()时,该函数将在呈现引擎的UI更新(集中在下一个元素上)的后面排队。尽管时间 没有什么区别,但是setTimeout()确保在用户界面更新后 进行检查。

事件顺序:

1. Fire blur event
2. Update UI

带有setTimeout()的事件的顺序:

1. Fire blur event (queue new function) ──┐
2. Update UI                              |
3. Run the queued function    <───────────┘
  

还有其他选择吗?

blur事件的relatedTarget指示哪个元素将成为焦点。

通过知道将要获得焦点的元素,您可以使用jQuery的.filter()来确定它是否是导航项目。

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>