题图为作者拍于墨石公园
碰到一个算是比较常见问题,如下图:
😏 需求:显而易见,当 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 创建,注意以下两点:
- Menu 这里使用三角形是必要的,因为若使用矩形,区域太大,会影响用户选择其他菜单项。
- 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
,高度为 svgHeight
,path
绘制的三角形在其中间,如图:
- 这里
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...
如若转载请附上原文地址,以便更新溯源。