一个看似简单,实际却有点复杂的 tooltip

1,786 阅读3分钟

需求描述:

在如下甘特图的蓝色活动条形区域,鼠标 hover 上后显示详情 tooltip,鼠标离开条形区域后只要不进入 tooltip 则消失

图一

分析需求

1、container 容器与侧边人员容器处会发生明显遮挡问题,因此为了解决层级问题,此处使用 portal 的方式,将 tooltip 组件 portal 外层容器,避免遮挡的发生。

2、由于 trigger 与tooltip 不存在父子关系,则选择 mouse 事件控制 tooltip 的现实与隐藏。

3、tooltip 中具体内容需要网络请求得到。

我能想到的问题及解决方法

1、当 trigger 发生 mouseleave 事件时,tooltip 在此种情况下不消失:当 tooltip 发生 mouseenter 时,该 tooltip 将继续存在。

解决方案1

方案1的问题:当快速移入移出同一个 trigger 时,或两个 trigger 间距极小(本例中为 1px)鼠标在两个元素间移动时,tooltip 的显示都会异常。

正常情况应该如下图所示。

快速移入移出同一个 trigger

trigger 元素间距极小

只要 trigger 与 tooltip 是无缝连接的,因此 trigger 的 leave 事件与 tooltip 的 enter 事件是无时间差发生setTimeout(fn, 0)便可完成监听(实际浏览器执行 fn 回调的时间差为四毫秒)。

因此问题出在 trigger enter 触发的网络请求时间差上。

解决方案2

判断 mouseleave 的方向,如果从 trigger 下方离开则保留 tooltip 组件。

很明显,解决方案2 有明显的问题,被 portal 到外部的tooltip 会根据 trigger 来计算最终位置,当 trigger 的四个方向有任何遮挡时,则会将 tooltip portal 到不会被遮挡的位置;这就说明,tooltip 会出现在 trigger 的上下两个方向

trigger 上方

trigger 上方

思路

来分析一下为什么会有这个显示问题:首先 trigger 的 enter 发生后触发 ajax 请求。在请求未结束前离开元素,过后请求结束。

那么就有一个 ajax 开始到结束的时间差引发的显示问题。

解决方法

如果不能用 leave 事件置 flag 来解决显示问题,那么:

换个思路考虑,如何保证,鼠标不聚焦在 trigger 上,tooltip 一定会消失呢?

由于我的 dom 结构如下:trigger 3,4 在容器 2 中,容器 1 为左侧人员显示区域,及其他相邻区域。

dom 结构图

那么除了 trigger 之外,在其他区域均添加 mousemove 监听事件,当鼠标在 trigger 之外的区域 move 时,则控制 tooltip 显示框消失。

鼠标事件判断

写在最后

其实这并不是一个最优方式,外部元素与一个小小的 tooltip 组件发生耦合,并且耗费浏览器性能监听 mouse 事件。但整个问题从出现到尝试解决的过程倒是比较有趣。有时候换个思路考虑问题,outside the box 也许能够更讨巧。

最后的最后

好奇怎么判断鼠标移出方向吗?一个小提示:可以用对角线斜率与移出点与中心点连线的斜率判断,代码如下:

// 0: up / right: 1 / 2: down / 3: left
const mouseLeaveDirection = (e: any) => {
  const rect = e.target.getBoundingClientRect();
  if (!rect.width) {
      rect.width = rect.right - rect.left;
  }
  if (!rect.height) {
      rect.height = rect.bottom - rect.top;
  }
  // 求各个点坐标
  const x1 = rect.left;
  const y1 = -rect.top;
  const x4 = rect.left + rect.width;
  const y4 = -(rect.top + rect.height);
  const x0 = rect.left + rect.width / 2;
  const y0 = -(rect.top + rect.height / 2);
  // 计算对角线斜率
  const k = (y1 - y4) / (x1 - x4);
  const range = [k, -k];
  const x = e.clientX;
  const y = -e.clientY;
  const kk = (y - y0) / (x - x0);
  if (isFinite(kk) && range[0] < kk && kk < range[1]) {
      return x > x0 ? 1 : 3;
  } else {
      return y > y0 ? 0 : 2;
  }
};