[英]React, click outside event fire right after the open event, preventing the modal from being displayed
I have a problem.我有个问题。 I added an
onClick
event to an element.我向一个元素添加了一个
onClick
事件。 Its event handler's work is to change a state.它的事件处理程序的工作是改变一个状态。 After that state changed, I display a popup.
在该状态更改后,我会显示一个弹出窗口。 I get access to that popup using
useRef
hook.我可以使用
useRef
挂钩访问该弹出窗口。 Then I add a click
event to the document
and its event handler work is to check that user clicked outside the popup or not.然后我向
document
添加一个click
事件,它的事件处理程序的工作是检查用户是否在弹出窗口之外点击。
But the problem is here: when user clicks on the element, immediately the added event handler to the document
will be executed!但问题就在这里:当用户点击元素时,添加到
document
的事件处理程序将立即被执行! But how?但是怎么做? look at these steps and you'll understand my point better:
看看这些步骤,你会更好地理解我的观点:
user clicked on the show popup button --> onClick event handler executed --> the state changed --> an other event added to the document(for outside click purpose) --> immediately the event handler of click event on document executed (all these happen when you click on show popup button just once!!).用户单击显示弹出按钮-> onClick 事件处理程序已执行--> 状态已更改--> 添加到文档中的另一个事件(用于外部单击目的)--> 立即执行文档上单击事件的事件处理程序(当您单击一次显示弹出按钮时,所有这些都会发生!!)。
Option popup component:选项弹出组件:
import { useRef } from "react";
import useAxis from "../../hooks/useAxis";
import useOutSideClick from "../../hooks/useOutSideClick";
const Options = (props) => {
const { children, setShowOptions, showOptions } = props;
const boxRef = useRef();
const { childrens, offset } = useAxis(children, {
/* add an onClick to this components children(it has just one child and it is the open
popup button)*/
onClick: () => {
console.log("test1");
//show the options popup
setShowOptions(!showOptions);
},
});
//close the popup if user clicked outside the popup
useOutSideClick(boxRef, () => {
console.log("test2");
//close the popup
setShowOptions((prevState) => !prevState);
}, showOptions);
return (
<>
{showOptions && (
<div
ref={boxRef}
className="absolute rounded-[20px] bg-[#0d0d0d] border border-[#1e1e1e] border-solid w-[250px] overflow-y-auto h-[250px]"
style={{ left: offset.x + 25 + "px", top: offset.y + "px" }}
></div>
)}
{childrens}
</>
);
};
export default Options;
useOutSideClick custom hook: useOutSideClick 自定义钩子:
import { useEffect } from "react";
//a custom hook to detect that user clicked
const useOutSideClick = (ref, outSideClickHandler, condition = true) => {
useEffect(() => {
if (condition) {
const handleClickOutside = (event) => {
console.log("hellloooo");
//if ref.current doesnt contain event.target it means that user clicked outside
if (ref.current && !ref.current.contains(event.target)) {
outSideClickHandler();
}
};
document.addEventListener("click", handleClickOutside);
return () => {
document.removeEventListener("click", handleClickOutside);
};
}
}, [ref, condition]);
};
export default useOutSideClick;
useAxis custom hook: useAxis 自定义钩子:
import React, { useEffect, useRef, useState } from "react";
const useAxis = (children, events) => {
const childRef = useRef();
const [offset, setOffset] = useState({
x: "",
y: "",
});
useEffect(() => {
Object.keys(events).forEach((event) => {
let eventHandler;
let callBack = events[event];
if (event === "onClick" || event === "onMouseEnter") {
eventHandler = (e) => {
callBack();
};
events[event] = eventHandler;
}
});
}, [JSON.stringify(events)]);
//setting mouse enter and leave event for the components children
const childrens = React.Children.map(children, (child) => {
return React.cloneElement(child, {
...events,
ref: childRef,
});
});
//initialize the position of child component at the first render
useEffect(() => {
setOffset(() => {
return {
x: childRef.current.offsetLeft,
y: childRef.current.offsetTop,
};
});
}, []);
return { childrens, offset };
};
export default useAxis;
the button(actually the element) component:按钮(实际上是元素)组件:
//basic styles for icons
const iconsStyles = "w-[24px] h-[24px] transition duration-300 cursor-pointer";
const NavBar = () => {
const [showOptions, setShowOptions] = useState(false);
return (
<Options
setShowOptions={setShowOptions}
showOptions={showOptions}
>
//onClick will be applied to this div only
<div
>
<TooltipContainer tooltipText="options">
<div>
<DotsHorizontalIcon
className={`${iconsStyles} ${
showOptions
? "text-[#fafafa]"
: "text-[#828282] hover:text-[#fafafa]"
}`}
/>
</div>
</TooltipContainer>
</span>
</Options>
//there are some other items that theres no need to be here
);
};
export default NavBar;
You can see my codes and the part of my app that you need to see in this CodeSandbox link .您可以在此 CodeSandbox 链接中查看我的代码和您需要查看的应用程序部分。 so what should I do to avoid executing the event handler(for outside click purpose) immediately after the event added to the
document
by clicking on open popup button for the first time?那么我应该怎么做才能避免在第一次单击打开弹出按钮将事件添加到
document
后立即执行事件处理程序(用于外部单击目的)?
I believe the event of the on click bubbles up, while the state is already toggled, and the ref is initialized, therefore at some point reaching the documents, whose listener is now registered and then called.我相信 on click 事件会冒泡,而状态已经切换,并且 ref 已初始化,因此在某个时候到达文档,其侦听器现在已注册然后调用。 to stop this behaviour you need to call
e.stopPropagation()
in useAxis
for the click event.要停止此行为,您需要在
useAxis
中为单击事件调用e.stopPropagation()
。 However the signed up listener to the button is not the one you would expect .然而,注册的按钮监听器不是您所期望的。 It will sign up the listener you passed to the
useAxis
hook, instead of the modified one in useAxis
itself.它将注册您传递给
useAxis
挂钩的侦听器,而不是在useAxis
本身中注册修改过的侦听器。 To circumvent this, put the code from inside the useEffect
in useAxis
simply in the render function.为了避免这种情况,只需将
useAxis
中的代码放在useEffect
中,只需在渲染函数中即可。 If you then call e.stopPropagation()
on the event, it should work.如果您随后在事件上调用
e.stopPropagation()
,它应该可以工作。 Here the final partial code in useAxis
:这是
useAxis
中的最终部分代码:
// now correctly updates events in time before cloning the children
Object.keys(events).forEach((event) => {
let eventHandler;
let callBack = events[event];
if (event === "onClick" || event === "onMouseEnter") {
eventHandler = (e) => {
// stop the propagation of the event
e.stopPropagation();
callBack();
};
events[event] = eventHandler;
console.log("inner events", events);
}
});
//setting updated mouse enter and leave event for the components children
const childrens = React.Children.map(children, (child) => {
console.log(events);
return React.cloneElement(child, {
...events,
ref: childRef
});
});
You are returning the below JSX inside Options.jsx
.您将在
Options.jsx
中返回以下 JSX。 Where childrens
contains the button that opens the modal.其中
childrens
包含打开模式的按钮。 If you look at your JSX, this button will not be rendered as a child of the div
with boxRef
, which is why this if (ref.current && !ref.current.contains(event.target))
inside useOutSideClick
will always pass, so it closes the modal right after.如果你查看你的 JSX,这个按钮不会被渲染为带有
boxRef
的div
的子元素,这就是为什么 useOutSideClick 内部的 if (ref.current && ! useOutSideClick
if (ref.current && !ref.current.contains(event.target))
总是会通过的原因,所以它会立即关闭模态。
return (
<>
{showOptions && (
<div
ref={boxRef}
className="absolute rounded-[20px] bg-[#0d0d0d] border border-[#1e1e1e] border-solid w-[250px] overflow-y-auto h-[250px]"
style={{ left: offset.x + 25 + "px", top: offset.y + "px" }}
></div>
)}
{childrens}
</>
);
A quick solution is to change the above return
with the below one.一个快速的解决方案是用下面的返回值来改变上面的
return
值。 Notice the ref
is being added to a outer div
, so the button for opening the modal is rendered as its child.请注意
ref
被添加到外部div
,因此用于打开模式的按钮呈现为其子项。
return (
<div ref={boxRef}>
{showOptions && (
<div
className="absolute rounded-[20px] bg-[#0d0d0d] border border-[#1e1e1e] border-solid w-[250px] overflow-y-auto h-[250px]"
style={{ left: offset.x + 25 + "px", top: offset.y + "px" }}
></div>
)}
{childrens}
</div>
);
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.