我正在寻找一种方法来检测单击事件是否发生在组件之外,如本文所述。jQueryclosest()用于查看单击事件的目标是否将dom元素作为其父元素之一。如果存在匹配项,则单击事件属于其中一个子项,因此不被视为在组件之外。
因此,在我的组件中,我想将一个单击处理程序附加到窗口。当处理程序启动时,我需要将目标与组件的dom子级进行比较。
click事件包含类似“path”的财产,它似乎保存了事件经过的dom路径。我不知道该比较什么,或者如何最好地遍历它,我想肯定有人已经把它放在了一个聪明的效用函数中。。。不
我有一个需要有条件地将孩子插入模态的例子。像这样,贝娄。
const [view, setView] = useState(VIEWS.SomeView)
return (
<Modal onClose={onClose}>
{VIEWS.Result === view ? (
<Result onDeny={() => setView(VIEWS.Details)} />
) : VIEWS.Details === view ? (
<Details onDeny={() => setView(VIEWS.Result) /> />
) : null}
</Modal>
)
所以parent.contains(event.target)在这里不起作用,因为一旦分离了子级,parent(modal)就不再包含event.targe。
我的解决方案(到目前为止有效,没有任何问题)是这样写:
const listener = (event: MouseEvent) => {
if (parentNodeRef && !event.path.includes(parentNodeRef)) callback()
}
若父级包含已经分离的树中的元素,那个么它不会触发回调。
编辑:event.path是新的,尚未在所有浏览器中退出。请改用composedPath。
这是我解决问题的方法
我从我的自定义钩子返回一个布尔值,当这个值发生变化时(如果单击在我作为参数传递的ref之外,则为true),这样我就可以用useEffect钩子捕捉到这个变化,我希望你能清楚。
下面是一个活生生的例子:codesandbox上的实时示例
import { useEffect, useRef, useState } from "react";
const useOutsideClick = (ref) => {
const [outsieClick, setOutsideClick] = useState(null);
useEffect(() => {
const handleClickOutside = (e) => {
if (!ref.current.contains(e.target)) {
setOutsideClick(true);
} else {
setOutsideClick(false);
}
setOutsideClick(null);
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
return outsieClick;
};
export const App = () => {
const buttonRef = useRef(null);
const buttonClickedOutside = useOutsideClick(buttonRef);
useEffect(() => {
// if the the click was outside of the button
// do whatever you want
if (buttonClickedOutside) {
alert("hey you clicked outside of the button");
}
}, [buttonClickedOutside]);
return (
<div className="App">
<button ref={buttonRef}>click outside me</button>
</div>
);
}
Ez的方式。。。(2022年更新)
创建挂钩:useOutsideClick.ts
export function useOutsideClick(ref: any, onClickOut: () => void){
useEffect(() => {
const onClick = ({target}: any) => !ref.contains(target) && onClickOut?.()
document.addEventListener("click", onClick);
return () => document.removeEventListener("click", onClick);
}, []);
}
将componentRef添加到组件并调用useOutsideClick
export function Example(){
const componentRef = useRef();
useOutsideClick(componentRef.current!, () => {
// do something here
});
return (
<div ref={componentRef as any}> My Component </div>
)
}
在我的DROPDOWN案例中,Ben Bud的解决方案工作得很好,但我有一个单独的切换按钮和一个onClick处理程序。因此,外部单击逻辑与单击切换按钮冲突。下面是我如何通过传递按钮的ref来解决这个问题:
import React, { useRef, useEffect, useState } from "react";
/**
* Hook that triggers onClose when clicked outside of ref and buttonRef elements
*/
function useOutsideClicker(ref, buttonRef, onOutsideClick) {
useEffect(() => {
function handleClickOutside(event) {
/* clicked on the element itself */
if (ref.current && !ref.current.contains(event.target)) {
return;
}
/* clicked on the toggle button */
if (buttonRef.current && !buttonRef.current.contains(event.target)) {
return;
}
/* If it's something else, trigger onClose */
onOutsideClick();
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
/**
* Component that alerts if you click outside of it
*/
export default function DropdownMenu(props) {
const wrapperRef = useRef(null);
const buttonRef = useRef(null);
const [dropdownVisible, setDropdownVisible] = useState(false);
useOutsideClicker(wrapperRef, buttonRef, closeDropdown);
const toggleDropdown = () => setDropdownVisible(visible => !visible);
const closeDropdown = () => setDropdownVisible(false);
return (
<div>
<button onClick={toggleDropdown} ref={buttonRef}>Dropdown Toggler</button>
{dropdownVisible && <div ref={wrapperRef}>{props.children}</div>}
</div>
);
}
因为对我来说!ref.current.contains(e.target)无法工作,因为ref中包含的DOM元素正在更改,我提出了一个稍微不同的解决方案:
function useClickOutside<T extends HTMLElement>(
element: T | null,
onClickOutside: () => void,
) {
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
const xCoord = event.clientX;
const yCoord = event.clientY;
if (element) {
const { right, x, bottom, y } = element.getBoundingClientRect();
if (xCoord < right && xCoord > x && yCoord < bottom && yCoord > y) {
return;
}
onClickOutside();
}
}
document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('click', handleClickOutside);
};
}, [element, onClickOutside]);