4

Ant Design 4.0 的一些杂事儿 - Trigger 篇

 3 years ago
source link: https://zhuanlan.zhihu.com/p/364776062
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Ant Design 4.0 的一些杂事儿 - Trigger 篇

《豆酱》漫画作者

在 antd 底层有一个基础组件叫做 rc-trigger ,它负责管理弹层相关部分的逻辑。在你使用的 Select、TreeSelect、Tooltip、Popover、Popconfirm、Cascader 等等等组件里都由其进行管理。对于游览器的原生元素而言,你是否点击在组件上是很好判断的事情。但是对于自行实现的弹层组件而言,它会有不少弯路要走。今天,我们就简单看看,关于弹层逻辑的一些问题。

Trigger 组件用法简单看像这样:

<Trigger
  popup={<SomeContent />}
  action="click"
>
  <Button />
</Trigger>
v2-8c4914c94be450abfcea36c337a57a8d_b.jpg
点击弹窗,点击内测不关闭,点击外侧关闭

为了实现点击外侧关闭能力,Trigger 会在 document 上添加 mouseDown 事件监听:

useEffect(() => {
  const onMouseDown = () => {
    closeTrigger(); // 某个实现的方法
  };
  document.addEventListener('mouseDown', onMouseDown);

  return () => {
    document.removeEventListener('mouseDown', onMouseDown);
  }
}, []);

接着,我们需要实现点击弹层内不关闭的效果。最直接的想法就是判断一下点击的元素是不是包含在弹层内。这里可以使用 contains 来进行判断:

const onMouseDown = ({ target }) => {
  if (!popupRef.current.contains(target)) {
    closeTrigger(); // 某个实现的方法
  }
};

然而,仅仅如此是不够的。会有一种情况是,点击的元素虽然不在弹层中,但是它本身确实弹层内容的一部分。最常见的就是嵌套的 Trigger,比如弹层里放一个 Select:

v2-aff05075390247259ee6d3e4a9c756a4_b.jpg
Select 下拉是在外部渲染并非其子元素

antd 中的弹层会在 document.body 上创建一个新的 div 将弹层内容通过 createPortal 在其之中渲染。 这样可以避免目标元素父节点存在 overflow: hidden 导致弹层被切割的情况:

v2-ccd08e435f3093cbde993b870cb6f88e_b.jpg

然而,在两者配合之下就会出现组件部分 DOM 节点不在相同层级中的情况。好在 React 中的 SyntheticEvent 会模拟事件冒泡,即便来自不同的 DOM 层级,只要在 virtual dom tree 中是上下游关系即可冒泡。

在弹层组件上,我们额外添加一个 mouseDown 事件监听:

const lockRef = useRef(false);

const onMouseDown = () => {
  lockRef.current = true;
}

const onMouseUp = () => {
  lockRef.current = false;
}
 
return (
  <div onMouseDown={onMouseDown} onMouseUp={onMouseUp}>
    {popup}
  </div>
);

点击时检测一下是否被锁定:

const onMouseDown = ({ target }) => {
  if (
    !popupRef.current.contains(target) && !lockRef.current
  ) {
    closeTrigger(); // 某个实现的方法
  }
};

看起来不错,点击下拉选项后不会消失了:

v2-70e0dcfb0426397bf50d1f26eeb6563d_b.jpg

然而,如果当弹层组件内组件调用了 stopPropagation 我们在上游则无法感知到它了。

https://github.com/ant-design/ant-design/issues/30116​github.com
v2-fe2364f421aad954f2b8c74134b1722c_b.jpg

在 v4 中,我们为 Select 默认开启了虚拟滚动。为了获得平滑的无白屏闪动效果,我们制作了一根“假的滚动条”。为了防止点击滚动条触发 onMouseDown 事件,我们添加了 stopPropagation 以阻止冒泡。然而,这就导致了在 Popover 中点击虚拟滚动条触发了 document 的点击事件,却没有被 Trigger 捕获到以至于以为当前弹层需要被关闭。

考虑到阻止冒泡还是比较常见的代码逻辑,所以我们并不准备从 rc-virtual-list 下手,让点击事件冒出来。取而代之的是改造 rc-trigger 让事件监听从冒泡阶段,改至捕获阶段:

    <div
--    onMouseDown={onMouseDown}
++    onMouseDownCapture={onMouseDown}
      onMouseUp={onMouseUp}
    >
      {popup}
    </div>

根据事件模型,捕获阶段总是先于子组件处理,因而我们不用担心子组件吃掉这个事件。从而达到冒泡解耦的目的:

v2-c99a932dfd523303ed544583f24b35c9_b.jpg

效果不错,收工。

掐指一算,antd v4 从 alpha 发布以来已经走过一年多时间。在此期间,我们发布了上百个新 feature 和修复了大量 BUG,在此向在背后默默付出的各位 Collaborators 表示衷心的感谢。个人的力量是渺小的,社区的力量是强大的。为开源精神干杯 ~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK