我正在寻找一种方法来检测单击事件是否发生在组件之外,如本文所述。jQueryclosest()用于查看单击事件的目标是否将dom元素作为其父元素之一。如果存在匹配项,则单击事件属于其中一个子项,因此不被视为在组件之外。
因此,在我的组件中,我想将一个单击处理程序附加到窗口。当处理程序启动时,我需要将目标与组件的dom子级进行比较。
click事件包含类似“path”的财产,它似乎保存了事件经过的dom路径。我不知道该比较什么,或者如何最好地遍历它,我想肯定有人已经把它放在了一个聪明的效用函数中。。。不
@ford04提案的字体+简化版:
使用OuterClick API
const Client = () => {
const ref = useOuterClick<HTMLDivElement>(e => { /* Custom-event-handler */ });
return <div ref={ref}> Inside </div>
};
实施
export default function useOuterClick<T extends HTMLElement>(callback: Function) {
const callbackRef = useRef<Function>(); // initialize mutable ref, which stores callback
const innerRef = useRef<T>(null); // returned to client, who marks "border" element
// update cb on each render, so second useEffect has access to current value
useEffect(() => { callbackRef.current = callback; });
useEffect(() => {
document.addEventListener("click", _onClick);
return () => document.removeEventListener("click", _onClick);
function _onClick(e: any): void {
const clickedOutside = !(innerRef.current?.contains(e.target));
if (clickedOutside)
callbackRef.current?.(e);
}
}, []); // no dependencies -> stable click listener
return innerRef; // convenience for client (doesn't need to init ref himself)
}
基于Tanner Linsley在2020年夏威夷联合会议上的精彩演讲:
使用OuterClick API
const Client = () => {
const innerRef = useOuterClick(ev => {/*event handler code on outer click*/});
return <div ref={innerRef}> Inside </div>
};
实施
function useOuterClick(callback) {
const callbackRef = useRef(); // initialize mutable ref, which stores callback
const innerRef = useRef(); // returned to client, who marks "border" element
// update cb on each render, so second useEffect has access to current value
useEffect(() => { callbackRef.current = callback; });
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
function handleClick(e) {
if (innerRef.current && callbackRef.current &&
!innerRef.current.contains(e.target)
) callbackRef.current(e);
}
}, []); // no dependencies -> stable click listener
return innerRef; // convenience for client (doesn't need to init ref himself)
}
下面是一个工作示例:
/*自定义挂钩*/函数useOuterClick(回调){const innerRef=useRef();const callbackRef=useRef();//在ref中设置当前回调,然后第二个useEffect使用它useEffect(()=>{//useEffect包装器对于并发模式是安全的callbackRef.current=回调;});使用效果(()=>{document.addEventListener(“单击”,handleClick);return()=>document.removeEventListener(“单击”,handleClick);//从ref中读取最近的回调和innerRefdom节点函数句柄Click(e){如果(内部参考当前&&回调参考当前&&!innerRef.current.contains(e.target)) {callbackRef.current(e);}}}, []); // 无需回调+innerRef-depreturn innerRef;//返回参考;客户端可以省略`useRef`}/*用法*/常量客户端=()=>{const[counter,setCounter]=useState(0);const innerRef=useOuterClick(e=>{//调用处理程序时,计数器状态是最新的alert(`Clicked outside!Increment counter to${counter+1}`);设置计数器(c=>c+1);});返回(<div><p>点击外面</p><div id=“container”ref={innerRef}>内部,计数器:{counter}</div></div>);};ReactDOM.render(<Client/>,document.getElementById(“root”));#容器{边框:1px纯红色;填充:20px;}<script src=“https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js“integrity=”sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=“crossrorigin=”匿名“></script><script src=“https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js“integrity=”sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGw=“crossrorigin=”匿名“></script><script>var{useRef,useEffect,useCallback,useState}=反应</script><div id=“root”></div>
要点
useOuterClick利用可变引用提供瘦客户端API包含组件([]deps)的生命周期的稳定单击侦听器客户端可以设置回调,而无需使用callback将其记忆回调主体可以访问最新的属性和状态-没有过时的闭包值
(iOS的侧注)
iOS通常只将某些元素视为可点击的。要使外部单击有效,请选择一个不同于文档的单击侦听器-不向上包括正文。例如,在React根div上添加一个监听器,并扩展其高度,如height:100vh,以捕捉所有外部点击。来源:quicksmod.org
[更新]使用挂钩的React ^16.8解决方案
代码沙盒
import React, { useEffect, useRef, useState } from 'react';
const SampleComponent = () => {
const [clickedOutside, setClickedOutside] = useState(false);
const myRef = useRef();
const handleClickOutside = e => {
if (!myRef.current.contains(e.target)) {
setClickedOutside(true);
}
};
const handleClickInside = () => setClickedOutside(false);
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
});
return (
<button ref={myRef} onClick={handleClickInside}>
{clickedOutside ? 'Bye!' : 'Hello!'}
</button>
);
};
export default SampleComponent;
反应溶液^16.3:
代码沙盒
import React, { Component } from "react";
class SampleComponent extends Component {
state = {
clickedOutside: false
};
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
myRef = React.createRef();
handleClickOutside = e => {
if (!this.myRef.current.contains(e.target)) {
this.setState({ clickedOutside: true });
}
};
handleClickInside = () => this.setState({ clickedOutside: false });
render() {
return (
<button ref={this.myRef} onClick={this.handleClickInside}>
{this.state.clickedOutside ? "Bye!" : "Hello!"}
</button>
);
}
}
export default SampleComponent;
所有提出的解决方案都假设可以将事件添加到文档中,并依赖于本机方法.contains()来区分事件是在当前组件内部还是外部触发的
ref.current.contains(event.target)
但这在React中并不总是有效的。事实上,在React中,React.createPortal API允许从一个组件中指定一个新的真实父组件,JSX在该组件中被呈现,但同时,事件冒泡被模拟为组件在声明的位置呈现(即React.cCreatePortal被调用的位置)。
这是通过将所有事件附加到应用程序根元素并在Javascript中模拟事件来实现的。
因此,在这种情况下,所提出的解决方案被打破了,因为在门户元素内部的单击,对于标准HTML来说,在当前元素之外,实际上应该像在内部一样进行处理。
所以我重写了这个问题的一个注释中提出的解决方案,并对其进行了重构以使用功能组件。这也适用于一个或多个嵌套门户。
export default function OutsideClickDetector({onOutsideClick, Component ='div', ...props} : OutsideClickDetectorProps) {
const isClickInside = useRef<boolean>(false);
const onMouseDown = () => {
isClickInside.current = true;
};
const handleBodyClick = useCallback((e) => {
if(!isClickInside.current) {
onOutsideClick?.(e);
}
isClickInside.current = false;
}, [isClickInside, onOutsideClick]);
useEffect(() => {
document.addEventListener('mousedown', handleBodyClick);
return () => document.removeEventListener('mousedown', handleBodyClick);
});
return <Component onMouseDown={onMouseDown} onMouseUp={() => isClickInside.current = false}{...props} />;
}