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

题图为作者拍于墨石公园

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

😏 需求:显而易见,当 hover 在对话框上时,显示反馈浮层,使用 hover 或鼠标事件都能实现。 🥲 BUG:由于浮层跟主体之间有空隙,当鼠标移动向浮层时触发了 MouseLeave 事件,以至于鼠标没来及移动到浮层上,浮层就消失掉了。

以下是两种解决思路:

方案一:延迟取消

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

以 react 为例,伪代码如下:

jsx 复制代码
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>
);

完成后效果如下:

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

方案二:安全三角

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

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

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

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

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

jsx 复制代码
<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 绘制的三角形在其中间,如图:

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

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

jsx 复制代码
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...

如若转载请附上原文地址,以便更新溯源。

相关推荐
_大菜鸟_18 分钟前
修改element-ui-时间框date 将文字月份改为数字
javascript·vue.js·ui
尽兴-27 分钟前
Vue 中父子组件间的参数传递与方法调用
前端·javascript·vue.js·父子组件·参数传递·组件调用
JerryXZR29 分钟前
Vue开发中常见优化手段总结
前端·javascript·vue.js·vue性能优化
堕落年代29 分钟前
Vue3的双向数据绑定
前端·javascript·vue.js
一撮不知名的呆毛1 小时前
Ajax局部刷新,异步请求
前端·javascript·ajax
好奇的菜鸟2 小时前
Vue.js 中 v-bind 和 v-model 的用法与异同
前端·javascript·vue.js
-代号95272 小时前
【React】一、JSX的使用
前端·react.js·前端框架
uhakadotcom3 小时前
AI搜索引擎的尽头是电商?从perplexity开始卖货说起...
前端·人工智能·后端
selfsuer3 小时前
Element-plus 【el-input输入框】和【el-select下拉选择框】样式修改
前端·javascript·vue.js
咔叽布吉4 小时前
【前端学习笔记】ES6 新特性
前端·笔记·学习