一个很棒的 click outside 解决方案

2020-06-12 13:42:15 +08:00
 mt1992

前言

在公司的一次小组分享会上,组长 给我们分享了一个他在项目中遇到的一个问题。在一个嵌入 iframe 的系统中,当我们点击 Dropdown 展开后,再去点击 iframe 发现无法触发 Dropdown 的 clickOutside 事件,导致 Dropdown 无法收起。

为什么无法触发 clickOutside

目前大多数的 UI 组件库,例如 Element 、Ant Design 、iView 等都是通过鼠标事件来处理, 下面这段是 iView 中的 clickOutside 代码,iView 直接给 Document 绑定了 click 事件,当 click 事件触发时候,判断点击目标是否包含在绑定元素中,如果不是就调用绑定的函数。

bind (el, binding, vnode) {
  function documentHandler (e) {
    if (el.contains(e.target)) {
      return false;
    }
    if (binding.expression) {
      binding.value(e);
    }
  }
  el.__vueClickOutside__ = documentHandler;
  document.addEventListener('click', documentHandler);
}

但 iframe 中加载的是一个相对独立的 Document,如果直接在父页面中给 Document 绑定 click 事件,点击 iframe 并不会触发该事件。

知道问题出现在哪里,接下来我们来思考怎么解决?

给 iframe 的 body 元素绑定事件

我们可以通过一些特殊的方式给 iframe 绑定上事件,但这种做法不优雅,而且也是存在问题的。我们来想想一下这样一个场景,左边是一个侧边栏(导航栏),上面是一个 Header 里面有一些 Dropdown 或是 Select 组件,下面是一个页面区域。但这些页面有的是嵌入 iframe,有些是当前系统的页面。如果使用这种方法,我们在切换路由的时候就要不断的去判断这个页面是否包含 iframe,然后绑定 /解绑事件。但如果 iframe 和当前系统不是同域,那么这种做法是无效的。

添加遮罩层

我们可以通过给 iframe 添加一个透明遮罩层,点击 Dropdown 的时候显示透明遮罩层,点击 Dropdown 之外的区域或遮罩层,就关闭遮罩层并派发 clickOutside 事件,这样虽然可以触发 clickOutside 事件,但存在一个问题,如果用户点击的区域正好是 iframe 页面中的某个按钮,那么第一次点击是不会生效的,这种做法对于交互不是很友好。

通过 focusin 与 focusout 事件

其实我们可以换一种思路,为什么一定要用鼠标事件呢? focusin 与 focusout 事件就很适合处理当前这种情况,当我们点击非绑定的元素时触发 focusout 事件,如果是就添加一个定时器,延时调用我们绑定的函数。当我们点击绑定元素例如 Dropdown 会触发 focusin 事件,这时候我们判断目标是否包含在绑定元素中,如果包含在绑定元素中就清除定时器。

不过使用 focusin 与 focusout 事件需要解决一个问题,那就是要将绑定的元素变成 focusable,那么怎么将元素变成 focusable 呢?通过将 tabindex 属性置为 -1 , 该元素就变成可由代码获取焦点。需要注意的是,元素变成 focusable 后,当它获取焦点浏览器会给它加上高亮样式,如果不需要这种样式可以将 outline 设置为 none 。

不过这种方法虽然很棒,但是也存在一些问题,浏览器兼容性,下面是 MDN 给出的浏览器兼容情况,Firefox 低版本不兼容。

使用 focus-outside 库

focus-outside 是我为了解决上述问题所创建的仓库。使用起来也非常方便,它只有两个方法,bind 与 unbind,它不依赖任何其他库,并且支持为多个元素绑定一个函数。

为什么要给多个元素绑定一个函数,这么做是为了兼容 Element,因为 Element 的 Dropdown 会被插入 body 元素中,它的按钮和容器是分离的,当我们点击按钮显示 Dropdown,当我们点击 Dropdown 区域,这时候按钮会失去焦点触发 focusout 事件。事实上我们并不希望这时关闭 Dropdown,所以我将它们视为同一个绑定源。

这里说明下 Element 为什么要将弹出层放在 body 中,如果直接挂在父元素下,会受到父元素样式的影响。比如父元素有 overflow: hidden,弹出菜单就有可能被隐藏掉。

并且 focus-outside 1.x 版本支持了 key 属性,可以通过 key 将一组不同的函数和元素绑定在一起。

https://github.com/txs1992/focus-outside

775 次点击
所在节点    JavaScript
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/680966

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX