为什么 $("a:focus").length 的计算结果为 0,除非包含在 setTimeout() 中?
Why does $("a:focus").length evaluate to 0 unless wrapped in setTimeout()?
我正在将键盘导航添加到我网站的主菜单中,并且我希望在用户在主菜单之外使用 Tab 键时关闭所有打开的子菜单。所以我有以下功能:
var $nav = $(".navigation nav.main"),
$navItems = $nav.find("a");
$navItems.on("blur", function() {
if ($nav.find("a:focus").length === 0) {
closeMenu();
}
});
我希望它的工作方式是这样的:
- 每当菜单中的任何 link 失去焦点时...
- 如果菜单中没有 link有焦点,关闭菜单
但实际发生的是 $(nav.find("a:focus").length)
总是 求值为 0
,即使我可以直接在屏幕上看到确实有一个 link 焦点位于菜单内。
但是,如果我将 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
,直到我在菜单外按 Tab 键,此时它的计算结果为 0
并关闭它。
这正是我想要的工作方式,但为什么 setTimeout()
是必需的?
Why doesn't it work?
正如 epascarello 在评论中指出的那样,您的 blur
发生在下一个元素获得焦点之前。
Why does setTimeout()
, even with a 0ms
delay, work?
我相信这与渲染引擎有关。当您执行 setTimeout()
时,该函数会 "queued" 落后于 渲染引擎的 UI 更新(关注下一个元素)。尽管 time 没有区别,setTimeout()
确保检查发生 after UI update.
事件顺序:
1. Fire blur event
2. Update UI
具有 setTimeout()
的事件顺序:
1. Fire blur event (queue new function) ──┐
2. Update UI |
3. Run the queued function <───────────┘
Is there an alternative?
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>
我正在将键盘导航添加到我网站的主菜单中,并且我希望在用户在主菜单之外使用 Tab 键时关闭所有打开的子菜单。所以我有以下功能:
var $nav = $(".navigation nav.main"),
$navItems = $nav.find("a");
$navItems.on("blur", function() {
if ($nav.find("a:focus").length === 0) {
closeMenu();
}
});
我希望它的工作方式是这样的:
- 每当菜单中的任何 link 失去焦点时...
- 如果菜单中没有 link有焦点,关闭菜单
但实际发生的是 $(nav.find("a:focus").length)
总是 求值为 0
,即使我可以直接在屏幕上看到确实有一个 link 焦点位于菜单内。
但是,如果我将 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
,直到我在菜单外按 Tab 键,此时它的计算结果为 0
并关闭它。
这正是我想要的工作方式,但为什么 setTimeout()
是必需的?
Why doesn't it work?
正如 epascarello 在评论中指出的那样,您的 blur
发生在下一个元素获得焦点之前。
Why does
setTimeout()
, even with a0ms
delay, work?
我相信这与渲染引擎有关。当您执行 setTimeout()
时,该函数会 "queued" 落后于 渲染引擎的 UI 更新(关注下一个元素)。尽管 time 没有区别,setTimeout()
确保检查发生 after UI update.
事件顺序:
1. Fire blur event
2. Update UI
具有 setTimeout()
的事件顺序:
1. Fire blur event (queue new function) ──┐
2. Update UI |
3. Run the queued function <───────────┘
Is there an alternative?
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>