我制作了一个像 Mac/Windows 顶部菜单栏一样工作的 React 组件。一切都很好,除了一件事——在触摸屏上,当用户从菜单中选择一个选项时,点击被注册在下面,这会导致一个大问题。
我已经准备了一个沙盒,其中包含最少的可重现示例来表示该问题。要重现问题,请按照以下步骤在移动设备上,因为在桌面上它工作正常:
问题似乎是点击手机有延迟,它不关心是什么触发了它。我隐藏了 Over 框
onPointerUp
无论如何都会在它下面的任何地方发生点击。 event.stopPropagation()
不起作用,因为树层次结构不适用于此处。
这里是组件:
import { useState, useEffect } from "react";
const App = () => {
const [isVisible, setIsVisible] = useState(false);
useEffect(
() => {
const callback = () => setIsVisible(false);
window.addEventListener("pointerdown", callback);
return () => window.removeEventListener("pointerdown", callback);
},
[]
);
return (
<div>
<button
type="button"
onClick={() => {
setIsVisible(true);
}}
onPointerDown={event => event.stopPropagation()}
>
Show Over
</button>
<div>
<button
type="button"
onClick={() => console.log("Clicked under :(")}
>
Under
</button>
{isVisible && (
<div>
<button
type="button"
onPointerDown={event => event.stopPropagation()}
onPointerUp={() => {
setIsVisible(false);
}}
>
Over
</button>
</div>
)}
</div>
</div>
);
};
export default App;
问题可以在带有模拟触摸的 Chrome 开发工具中重现,也可以在 Safari iOS(但并非总是)和三星移动浏览器中重现。还没有在其他浏览器中检查过。
编辑
我发现了一个不完美的解决方案,我想改进它。我捕获了一个
onClick
事件并在 200 毫秒后删除了监听器:
onPointerUp={() => {
setIsVisible(false);
const lockClick = event => event.stopPropagation();
window.addEventListener("click", lockClick, true);
window.setTimeout(
() => window.removeEventListener("click", lockClick, true),
200
);
}}
之前,我延迟了
setIsVisible(false)
本身并且它也有效,因为当点击发生时 Over 框仍然会被安装。但缩小尺寸是在盒子消失之前有一个视觉延迟。
我还在寻找更好的解决方案,一个没有
setTimeout()
.
似乎可以通过使用 setTimeout 函数包装 setIsVisble 来修复它,如下所示:
<button
type="button"
className="button button-over"
onPointerDown={(event) => event.stopPropagation()}
onPointerUp={() => {
setTimeout(() => setIsVisible(false), 5)
}}
>
我在 Android 的 Chrome 和 Safari 上试过了。
你正在停止
pointerDown
事件的传播,但不是 onClick 事件的传播,这是“under”div 正在监听的。有很多方法可以解决这个问题,但最简单的方法是将“under”div 更改为
<button
type="button"
onPointerDown={() => console.log("Clicked under :(")}>
Under
</button>
我不完全确定为什么它只在移动设备中是一个问题——也许其他人可以在这里插话,但我怀疑浏览器可能在桌面模式下以不同的方式处理指针事件,并且可能将它们与鼠标事件或其他东西捆绑在一起。