实现一个Select/Popover

641 阅读2分钟

需求:

  • 点击一个按钮,它弹出一个浮层。
  • 浮层上面可以作点、选、输入、确定等操作。其中一些操作可以引起浮层关闭。
  • 点击浮层外部时,该浮层关闭。

实现

一、弹出浮层

状态里存个show: false
点击按钮切换这个show的值。
show===true时,显示浮层。

二、主动操作 · 关闭浮层

需要关闭时,让show = false

三、点击浮层外部,关闭浮层

关键点就在这里。我们不知道什么时候用户点击了浮层的外部。
如果用监听windowclick事件冒泡,有可能现在,或是将来,被一些阻止冒泡的元素截断该事件。
所以我采用了聚焦/失焦。

<div tabindex="0" class="box">
  <div>触发按钮</div>
  <div>浮层</div>
</div>

现在监听.box元素,如果该元素blur(失焦)了,show = false,浮层关闭。
因为浮层在.box元素里,所以点击浮层外部时,即触发该元素的blur
目前为止,如果你在浮层里面只放了div等不会focus(聚焦)的元素,它已足以工作。这可以用来制造一个简单的select组件。

问题

抢焦

依据上文第三步,可知,失焦时会导致关浮层。
假如在.box元素里,放置了会focus的元素input,那么当点击input使之focus时,会触发.boxblur,使得产生 点击浮层内input时,关闭了浮层 的不合理效果。

解决思路:冒泡思路。在监听.boxblur事件时,如果该事件来自子元素,那就不关闭浮层。但这用冒泡技术做不到,因为focusblur事件不冒泡。
现在的重点是,要在blur时,知道这事是不是子孙干的。
有两样东西可以做到这一点

  • document.activeElement。获取当前focus的dom元素
  • domA.contains(domB)。检查domB是不是domA的子孙

所以,只要在blur时,检查一下当前网页focus的dom元素是不是.box的子孙,就行了。
但是有一个坏消息,事件顺序在chrome浏览器里如此发生:blur -> focus。也就是说,还来不及focus子孙,就已经先blur了,导致浮层提前关闭,而因为浮层关闭,导致input跟着消失,也就无法再被focus了。

解决思路:调整顺序,让focus,发生得比blur的监听函数早。
事件顺序是板上钉钉的事情,无法改变。但blur的监听函数,可以做点手脚。
setTimeout,把它包在blur监听函数的函数体上,也就是blur监听函数的业务代码上。

function handleBlur(){
  setTimeout(()=>{
    if(聚焦的是当前元素的儿子) return;
    show = false
  })
}

于是,达到了调整顺序效果。该问题解决。
至此,Popover弹出框,安全实现。