element-plus popover自动收起问题定位

1,794 阅读3分钟

问题现象

有一段代码,使用 element-ui(Vue2)的 popover 组件,popover 面板中有一个 select 下拉框,使用 click 来触发面板

<el-popover placement="bottom" :width="400" trigger="click">
  <span>hello world</span>
  <el-select v-model="value">
    <el-option label="one" value="one"></el-option>
    <el-option label="two" value="two"></el-option>
  </el-select>
  <div>
    <el-button @click="okFn">确定</el-button>
    <el-button @click="cancelFn">取消</el-button>
  </div>

  <template #reference>
    <div>
      <el-button>打开</el-button>
    </div>
  </template>
</el-popover>

把这段代码迁移到 Vue3 时,发现选中 select 下拉框的选项时,popover 面板就自动收起了。

20240804_205717_image.png

根因分析

想知道展开下拉框时触发了什么代码,所以,对event listener打断点。

20240804_203552_image.png

点击下拉选项,发现进入了 @vueuse+core@9.13.0_vue@3.4.24/node_modules/@vueuse/core/index.mjslistener 函数,执行 handler 函数

20240804_204346_image.png

handler 对应的代码在 element-plus@2.7.1_vue@3.4.24/node_modules/element-plus/es/components/tooltip/src/content2.mjs

20240804_204428_image.png

点开 content2.mjs,发现进入了 onClose 函数

20240804_204049_image.png

onClose 对应的代码在 tooltip2.mjs

20240804_204206_image.png

点开 tooltip2.mjs,发现执行了 onClose 函数

20240804_204652_image.png

onClose 代码在 index.mjs

20240804_204729_image.png

进入函数,发现代码位置在 element-plus@2.7.1_vue@3.4.24/node_modules/element-plus/es/hooks/use-delayed-toggle/index.mjs

20240804_205120_image.png

到这里可以看出:

  1. element-pluspopover 组件,依赖 tooltip 组件;
  2. tooltiptrigger 的值不是 hover 时,会触发 close 事件;
  3. 下拉框的 click 事件,触发了 tooltip 组件的 close 事件

看看 Vue3element-pluspopover 是如何实现的:

20240804_215053_image.png

是通过 tooltip 组件实现的。

再对比 Vue2element-ui,发现 Vue2popover 是通过 vue-popper 实现的。

20240804_215256_image.png

看看DOM是啥样的

20240804_205519_image.png

可以看出,select的下拉框,是挂载在#popper-container中,和#app是同级的

再看看popover面板所在的DOM,是和select下拉框同级的

20240804_210606_image.png

为啥两个同级的DOM,下拉框的click会触发popover(tooltip)的close事件呢?

回到一开始进入的回调函数,发现下方的函数中有一行代码:

20240804_211024_image.png

点击下拉框时,会调用这行代码,所以 click 事件就绑定了回调:listener 函数。

总结一下,element 组件执行逻辑是:

点击下拉框,会绑定 click 的事件回调函数。

点击下拉选项,会执行 click 的回调函数:lisntener,以及内部的 handler 函数。handler 函数对应的代码在 tooltip 组件中。

执行 handler 时,由于tooltiptrigger 不为 hover,所以会执行 close 函数。这就导致了 popover 面板被关闭。

解决方案

原因定位出来了,如何解决呢?

还是看回这段代码:

20240804_213435_image.png

composedPath表示调用该侦听器时返回事件路径 MDN 文档

当这个条件满足时,可以让函数提前返回,不执行handler函数

当前的composedPath是这样的:

20240804_213727_image.png

可以看出,这是click事件冒泡时的DOM顺序。当 select 的下拉框在 popover 的内部时,click事件冒泡的 composedPath 会包含 popover,此时 event.composedPath().includes(el) 结果为true

20240804_213810_image.png

所以,这个问题的解决办法就是:将 select 下拉框的 DOM 放到 popover 内部,可以通过修改 el-select组件的 teleported 属性为 false 来实现。

修改后,代码如下:

<el-popover placement="bottom" :width="400" trigger="click">
  <span>hello world</span>

  <el-select v-model="value" :teleported="false">
    <el-option label="one" value="one"></el-option>
    <el-option label="two" value="two"></el-option>
  </el-select>
  <div>
    <el-button @click="okFn">确定</el-button>
    <el-button @click="cancelFn">取消</el-button>
  </div>

  <template #reference>
    <div>
      <el-button>打开</el-button>
    </div>
  </template>
</el-popover>

断点执行结果如下:

20240804_213612_image.png

20240804_213642_image.png

event.composedPath 已经包含了ellistener函数提前返回了。

页面效果如下:

20240804_214838_image.png

总结

  1. Vue3element-plus 相对于 Vue2element-uipopover组件的实现方式有较大变化:不使用vue-popper,而是直接用el-tooltip来实现。
  2. element-pluspopover 组件内部如果使用了el-select组件,需要确保 el-selectteleported 值为false,否则会出现展开select下拉框时,popover组件自动收起。
  3. 当遇到和浏览器事件有关的问题时,可以通过浏览器的event listener来打断点,快速找到事件回调。

相关的github issue

github.com/element-plu…