检测React组件外部的单击

我正在寻找一种方法来检测是否在组件之外发生了单击事件,如本文章中所述。jQuery最近()用于查看单击事件的目标是否将dom元素作为其父元素之一。如果有匹配,则单击事件属于其中一个子事件,因此不被认为是组件之外的。

因此,在我的组件中,我想将单击处理程序附加到window。当处理程序触发时,我需要将目标与我组件的dom子级进行比较。

单击事件包含像“path”这样的属性,它似乎保存了事件经过的dom路径。我不确定要比较什么或如何最好地遍历它,我想一定有人已经把它放在了一个聪明的实用函数中…不?

745894 次浏览

你可以在主体上安装一个双击处理程序,在该元素上安装另一个处理程序。在此元素的处理程序中只需返回false以防止事件传播。因此,当双击发生时,如果它在元素上,它将被捕获,不会传播到主体上的处理程序。否则它将被主体上的处理程序捕获。

更新:如果你真的不想阻止事件传播,你只需要使用最接近来检查单击是否发生在你的元素或他的一个子元素上:

<html><head><script src="https://code.jquery.com/jquery-2.1.4.min.js"></script><script>$(document).on('click', function(event) {if (!$(event.target).closest('#div3').length) {alert("outside");}});</script></head><body><div style="background-color:blue;width:100px;height:100px;" id="div1"></div><div style="background-color:red;width:100px;height:100px;" id="div2"></div><div style="background-color:green;width:100px;height:100px;" id="div3"></div><div style="background-color:yellow;width:100px;height:100px;" id="div4"></div><div style="background-color:grey;width:100px;height:100px;" id="div5"></div></body></html>

更新:没有jQuery:

<html><head><script>function findClosest (element, fn) {if (!element) return undefined;return fn(element) ? element : findClosest(element.parentElement, fn);}document.addEventListener("click", function(event) {var target = findClosest(event.target, function(el) {return el.id == 'div3'});if (!target) {alert("outside");}}, false);</script></head><body><div style="background-color:blue;width:100px;height:100px;" id="div1"></div><div style="background-color:red;width:100px;height:100px;" id="div2"></div><div style="background-color:green;width:100px;height:100px;" id="div3"><div style="background-color:pink;width:50px;height:50px;" id="div6"></div></div><div style="background-color:yellow;width:100px;height:100px;" id="div4"></div><div style="background-color:grey;width:100px;height:100px;" id="div5"></div></body></html>

我在discuss.reactjs.org上找到了一个解决方案,这要归功于Ben Alpert。建议的方法将一个处理程序附加到文档,但事实证明这是有问题的。单击我树中的一个组件会导致重新渲染,该重渲染会在更新时删除单击的元素。因为React的重新渲染发生在之前,文档主体处理程序被调用,所以未检测到该元素在树中的“内部”。

解决方案是在应用程序根元素上添加处理程序。

主要:

window.__myapp_container = document.getElementById('app')React.render(<App/>, window.__myapp_container)

组件:

import { Component, PropTypes } from 'react';import ReactDOM from 'react-dom';
export default class ClickListener extends Component {
static propTypes = {children: PropTypes.node.isRequired,onClickOutside: PropTypes.func.isRequired}
componentDidMount () {window.__myapp_container.addEventListener('click', this.handleDocumentClick)}
componentWillUnmount () {window.__myapp_container.removeEventListener('click', this.handleDocumentClick)}
/* using fat arrow to bind to instance */handleDocumentClick = (evt) => {const area = ReactDOM.findDOMNode(this.refs.area);
if (!area.contains(evt.target)) {this.props.onClickOutside(evt)}}
render () {return (<div ref='area'>{this.props.children}</div>)}}

2021更新:

自从我添加这个响应已经有一段时间了,因为它似乎仍然引起了一些兴趣,我想我会把它更新到更最新的React版本。在2021年,我将这样编写这个组件:

import React, { useState } from "react";import "./DropDown.css";
export function DropDown({ options, callback }) {const [selected, setSelected] = useState("");const [expanded, setExpanded] = useState(false);
function expand() {setExpanded(true);}
function close() {setExpanded(false);}
function select(event) {const value = event.target.textContent;callback(value);close();setSelected(value);}
return (<div className="dropdown" tabIndex={0} onFocus={expand} onBlur={close} ><div>{selected}</div>{expanded ? (<div className={"dropdown-options-list"}>{options.map((O) => (<div className={"dropdown-option"} onClick={select}>{O}</div>))}</div>) : null}</div>);}

原始答案(2016):

以下是最适合我的解决方案,无需将事件附加到容器:

某些超文本标记语言元素可以具有所谓的“重点”,例如输入元素。当这些元素失去焦点时,它们也会响应模糊事件。

要使任何元素具有焦点的能力,只需确保其tabindex属性设置为-1以外的任何值。在常规超文本标记语言中,这将通过设置tabindex属性来实现,但在React中,您必须使用tabIndex(注意大写I)。

您也可以使用element.setAttribute('tabindex',0)通过JavaScript完成

这就是我使用它的目的,制作一个自定义的DropDown菜单。

var DropDownMenu = React.createClass({getInitialState: function(){return {expanded: false}},expand: function(){this.setState({expanded: true});},collapse: function(){this.setState({expanded: false});},render: function(){if(this.state.expanded){var dropdown = ...; //the dropdown content} else {var dropdown = undefined;}        
return (<div className="dropDownMenu" tabIndex="0" onBlur={ this.collapse } ><div className="currentValue" onClick={this.expand}>{this.props.displayValue}</div>{dropdown}</div>);}});

在这里尝试了很多方法之后,我决定使用github.com/Pomax/react-onclickoutside,因为它是多么完整。

我通过npm安装了模块并将其导入我的组件:

import onClickOutside from 'react-onclickoutside'

然后,在我的组件类中,我定义了handleClickOutside方法:

handleClickOutside = () => {console.log('onClickOutside() method called')}

导出我的组件时,我将其包装在onClickOutside()中:

export default onClickOutside(NameOfComponent)

就这样了。

这是我的方法(demo-https://jsfiddle.net/agymay93/4/):

我创建了名为WatchClickOutside的特殊组件,它可以像(我假设JSX语法)一样使用:

<WatchClickOutside onClickOutside={this.handleClose}><SomeDropdownEtc></WatchClickOutside>

这是WatchClickOutside组件的代码:

import React, { Component } from 'react';
export default class WatchClickOutside extends Component {constructor(props) {super(props);this.handleClick = this.handleClick.bind(this);}
componentWillMount() {document.body.addEventListener('click', this.handleClick);}
componentWillUnmount() {// remember to remove all events to avoid memory leaksdocument.body.removeEventListener('click', this.handleClick);}
handleClick(event) {const {container} = this.refs; // get container that we'll wait to be clicked outsideconst {onClickOutside} = this.props; // get click outside callbackconst {target} = event; // get direct click event target
// if there is no proper callback - no point of checkingif (typeof onClickOutside !== 'function') {return;}
// if target is container - container was not clicked outside// if container contains clicked target - click was not outside of itif (target !== container && !container.contains(target)) {onClickOutside(event); // clicked outside - fire callback}}
render() {return (<div ref="container">{this.props.children}</div>);}}

对于那些需要绝对定位的人,我选择的一个简单选项是添加一个包装器组件,该组件的样式为用透明背景覆盖整个页面。然后你可以在这个元素上添加一个onClick来关闭你的内部组件。

<div style=\{\{position: 'fixed',top: '0', right: '0', bottom: '0', left: '0',zIndex: '1000',}} onClick={() => handleOutsideClick()} ><Content style=\{\{position: 'absolute'}}/></div>

就像现在一样,如果您在内容上添加单击处理程序,该事件也将传播到上层div并因此触发处理程序Outside Click。如果这不是您想要的行为,只需停止处理程序上的事件传播。

<Content style=\{\{position: 'absolute'}} onClick={e => {e.stopPropagation();desiredFunctionCall();}}/>

'

以下解决方案使用ES6并遵循绑定的最佳实践以及通过方法设置ref。

看看它在行动:

钩子实现:

import React, { useRef, useEffect } from "react";
/*** Hook that alerts clicks outside of the passed ref*/function useOutsideAlerter(ref) {useEffect(() => {/*** Alert if clicked on outside of element*/function handleClickOutside(event) {if (ref.current && !ref.current.contains(event.target)) {alert("You clicked outside of me!");}}// Bind the event listenerdocument.addEventListener("mousedown", handleClickOutside);return () => {// Unbind the event listener on clean updocument.removeEventListener("mousedown", handleClickOutside);};}, [ref]);}
/*** Component that alerts if you click outside of it*/export default function OutsideAlerter(props) {const wrapperRef = useRef(null);useOutsideAlerter(wrapperRef);
return <div ref={wrapperRef}>{props.children}</div>;}

类实现:

16.3之后

import React, { Component } from "react";
/*** Component that alerts if you click outside of it*/export default class OutsideAlerter extends Component {constructor(props) {super(props);
this.wrapperRef = React.createRef();this.handleClickOutside = this.handleClickOutside.bind(this);}
componentDidMount() {document.addEventListener("mousedown", this.handleClickOutside);}
componentWillUnmount() {document.removeEventListener("mousedown", this.handleClickOutside);}
/*** Alert if clicked on outside of element*/handleClickOutside(event) {if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {alert("You clicked outside of me!");}}
render() {return <div ref={this.wrapperRef}>{this.props.children}</div>;}}

16.3之前

import React, { Component } from "react";
/*** Component that alerts if you click outside of it*/export default class OutsideAlerter extends Component {constructor(props) {super(props);
this.setWrapperRef = this.setWrapperRef.bind(this);this.handleClickOutside = this.handleClickOutside.bind(this);}
componentDidMount() {document.addEventListener("mousedown", this.handleClickOutside);}
componentWillUnmount() {document.removeEventListener("mousedown", this.handleClickOutside);}
/*** Set the wrapper ref*/setWrapperRef(node) {this.wrapperRef = node;}
/*** Alert if clicked on outside of element*/handleClickOutside(event) {if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {alert("You clicked outside of me!");}}
render() {return <div ref={this.setWrapperRef}>{this.props.children}</div>;}}

我对所有其他答案最大的担忧是必须从根/父级向下过滤单击事件。我发现最简单的方法是简单地设置一个具有位置的兄弟元素:固定,下拉列表后面的z-index 1并处理同一组件内固定元素上的单击事件。将所有内容集中到给定组件。

示例代码

#HTML<div className="parent"><div className={`dropdown ${this.state.open ? open : ''}`}>...content</div><div className="outer-handler" onClick={() => this.setState({open: false})}></div></div>
#SASS.dropdown {display: none;position: absolute;top: 0px;left: 0px;z-index: 100;&.open {display: block;}}.outer-handler {position: fixed;top: 0;left: 0;right: 0;bottom: 0;opacity: 0;z-index: 99;display: none;&.open {display: block;}}

这里的其他答案都不适合我。我试图在blur上隐藏一个弹出窗口,但由于内容是绝对定位的,即使点击内部内容,onBlur也会触发。

这是一个对我有效的方法:

// Inside the component:onBlur(event) {// currentTarget refers to this component.// relatedTarget refers to the element where the user clicked (or focused) which// triggered this event.// So in effect, this condition checks if the user clicked outside the component.if (!event.currentTarget.contains(event.relatedTarget)) {// do your thing.}},

希望这有帮助。

战略的一个例子

我喜欢提供的解决方案,它们通过在组件周围创建包装器来做同样的事情。

由于这更像是一种行为,我想到了战略,并提出了以下几点。

我是React的新手,我需要一些帮助才能在用例中保存一些样板

请回顾并告诉我你的想法。

点击外部行为

import ReactDOM from 'react-dom';
export default class ClickOutsideBehavior {
constructor({component, appContainer, onClickOutside}) {
// Can I extend the passed component's lifecycle events from here?this.component = component;this.appContainer = appContainer;this.onClickOutside = onClickOutside;}
enable() {
this.appContainer.addEventListener('click', this.handleDocumentClick);}
disable() {
this.appContainer.removeEventListener('click', this.handleDocumentClick);}
handleDocumentClick = (event) => {
const area = ReactDOM.findDOMNode(this.component);
if (!area.contains(event.target)) {this.onClickOutside(event)}}}

样品使用

import React, {Component} from 'react';import {APP_CONTAINER} from '../const';import ClickOutsideBehavior from '../ClickOutsideBehavior';
export default class AddCardControl extends Component {
constructor() {super();
this.state = {toggledOn: false,text: ''};
this.clickOutsideStrategy = new ClickOutsideBehavior({component: this,appContainer: APP_CONTAINER,onClickOutside: () => this.toggleState(false)});}
componentDidMount () {
this.setState({toggledOn: !!this.props.toggledOn});this.clickOutsideStrategy.enable();}
componentWillUnmount () {this.clickOutsideStrategy.disable();}
toggleState(isOn) {
this.setState({toggledOn: isOn});}
render() {...}}

备注

我想过存储传递的component生命周期钩子并用类似的方法覆盖它们:

const baseDidMount = component.componentDidMount;
component.componentDidMount = () => {this.enable();baseDidMount.call(component)}

component是传递给ClickOutsideBehavior的构造函数的组件。
这将删除此行为的用户的启用/禁用样板,但它看起来不太好

我被同样的问题卡住了。我参加聚会有点晚了,但对我来说这是一个非常好的解决方案。希望它能对其他人有所帮助。您需要从react-dom导入findDOMNode

import ReactDOM from 'react-dom';// ... ✂
componentDidMount() {document.addEventListener('click', this.handleClickOutside, true);}
componentWillUnmount() {document.removeEventListener('click', this.handleClickOutside, true);}
handleClickOutside = event => {const domNode = ReactDOM.findDOMNode(this);
if (!domNode || !domNode.contains(event.target)) {this.setState({visible: false});}}

React Hooks方法(16.8+)

您可以创建一个名为useComponentVisible的可重用钩子。

import { useState, useEffect, useRef } from 'react';
export default function useComponentVisible(initialIsVisible) {const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);const ref = useRef(null);
const handleClickOutside = (event) => {if (ref.current && !ref.current.contains(event.target)) {setIsComponentVisible(false);}};
useEffect(() => {document.addEventListener('click', handleClickOutside, true);return () => {document.removeEventListener('click', handleClickOutside, true);};}, []);
return { ref, isComponentVisible, setIsComponentVisible };}

然后在组件中,您希望添加功能以执行以下操作:

const DropDown = () => {const { ref, isComponentVisible } = useComponentVisible(true);return (<div ref={ref}>{isComponentVisible && (<p>Dropdown Component</p>)}</div>); 
}

在这里找到一个代码框的例子。

componentWillMount(){
document.addEventListener('mousedown', this.handleClickOutside)}
handleClickOutside(event) {
if(event.path[0].id !== 'your-button'){this.setState({showWhatever: false})}}

事件path[0]是最后一个点击的项目

我从下面的文章中找到了这个:

应用场景返回({this.node=节点;}}> 切换Popover {this.state.popup可见&&( 我是波波! )} );}}

这里有一篇关于这个问题的好文章:"处理React组件之外的点击"https://larsgraubner.com/handle-outside-clicks-react/

我用了本模块(我与作者没有关联)

npm install react-onclickout --save

const ClickOutHandler = require('react-onclickout'); 
class ExampleComponent extends React.Component { 
onClickOut(e) {if (hasClass(e.target, 'ignore-me')) return;alert('user clicked outside of the component!');} 
render() {return (<ClickOutHandler onClickOut={this.onClickOut}><div>Click outside of me!</div></ClickOutHandler>);}}

它做得很好。

我这样做的部分原因是遵循这个并遵循React官方文档中关于处理ref的说明,这需要react^16.3。在尝试了这里的一些其他建议后,这是唯一对我有用的东西…

class App extends Component {constructor(props) {super(props);this.inputRef = React.createRef();}componentWillMount() {document.addEventListener("mousedown", this.handleClick, false);}componentWillUnmount() {document.removeEventListener("mousedown", this.handleClick, false);}handleClick = e => {/*Validating click is made inside a component*/if ( this.inputRef.current === e.target ) {return;}this.handleclickOutside();};handleClickOutside(){/*code to handle what to do when clicked outside*/}render(){return(<div><span ref={this.inputRef} /></div>)}}

onClick处理程序添加到您的顶层容器,并在用户单击时递增状态值。将该值传递给相关组件,每当值更改时,您都可以工作。

在这种情况下,每当clickCount值发生变化时,我们都会调用this.closeDropdown()

incrementClickCount方法在.app容器内触发,而不是.dropdown,因为我们使用event.stopPropagation()来防止事件冒泡。

你的代码可能最终看起来像这样:

class App extends Component {constructor(props) {super(props);this.state = {clickCount: 0};}incrementClickCount = () => {this.setState({clickCount: this.state.clickCount + 1});}render() {return (<div className="app" onClick={this.incrementClickCount}><Dropdown clickCount={this.state.clickCount}/></div>);}}class Dropdown extends Component {constructor(props) {super(props);this.state = {open: false};}componentDidUpdate(prevProps) {if (this.props.clickCount !== prevProps.clickCount) {this.closeDropdown();}}toggleDropdown = event => {event.stopPropagation();return (this.state.open) ? this.closeDropdown() : this.openDropdown();}render() {return (<div className="dropdown" onClick={this.toggleDropdown}>...</div>);}}

[更新]解决方案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;

React 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;

为了扩展由benbud做出的可接受的答案,如果您使用的是style组件,以这种方式传递引用将给您一个错误,例如“this.wrapperRef.contains不是函数”。

建议的修复,在评论中,用div包装样式组件并将ref传递到那里,有效。话虽如此,在他们的文档中,他们已经解释了这样做的原因以及在style ed-组件中正确使用refs:

将ref prop传递给样式组件将为您提供StyledComponent包装器的实例,但不会传递给底层DOM节点。这是由于refs的工作方式。无法直接在我们的包装器上调用DOM方法,例如焦点。要获取实际包装的DOM节点的ref,请将回调传递给innerRef prop。

像这样:

<StyledDiv innerRef={el => { this.el = el }} />

然后你可以直接在“handleClick外部”函数中访问它:

handleClickOutside = e => {if (this.el && !this.el.contains(e.target)) {console.log('clicked outside')}}

这也适用于“onBlur”方法:

componentDidMount(){this.el.focus()}blurHandler = () => {console.log('clicked outside')}render(){return(<StyledDivonBlur={this.blurHandler}tabIndex="0"innerRef={el => { this.el = el }}/>)}

这已经有很多答案,但它们没有解决e.stopPropagation()问题,并阻止单击您希望关闭的元素之外的反应链接。

由于React有自己的人工事件处理程序,您无法使用文档作为事件侦听器的基础。您需要在此之前e.stopPropagation(),因为React使用文档本身。如果您使用示例document.querySelector('body')代替。您可以阻止来自React链接的点击。以下是我如何实现单击外部和关闭的示例。
这使用ES6React16.3

import React, { Component } from 'react';
class App extends Component {constructor(props) {super(props);
this.state = {isOpen: false,};
this.insideContainer = React.createRef();}
componentWillMount() {document.querySelector('body').addEventListener("click", this.handleClick, false);}
componentWillUnmount() {document.querySelector('body').removeEventListener("click", this.handleClick, false);}
handleClick(e) {/* Check that we've clicked outside of the container and that it is open */if (!this.insideContainer.current.contains(e.target) && this.state.isOpen === true) {e.preventDefault();e.stopPropagation();this.setState({isOpen: false,})}};
togggleOpenHandler(e) {e.preventDefault();
this.setState({isOpen: !this.state.isOpen,})}
render(){return(<div><span ref={this.insideContainer}><a href="#open-container" onClick={(e) => this.togggleOpenHandler(e)}>Open me</a></span><a href="/" onClick({/* clickHandler */})>Will not trigger a click when inside is open.</a></div>);}}
export default App;

要使“聚焦”解决方案适用于带有事件侦听器的下拉列表,您可以使用onMouseDown事件而不是onClick添加它们。这样事件将触发,之后弹出窗口将像这样关闭:

<TogglePopupButtononClick = { this.toggleDropup }tabIndex = '0'onBlur = { this.closeDropup }/>{ this.state.isOpenedDropup &&<ul className = { dropupList }>{ this.props.listItems.map((item, i) => (<likey = { i }onMouseDown = { item.eventHandler }>{ item.itemName}</li>))}</ul>}

基于Tanner Linsley优秀的在JSConf夏威夷2020上发言的钩子实现:

useOuterClick接口

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 callbackconst innerRef = useRef(); // returned to client, who marks "border" element
// update cb on each render, so second useEffect has access to current valueuseEffect(() => { 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)}

下面是一个工作示例:

/*Custom Hook*/function useOuterClick(callback) {const innerRef = useRef();const callbackRef = useRef();
// set current callback in ref, before second useEffect uses ituseEffect(() => { // useEffect wrapper to be safe for concurrent modecallbackRef.current = callback;});
useEffect(() => {document.addEventListener("click", handleClick);return () => document.removeEventListener("click", handleClick);
// read most recent callback and innerRef dom node from refsfunction handleClick(e) {if (innerRef.current &&callbackRef.current &&!innerRef.current.contains(e.target)) {callbackRef.current(e);}}}, []); // no need for callback + innerRef dep  
return innerRef; // return ref; client can omit `useRef`}
/*Usage*/const Client = () => {const [counter, setCounter] = useState(0);const innerRef = useOuterClick(e => {// counter state is up-to-date, when handler is calledalert(`Clicked outside! Increment counter to ${counter + 1}`);setCounter(c => c + 1);});return (<div><p>Click outside!</p><div id="container" ref={innerRef}>Inside, counter: {counter}</div></div>);};
ReactDOM.render(<Client />, document.getElementById("root"));
#container { border: 1px solid red; padding: 20px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js" integrity="sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=" crossorigin="anonymous"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js" integrity="sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGww=" crossorigin="anonymous"></script><script> var {useRef, useEffect, useCallback, useState} = React</script><div id="root"></div>

重点内容

  • useOuterClick利用可变裁判提供精益Client API
  • 稳定单击监听器以获取包含组件的生命周期([] deps)
  • Client可以设置回调,而无需通过useCallback记住它
  • 回调主体可以访问最新的道具和状态-没有陈旧的闭包值

(iOS附注)

iOS通常只将某些元素视为可点击的。要使外部点击有效,请选择与document不同的单击侦听器-不包括body。例如,在React根目录div上添加一个侦听器并扩展其高度,如height: 100vh,以捕获所有外部点击。来源:quirksmode.org

import ReactDOM from 'react-dom' ;
class SomeComponent {
constructor(props) {// First, add this to your constructorthis.handleClickOutside = this.handleClickOutside.bind(this);}
componentWillMount() {document.addEventListener('mousedown', this.handleClickOutside, false);}
// Unbind event on unmount to prevent leakscomponentWillUnmount() {window.removeEventListener('mousedown', this.handleClickOutside, false);}
handleClickOutside(event) {if(!ReactDOM.findDOMNode(this).contains(event.path[0])){console.log("OUTSIDE");}}}

我为所有场合提供了解决方案。

您应该使用高阶组件来包装您想要侦听它之外的点击的组件。

这个组件示例只有一个prop:“onClicked外部”接收一个函数。

ClickedOutside.js
import React, { Component } from "react";
export default class ClickedOutside extends Component {componentDidMount() {document.addEventListener("mousedown", this.handleClickOutside);}
componentWillUnmount() {document.removeEventListener("mousedown", this.handleClickOutside);}
handleClickOutside = event => {// IF exists the Ref of the wrapped component AND his dom children doesnt have the clicked componentif (this.wrapperRef && !this.wrapperRef.contains(event.target)) {// A props callback for the ClikedClickedOutsidethis.props.onClickedOutside();}};
render() {// In this piece of code I'm trying to get to the first not functional component// Because it wouldn't work if use a functional component (like <Fade/> from react-reveal)let firstNotFunctionalComponent = this.props.children;while (typeof firstNotFunctionalComponent.type === "function") {firstNotFunctionalComponent = firstNotFunctionalComponent.props.children;}
// Here I'm cloning the element because I have to pass a new prop, the "reference"const children = React.cloneElement(firstNotFunctionalComponent, {ref: node => {this.wrapperRef = node;},// Keeping all the old props with the new element...firstNotFunctionalComponent.props});
return <React.Fragment>{children}</React.Fragment>;}}

Hook-React 16.8+使用OnClick外部钩子-React 16.8+

创建通用useOnOutside Click函数

export const useOnOutsideClick = handleOutsideClick => {const innerBorderRef = useRef();
const onClick = event => {if (innerBorderRef.current &&!innerBorderRef.current.contains(event.target)) {handleOutsideClick();}};
useMountEffect(() => {document.addEventListener("click", onClick, true);return () => {document.removeEventListener("click", onClick, true);};});
return { innerBorderRef };};
const useMountEffect = fun => useEffect(fun, []);

然后在任何功能组件中使用钩子。

const OutsideClickDemo = ({ currentMode, changeContactAppMode }) => {
const [open, setOpen] = useState(false);const { innerBorderRef } = useOnOutsideClick(() => setOpen(false));
return (<div><button onClick={() => setOpen(true)}>open</button>{open && (<div ref={innerBorderRef}><SomeChild/></div>)}</div>);
};

链接到演示

部分灵感来自@pau1Fitzgerald答案。

以上答案都不适合我,所以这是我最终做的:

从'React'导入React,{Component};

/*** Component that alerts if you click outside of it*/export default class OutsideAlerter extends Component {constructor(props) {super(props);
this.handleClickOutside = this.handleClickOutside.bind(this);}
componentDidMount() {document.addEventListener('mousedown', this.handleClickOutside);}
componentWillUnmount() {document.removeEventListener('mousedown', this.handleClickOutside);}
/*** Alert if clicked on outside of element*/handleClickOutside(event) {if (!event.path || !event.path.filter(item => item.className=='classOfAComponent').length) {alert('You clicked outside of me!');}}
render() {return <div>{this.props.children}</div>;}}
OutsideAlerter.propTypes = {children: PropTypes.element.isRequired,};

聚会有点晚了,但我在让这些与反应一起工作时遇到了问题。选择下拉列表,因为单击的选项将不再包含在我希望单击的父级中。

我通过使用来解决这个问题:

componentDidMount() {document.addEventListener('mousedown', this.onClick );}
componentWillUnmount() {document.removeEventListener('mousedown', this.onClick );}
onClick = (event) => {if(!event.path.includes(this.detectOutsideClicksDiv)) {// Do stuff here}}

MUI有一个小组件来解决这个问题:https://mui.com/base/react-click-away-listener/,你可以选择它。它的权重低于1 kB gzip,它支持移动、IE 11和门户。

如果您想使用已经存在的用于此功能的微小组件(466字节gzip),那么您可以查看此库反应出击

该库的优点是它还允许您检测组件外部和另一个组件内部的点击。它还支持检测其他类型的事件。

在我的DROPDOWN案例中,Ben Bud的解决方案工作得很好,但是我有一个单独的切换按钮和一个onClick处理程序。所以外部单击逻辑与按钮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 listenerdocument.addEventListener("mousedown", handleClickOutside);return () => {// Unbind the event listener on clean updocument.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>);}

我知道这是一个古老的问题,但我不断遇到这个问题,并且我在以简单的格式解决这个问题方面遇到了很多麻烦。因此,如果这会让任何人的生活更轻松,请使用airbnb的Outside ClickHandler。这是一个最简单的插件,无需编写自己的代码即可完成此任务。

示例:

hideresults(){this.setState({show:false})}render(){return(<div><div onClick={() => this.setState({show:true})}>SHOW</div> {(this.state.show)? <OutsideClickHandler onOutsideClick={() =>{this.hideresults()}} > <div className="insideclick"></div> </OutsideClickHandler> :null}</div>)}
import { useClickAway } from "react-use";
useClickAway(ref, () => console.log('OUTSIDE CLICKED'));

你可以用一个简单的方法来解决你的问题,我给你看:

……

const [dropDwonStatus , setDropDownStatus] = useState(false)
const openCloseDropDown = () =>{setDropDownStatus(prev => !prev)}
const closeDropDown = ()=> {if(dropDwonStatus){setDropDownStatus(false)}}...<parent onClick={closeDropDown}><child onClick={openCloseDropDown} /></parent>

这对我有用,祝你好运;)

或者.包含,您可以使用.最接近的方法。当你想检查点击是否在id="apple"的元素之外时,我可以使用:

const isOutside = !e.target.closest("#apple");

这将检查被点击的元素上方的树中是否有任何元素的id为“apple”。我们必须否定结果!

带钩子的打字稿

注意:我使用的是React版本16.3,带有React.createRef。对于其他版本,请使用ref回调。

下拉组件:

interface DropdownProps {...};
export const Dropdown: React.FC<DropdownProps> () {const ref: React.RefObject<HTMLDivElement> = React.createRef();  
const handleClickOutside = (event: MouseEvent) => {if (ref && ref !== null) {const cur = ref.current;if (cur && !cur.contains(event.target as Node)) {// close all dropdowns}}}
useEffect(() => {// Bind the event listenerdocument.addEventListener("mousedown", handleClickOutside);return () => {// Unbind the event listener on clean updocument.removeEventListener("mousedown", handleClickOutside);};});
return (<div ref={ref}>...</div>);}

这是最适合我做下拉框的方法:

handleClick = () => {document.getElementById("myDrop").classList.toggle("showing");}
render() {
return (<div className="courses"><div class="dropdownBody"><button onClick={this.handleClick} onBlur={this.handleClick} class="dropbtn">Dropdown</button><div id="myDrop" class="dropdown-content"><a href="#home">Home</a><a href="#about">About</a><a href="#contact">Contact</a></div></div></div>)}

https://stackoverflow.com/a/42234988/9536897解决方案在手机上不起作用。

你可以试试:

  // returns true if the element or one of its parents has the class classnamehasSomeParentTheClass(element, classname) {if(element.target)element=element.target;    
if (element.className&& element.className.split(" ").indexOf(classname) >= 0) return true;return (element.parentNode &&this.hasSomeParentTheClass(element.parentNode, classname));}
  componentDidMount() {const fthis = this;
$(window).click(function (element) {if (!fthis.hasSomeParentTheClass(element, "myClass"))fthis.setState({ pharmacyFocus: null });});}
  • 在视图上,为您的特定元素提供了class Name。

我有一个案例,当我需要有条件地将儿童插入情态时。像这样,贝洛。

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)在这里不起作用,因为一旦你分离了子节点,父节点(modal)就不再包含event.target了。

我的解决方案(到目前为止有效,没有任何问题)是写这样的东西:

const listener = (event: MouseEvent) => {if (parentNodeRef && !event.path.includes(parentNodeRef)) callback()}

如果父级包含已分离树中的元素,则不会触发回调。

编辑:event.path是新的,尚未在所有浏览器中退出。改用compoesedPath

或者:

const onClickOutsideListener = () => {alert("click outside")document.removeEventListener("click", onClickOutsideListener)}
...
return (<divonMouseLeave={() => {document.addEventListener("click", onClickOutsideListener)}}>...</div>

我喜欢@Ben Bud的答案,但当有视觉嵌套元素时,contains(event.target)就不能按预期工作。

因此,有时最好计算点击点在视觉上是否在元素内部。

这是我针对这种情况的React Hook代码。

import { useEffect } from 'react'
export function useOnClickRectOutside(ref, handler) {useEffect(() => {const listener = (event) => {const targetEl = ref.currentif (targetEl) {const clickedX = event.clientXconst clickedY = event.clientYconst rect = targetEl.getBoundingClientRect()const targetElTop = rect.topconst targetElBottom = rect.top + rect.heightconst targetElLeft = rect.leftconst targetElRight = rect.left + rect.width
if (// check X CoordinatetargetElLeft < clickedX &&clickedX < targetElRight &&// check Y CoordinatetargetElTop < clickedY &&clickedY < targetElBottom) {return}
// trigger event when the clickedX,Y is outside of the targetElhandler(event)}}
document.addEventListener('mousedown', listener)document.addEventListener('touchstart', listener)
return () => {document.removeEventListener('mousedown', listener)document.removeEventListener('touchstart', listener)}}, [ref, handler])}

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>)}

非吸引方式不需要添加另一个DIV EL。

注意:React可能会说findDomNode已弃用,但到目前为止我还没有遇到任何问题

@异常:如果单击了要忽略的类

@idException:点击时忽略的id

import React from "react"import ReactDOM from "react-dom"type Func1<T1, R> = (a1: T1) => R

export function closest(el: Element,fn: (el: Element) => boolean): Element | undefined {let el_: Element | null = el;  
while (el_) {if (fn(el_)) {return el_;}  
el_ = el_.parentElement;}}let instances: ClickOutside[] = []
type Props = {idException?: string,exceptions?: (string | Func1<MouseEvent, boolean>)[]handleClickOutside: Func1<MouseEvent, void>
}

export default class ClickOutside extends React.Component<Props> {static defaultProps = {exceptions: []};
componentDidMount() {if (instances.length === 0) {document.addEventListener("mousedown", this.handleAll, true)window.parent.document.addEventListener("mousedown",this.handleAll,true)}instances.push(this)}
componentWillUnmount() {instances.splice(instances.indexOf(this), 1)if (instances.length === 0) {document.removeEventListener("mousedown", this.handleAll, true)window.parent.document.removeEventListener("mousedown",this.handleAll,true)}}
handleAll = (e: MouseEvent) => {
const target: HTMLElement = e.target as HTMLElementif (!target) return
instances.forEach(instance => {const { exceptions, handleClickOutside: onClickOutside, idException } = instance.props as Required<Props>let exceptionsCount = 0
if (exceptions.length > 0) {const { functionExceptions, stringExceptions } = exceptions.reduce((acc, exception) => {switch (typeof exception) {case "function":acc.functionExceptions.push(exception)breakcase "string":acc.stringExceptions.push(exception)break}
return acc},{ functionExceptions: [] as Func1<MouseEvent, boolean>[], stringExceptions: [] as string[] })if (functionExceptions.length > 0) {exceptionsCount += functionExceptions.filter(exception => exception(e) === true).length}
if (exceptionsCount === 0 && stringExceptions.length > 0) {
const el = closest(target, (e) => stringExceptions.some(ex => e.classList.contains(ex)))if (el) {exceptionsCount++}}}
if (idException) {const target = e.target as HTMLDivElementif (document.getElementById(idException)!.contains(target)) {exceptionsCount++}}
if (exceptionsCount === 0) {// eslint-disable-next-line react/no-find-dom-nodeconst node = ReactDOM.findDOMNode(instance)
if (node && !node.contains(target)) {onClickOutside(e)}}})};
render() {return React.Children.only(this.props.children)}}

用法

<ClickOutside {...{ handleClickOutside: () => { alert('Clicked Outside') } }}><div ><div>Breathe</div></div></ClickOutside>

所以我遇到了类似的问题,但在我的情况下,这里选择的答案不起作用,因为我有一个用于下拉的按钮,它是文档的一部分。所以单击按钮也触发了handleClickOutside函数。为了阻止触发,我不得不在按钮上添加一个新的ref,在条件下添加这个!menuBtnRef.current.contains(e.target)。如果有人像我一样面临同样的问题,我会把它留在这里。

这是组件现在的样子:

const Component = () => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);const menuRef     = useRef(null);const menuBtnRef  = useRef(null);
const handleDropdown = (e) => {setIsDropdownOpen(!isDropdownOpen);}
const handleClickOutside = (e) => {if (menuRef.current && !menuRef.current.contains(e.target) && !menuBtnRef.current.contains(e.target)) {setIsDropdownOpen(false);}}
useEffect(() => {document.addEventListener('mousedown', handleClickOutside, true);return () => {document.removeEventListener('mousedown', handleClickOutside, true);};}, []);
return (
<button ref={menuBtnRef} onClick={handleDropdown}></button>
<div ref={menuRef} className={`${isDropdownOpen ? styles.dropdownMenuOpen : ''}`}>// ...dropdown items</div>)}

这是我解决问题的方法

我从我的自定义钩子返回一个布尔值,当这个值发生变化时(如果单击在我作为arg传递的ref之外,则为true),这样我就可以用useEffects钩子捕获此更改,我希望它对您来说很清楚。

下面是一个例子: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 wantif (buttonClickedOutside) {alert("hey you clicked outside of the button");}}, [buttonClickedOutside]);
return (<div className="App"><button ref={buttonRef}>click outside me</button></div>);}

打字稿+@ford04提案的简化版本:

useOuterClick接口

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 callbackconst innerRef = useRef<T>(null); // returned to client, who marks "border" element
// update cb on each render, so second useEffect has access to current valueuseEffect(() => { 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)}

我有一个类似的用例,我必须开发一个自定义下拉框.它应该在用户单击外部时自动关闭.这是最近的React Hooks实现-

import { useEffect, useRef, useState } from "react";
export const  App = () => {  
const ref = useRef();
const [isMenuOpen, setIsMenuOpen] = useState(false);
useEffect(() => {const checkIfClickedOutside = (e) => {// If the menu is open and the clicked target is not within the menu,// then close the menuif (isMenuOpen && ref.current && !ref.current.contains(e.target)) {setIsMenuOpen(false);}};
document.addEventListener("mousedown", checkIfClickedOutside);
return () => {// Cleanup the event listenerdocument.removeEventListener("mousedown", checkIfClickedOutside);};}, [isMenuOpen]);
return (<div className="wrapper" ref={ref}><buttonclassName="button"onClick={() => setIsMenuOpen((oldState) => !oldState)}>Click Me</button>{isMenuOpen && (<ul className="list"><li className="list-item">dropdown option 1</li><li className="list-item">dropdown option 2</li><li className="list-item">dropdown option 3</li><li className="list-item">dropdown option 4</li></ul>)}</div>);}

简单地使用来自mui的ClickAwayListener(材料-用户界面):

<ClickAwayListener onClickAway={handleClickAway}>{children}<ClickAwayListener >

有关更多信息,您可以查看:https://mui.com/base/react-click-away-listener/

打字稿

function Tooltip(): ReactElement {const [show, setShow] = useState(false);const ref = useRef<HTMLDivElement>(null);
useEffect(() => {function handleClickOutside(event: MouseEvent): void {if (ref.current && !ref.current.contains(event.target as Node)) {setShow(false);}}// Bind the event listenerdocument.addEventListener('mousedown', handleClickOutside);return () => {// Unbind the event listener on clean updocument.removeEventListener('mousedown', handleClickOutside);};});
return (<div ref={ref}></div>)}

import React, { useState, useEffect, useRef } from "react";
const YourComponent: React.FC<ComponentProps> = (props) => {const ref = useRef<HTMLDivElement | null>(null);const [myState, setMyState] = useState(false);useEffect(() => {const listener = (event: MouseEvent) => {// we have to add some logic to decide whether or not a click event is inside of this editor// if user clicks on inside the div we dont want to setState// we add ref to div to figure out whether or not a user is clicking inside this div to determine whether or not event.target is inside the divif (ref.current &&event.target &&// contains is expect other: Node | nullref.current.contains(event.target as Node)) {return;}// if we are outsidesetMyState(false);};// anytime user clics anywhere on the dom, that click event will bubble up into our body element// without { capture: true } it might not workdocument.addEventListener("click", listener, { capture: true });return () => {document.removeEventListener("click", listener, { capture: true });};}, []);
return (<div  ref={ref}>....</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]);
import { RefObject, useEffect } from 'react';
const useClickOutside = <T extends HTMLElement>(ref: RefObject<T>, fn: () => void) => {useEffect(() => {const element = ref?.current;function handleClickOutside(event: Event) {if (element && !element.contains(event.target as Node | null)) {fn();}}document.addEventListener('mousedown', handleClickOutside);return () => {document.removeEventListener('mousedown', handleClickOutside);};}, [ref]);};
export default useClickOutside;

所有提出的解决方案都假设事件可以添加到文档中,并依靠本机方法.contains()来区分事件是在当前组件内部还是外部触发的

ref.current.contains(event.target)

但这在React中并不总是有效的。事实上,在React中,有React.createPortal API允许从组件中指定一个新的真实父组件,其中JSX被呈现到其中,但同时,事件冒泡被模拟为组件在声明的位置(即调用React.createPortal的地方)呈现。

这是通过将所有事件附加到应用程序根元素并在Javascript中模拟事件来实现的。

因此,在这种情况下提出的解决方案被破坏了,因为在门户元素内部单击,对于标准超文本标记语言来说,它在当前元素之外,实际上应该像在内部一样处理。

因此,我重写了这个问题的注释中提出的解决方案,并将其重构为使用功能组件。这也适用于一个或多个嵌套门户的情况。

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} />;}