likes
comments
collection
share

使用安全三角和延迟取消的方法优化 hover 浮层

作者站长头像
站长
· 阅读数 24

题图为作者拍于墨石公园

碰到一个算是比较常见问题,如下图:

使用安全三角和延迟取消的方法优化 hover 浮层

😏 需求:显而易见,当 hover 在对话框上时,显示反馈浮层,使用 hover 或鼠标事件都能实现。

🥲 BUG:由于浮层跟主体之间有空隙,当鼠标移动向浮层时触发了 MouseLeave 事件,以至于鼠标没来及移动到浮层上,浮层就消失掉了。

以下是两种解决思路:

方案一:延迟取消

这是个比较简单的方案,可快速实现:当鼠标离开主体时使用 debounce 延迟触发 onMouseLeave 函数,在此期间如果鼠标移动进浮层,则再取消 onMouseLeave。

以 react 为例,伪代码如下:

const [isShowPopover, setisShowPopover] = useState(false);

const handleMouseLeave = debounce(() => setIsShowFeedbackButton(false), 500);
const handleMouseEnter = () => {
	handleMouseLeave.cancel(); // 如果触发了enter事件,则取消延迟执行的leave函数
	setisShowPopover(true);
};

return (
	<div
		className="container"
		onMouseEnter={handleMouseEnter}
		onMouseLeave={handleMouseLeave}
	>
		Some Text
		{/* 浮层 */}
		<Popover style=style={{display: isShowPopover ? 'inline-block' : 'none'}}>
			Some Buttons
		</Popover>
	</div>
);

完成后效果如下:

使用安全三角和延迟取消的方法优化 hover 浮层

简单完美优雅,但略显无趣,我们看看另一种解决方案。

方案二:安全三角

这是一个在 Menu 组件上常见到的方案,主要思路是增加一个不可见的安全元素,填平浮层和主体间的空隙,保证鼠标在两者间移动时不发生其他事件(例如误触发 MouseLeave 或者误触发其他菜单项的 MouseEnter)。

在 Menu 组件中的示意图如下:

使用安全三角和延迟取消的方法优化 hover 浮层

图里这个绿色的三角可以使用 SVG 创建,注意以下两点:

  1. Menu 这里使用三角形是必要的,因为若使用矩形,区域太大,会影响用户选择其他菜单项。
  2. SVG 仅由 path 元素构成,中间是空的,所以需要使用 pointer-event: auto 来保证这块三角区域不会发生事件穿透(避免在 path 内部发生 onMouseLeave 事件)。

另外,本文开头的对话框需求的浮层问题直接使用矩形作为安全元素就可以解决,不用三角形就也不用使用 SVG,直接一个空 div 即可,但字数太少难度太低,所以我们以 Menu 为例实现这个三角吧,伪代码如下:

<svg
  style={{
    position: "fixed",
    width: svgWidth,
    height: submenuHeight,
    pointerEvents: "none",
    zIndex: 2,
    top: submenuY,
    left: mouseX - 2
  }}
  id="svg-safe-area"
>
  {/* Safe Area */}
  <path
    pointerEvents="auto"
    stroke="red"
    strokeWidth="0.4"
    fill="rgb(114 140 89 / 0.3)"
    d={
      `M 0, ${mouseY-submenuY} 
        L ${svgWidth},${svgHeight}
        L ${svgWidth},0 
        z`
    }
  />
</svg>

脑海里想象一个矩形,它的左下角是坐标起点 0,0,宽度为 svgWidth,高度为 svgHeightpath 绘制的三角形在其中间,如图:

使用安全三角和延迟取消的方法优化 hover 浮层

  • 这里0,0是矩形的起点,可以通过选中菜单项(鼠标)的位置和其子菜单项的高确定。
  • 0, ${mouseY-submenuY} 是三角形路径的起点,也就是鼠标所在菜单项的中央位置。
  • 接着画两条线:L ${svgWidth},${svgHeight} 和 L ${svgWidth},0。第一条线(line, L)链接向矩形右上角的坐标,第二条线链接向矩形右下角的坐标。
  • z 表示闭合整个路径,这样就形成了三角形,大功告成 🎉。

如此一来,把这个三角形作为 SafeArea 组件放进菜单项就行:

const SafeAreaNestedOption = () => {
  const [open, setOpen] = useState<boolean>(false);
  const parent = useRef<HTMLLIElement>(null);
  const child = useRef<HTMLDivElement>(null);
  const getTop = useCallback(() => {
    const height = child.current?.offsetHeight;
    return height ? `-${height / 2 - 15}px` : 0;
  }, [child]);

  return (
    <li
      ref={parent}
      style={{ position: "relative" }}
      onMouseEnter={() => setOpen(true)}
      onMouseLeave={() => setOpen(false)}
    >
      <NestedPlaceholder />
      {/* Safe mouse area */}
      {/* This is where the magic will happen */}
      {open && parent.current && child.current && (
        <SafeArea anchor={parent.current} submenu={child.current} />
      )}
      {/* Nested elements as children */}
      <div
        style={{
          visibility: open ? "visible" : "hidden",
          position: "absolute",
          left: parent.current?.offsetWidth || 0,
          top: getTop()
        }}
        ref={child}
      >
        <ul>
          <li>Nested Option 1</li>
          <li>Nested Option 2</li>
          <li>Nested Option 3</li>
          <li>Nested Option 4</li>
        </ul>
      </div>
    </li>
  );
};

这样就构建完成了一个用户交互十分友好的安全三角区域,既不会影响用户选择其他菜单项,又能保证用户在鼠标斜着滑向子菜单时不会出现意外😀。

关于更多安全三角的内容,可以参考该文章: www.smashingmagazine.com/2023/08/bet…

回见。

本文首发于个人博客: www.ferecord.com/use-safe-tr… 如若转载请附上原文地址,以便更新溯源。