从用户体验的角度来看,点击打开某个内容,然后不仅可以通过再次点击该内容来关闭它,还可以点击该内容外部来关闭它,这是一种合理的做法。Kitty Giraudel 刚刚 写了一篇关于此的博文。诀窍在于,一旦内容打开,您就在 window
上附加一个事件处理程序,该处理程序监视事件(例如另一个点击)。如果后续点击未发生在新打开的区域内,则将其关闭。例如,字面意思是 thing.contains(event.target)
。我认为这是一个不错的技巧。
不过,还有很多小细节需要考虑。例如
我们必须停止在切换本身上的点击事件的传播。否则,它会传递到窗口点击监听器,并且由于切换不包含在菜单中,因此在我们尝试打开它时,它会立即关闭后者。
没错。不能这样,否则会破坏整个程序。
我们在 CodePen 上的很多地方都使用了相同的模式。就像 Kitty 一样,我们在 React 中实现了它。在查看我们的实现时,我发现它有一些值得一提的花哨功能。例如,我们的不是一个函数或钩子,而是一个组件包装器,我们像这样使用它
<ClickOutsideDetector
listen
onClickOutside={() => { closeTheThing(); }}
>
A Menu or Modal or something.
</ClickOutsideDetector>
这样,它就是一个通用的包装器,我们可以将其用于任何“点击外部”的操作。这些花哨的功能包括
- 您可以传入
component
属性,以便它不必显示为<div>
,而是您可以根据语义将其设置为任何您想要的内容。 listen
属性允许您切换它当前是否正在积极监听事件。就像一种快速绕过它的方法。- ESC 键按下等同于点击外部。
- 处理触摸事件以及点击事件
- 处理点击外部发生在
<iframe>
中的情况,在这种情况下,window
会发生blur
事件而不是点击事件。 - 允许您传入要忽略的元素,因此,与其使用 Kitty 记录的
stopPropagation
技巧,我们可以针对不会触发点击外部的元素进行具体指定。
这么多小细节!对我来说,这是现实世界开发中完美的例子。您只需要一个小的行为,但最终需要考虑大量因素和边缘情况,而且它永远不会真正完成。就在几个月前,我刚刚修改了我们的组件,因为我们使用的第三方工具更改了它们执行某些操作的方式,从而影响了页面上使用的 iframe。最终,我不得不监视 blur
事件,然后检查 document.activeElement
的 classList
,以查看是否是该元素吞噬了点击外部事件!
无论如何,我在这里发布了一个 我们代码的稍微简化版本。
而且我从 Kitty 的文章中看到了一些我们没有处理的内容,它就在第一句话中
我们需要一种方法来在点击菜单外部或跳出菜单时关闭菜单。
我加粗了重点。别担心,我现在已经在我们的代码中添加了一个 TODO 用于处理这种情况。
如果您想要一个无 JavaScript 的解决方案,您可以只在 body 上设置 tabindex="-1"。
我不太明白。
一个更简单的解决方案是在一个覆盖 100vh 和 100vw 的 div 中关闭该内容,因此,无论点击该内容还是点击该 div,切换效果都会发生。
这正是响应式汉堡菜单应该做的事情:点击小型汉堡图标,您的移动网站会被透明或任何其他纯色背景覆盖,背景中完全居中放置了导航链接(在该 div 的样式中放置了
display: flex
加justify-content: center; align-items: center;
)。当然,该 div 最初是display: none;
即隐藏的。同时,该内容(例如,该导航块中的链接)可以具有
onclick='this.style.display="none"'
,当然,它之前是它应该执行的操作(在新标签页中打开等),如果您只是想让它消失,或者更好的是,onclick='this.parentNode.parentNode.style.display="none"'
用于隐藏效果需要向上跳转 2 个级别的情况,以便使该 div 消失(因为链接的父元素的父元素是导航元素的父元素)。在我看来,我们这里有一个已经通过响应式设计常用的编码实践解决的过时问题,因此无需重新发明轮子或假装需要新的解决方案。
为什么不使用 onblur 事件?
我不太明白。
这可以是一个纯 CSS 的解决方案吗?(ESC 键按下无效!)
网格、z-index、几个伪元素、焦点和 focus-within 的组合。
将 tabindex: -1 和模态上的 foucusout 事件监听器组合在一起,只需更少的步骤即可实现相同的结果。我是否遗漏了什么?