检测React组件外部的点击


410

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

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

click事件包含“ path”之类的属性,该属性似乎包含事件经过的dom路径。我不确定要比较什么或如何最好地遍历它,并且我认为有人必须已经将其放在聪明的实用程序函数中了……不?


您可以将点击处理程序附加到父目录而不是窗口吗?
J. Mark Stevens

如果将单击处理程序附加到父项,则知道单击该元素或其子项时,但是我需要检测所有其他被单击的位置,因此需要将处理程序附加到窗口。
Thijs Koerselman

在上次回复后,我看了这篇文章。如何在顶部组件中设置clickState并传递孩子的点击动作。然后,您将检查孩子中的道具以管理打开关闭状态。
J. Mark Stevens

最重要的组件是我的应用。但是,侦听组件的层次很深,在dom中没有严格的位置。我可能无法将单击处理程序添加到我应用程序中的所有组件,只是因为其中之一有兴趣知道您是否单击了它之外的某个位置。其他组件不应意识到此逻辑,因为这会创建可怕的依赖关系和样板代码。
Thijs Koerselman

5
我想向您推荐一个非常不错的lib。由AirBnb创建:github.com/airbnb/react-outside-click-handler
Osoian Marcel

Answers:


681

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

要查看实际效果:

类的实现:

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

OutsideAlerter.propTypes = {
  children: PropTypes.element.isRequired,
};

挂钩实现:

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 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 OutsideAlerter(props) {
  const wrapperRef = useRef(null);
  useOutsideAlerter(wrapperRef);

  return <div ref={wrapperRef}>{props.children}</div>;
}

3
您如何测试呢?如何将有效的event.target发送到handleClickOutside函数?
csilk

5
您如何使用React的综合事件,而不是document.addEventListener此处的事件?
拉尔夫·戴维·阿伯纳西

9
@ polkovnikov.ph 1. context仅在使用构造函数时才作为参数使用。在这种情况下不使用它。react团队建议props在构造函数中将其用作参数的原因是this.props,调用前super(props)将使用undefined并可能导致错误。context在内部节点上仍然可用,这是的全部目的context。这样,您就不必像使用props一样将其从一个组件传递到另一个组件。2.这只是一种风格上的偏爱,在这种情况下不保证就此进行辩论。
本·布德

7
我收到以下错误:“ this.wrapperRef.contains不是函数”
Bogdan

5
@Bogdan,使用样式化组件时出现相同的错误。考虑在顶层使用<div>
user3040068

149

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

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

要使任何元素都有焦点的能力,只需确保其tabindex属性设置为-1以外的任何值即可。在常规HTML中,可以通过设置tabindex属性来实现,但是在React中,必须使用tabIndex(请注意大写I)。

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

这就是我用来制作自定义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>
        );
    }
});

10
这会不会导致可访问性问题?由于您使一个额外的元素可聚焦,只是为了检测它何时进入/聚焦,但交互应该在下拉值上进行,不是吗?
Thijs Koerselman'8

2
这是一个非常有趣的方法。完全适合我的情况,但是我很想了解这可能会影响可访问性。
klinore'9

17
我在某种程度上拥有这种“工作”,这是一个很棒的小技巧。我的问题是我的下拉列表内容是display:absolute这样的,因此下拉列表不会影响父div的大小。这意味着当我单击下拉菜单中的项目时,onblur会触发。
戴夫

2
我对此方法有疑问,下拉内容中的任何元素都不会触发onClick之类的事件。
AnimaSola

8
如果下拉内容包含一些可聚焦的元素(例如表单输入),则可能会遇到其他问题。他们会窃取您的焦点,onBlur 将在您的下拉容器上触发expanded 并将其设置为false。
Kamagatos

106

我陷入了同样的问题。我在这里参加聚会有点晚了,但是对我来说这是一个非常好的解决方案。希望它将对其他人有所帮助。您需要进口findDOMNodereact-dom

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

}

在此处找到一个codeandbox示例。


1
@LeeHanKyeol并非完全如此-此答案在事件处理的捕获阶段调用事件处理程序,而链接到该答案的答案在冒泡阶段调用事件处理程序。
stevejay

3
这应该是公认的答案。与绝对定位的下拉菜单完美搭配。
dishwasherWithProgrammingSkill

1
ReactDOM.findDOMNode并已弃用,应使用ref回调:github.com/yannickcr/eslint-plugin-react/issues/…–
dain

2
绝佳且干净的解决方案
Karim '18

1
这对我有用,尽管我必须破解您的组件才能在200毫秒后将可见重置为“ true”(否则我的菜单将不再显示)。谢谢!
李萌

87

在尝试了许多方法之后,我决定使用github.com/Pomax/react-onclickoutside由于它的完整性。

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

import onClickOutside from 'react-onclickoutside'

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

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

在导出组件时,我将其包裹在onClickOutside()

export default onClickOutside(NameOfComponent)

而已。


2
与Pablo提出tabIndex/ onBlur方法相比,此方法有什么具体优势吗?实现如何工作,其行为与onBlur方法有何不同?
Mark Amery

4
最好是先使用tabIndex hack,然后再使用专用组件。赞!
Atul Yadav

5
@MarkAmery-我对该tabIndex/onBlur方法发表了评论。当下拉position:absolute菜单为时,例如菜单悬停在其他内容上时,该按钮将不起作用。
博·史密斯,

4
与tabindex相比,使用此方法的另一个优势是,如果将焦点放在子元素上,则tabindex解决方案还会
引发

1
@BeauSmith-非常感谢!拯救了我的一天!
米卡

38

我感谢Ben Alpert在describe.reactjs.org上找到了解决方案。建议的方法将处理程序附加到文档,但事实证明这是有问题的。单击树中的一个组件会导致重新渲染,从而删除了更新时单击的元素。因为从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>
    )
  }
}

8
这在React 0.14.7中对我不再起作用-可能是React更改了某些内容,或者当我将代码适应所有对React的更改时,我出错了。相反,我使用的是github.com/Pomax/react-onclickoutside,它的工作原理很像。
妮可

1
嗯 我看不出任何应有的理由。如果应用程序的根DOM节点上没有一个处理程序,为什么要保证在另一个处理程序触发重新渲染之前触发该处理程序document
Mark Amery

1
纯UX点:mousedown可能比click这里更好。在大多数应用程序中,单击外部关闭菜单的行为是在您按下鼠标按钮时发生的,而不是在释放时发生的。例如,尝试使用Stack Overflow的标志共享对话框,或者使用浏览器顶部菜单栏中的下拉菜单之一进行尝试。
Mark Amery

ReactDOM.findDOMNode和字符串ref已弃用,应使用ref回调:github.com/yannickcr/eslint-plugin-react/issues/…–
dain

这是最简单的解决方案,即使对document
Flion '18

37

基于Tanner Linsley 在JSConf Hawaii 2020上的精彩演讲的 Hook实现:

useOuterClick 挂钩API

const Client = () => {
  const innerRef = useOuterClick(ev => { /* what you want do on outer click */ });
  return <div ref={innerRef}> Inside </div> 
};

实作

/*
  Custom Hook
*/
function useOuterClick(callback) {
  const innerRef = useRef();
  const callbackRef = useRef();

  // set current callback in ref, before second useEffect uses it
  useEffect(() => { // useEffect wrapper to be safe for concurrent mode
    callbackRef.current = callback;
  });

  useEffect(() => {
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);

    // read most recent callback and innerRef dom node from refs
    function 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 called
    alert(`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利用可变引用为。创建精益API Client。可以设置回调,而不必通过来记住useCallback。回调主体仍然可以访问最新的道具和状态(没有过期的关闭值)。


iOS用户注意事项

iOS仅将某些元素视为可点击的。为了避免这种情况,请选择其他外部点击侦听器,而不要选择document-包括在内的所有内容body。例如,您可以div在上面的示例中的React根上添加一个侦听器,并扩展其高度(height: 100vh或类似高度)以捕获所有外部点击。资料来源:quirksmode.org这个答案


不能在移动设备上工作,如此不幸
Omar

@Omar刚刚在我的帖子中使用android设备上的Firefox 67.0.3测试了Codesandbox-一切正常。您能否详细说明使用的浏览器,错误是什么?
ford04

没有错误,但不适用于Chrome,iPhone 7+。它不会检测到外面的水龙头。在移动设备上的chrome开发工具中,它可以工作,但在真正的ios设备中,它不能工作。
奥马尔(Omar)

1
@ ford04感谢您的挖掘,我偶然发现了另一个提示以下技巧的线程@media (hover: none) and (pointer: coarse) { body { cursor:pointer } }以我的整体风格添加它,似乎解决了这个问题。
Kostas Sarantopoulos

1
@ ford04是的,顺便说一句,根据这个线程,有一个更具体的媒体查询@supports (-webkit-overflow-scrolling: touch),仅针对IOS,即使我不知道多少!我刚刚对其进行了测试,并且可以正常工作。
Kostas Sarantopoulos

30

这里没有其他答案对我有用。我试图隐藏一个模糊的弹出窗口,但是由于内容是绝对定位的,因此即使单击内部内容,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.
    }
},

希望这可以帮助。


很好!谢谢。但是在我的情况下,使用div的绝对位置捕获“当前位置”是一个问题,因此我使用“ OnMouseLeave”作为div的输入和下拉日历,以在鼠标离开div时禁用所有div。
亚历克斯(Alex)

1
谢谢!帮我很多
安格拉·卢卡斯

1
我喜欢此方法,而不是喜欢那些方法document。谢谢。
kyw

为此,您需要确保clicked元素(relatedTarget)是可聚焦的。见stackoverflow.com/questions/42764494/...
xabitrigo

21

[更新]使用Hooks的React ^ 16.8解决方案

CodeSandbox

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的解决方案:

CodeSandbox

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;

3
这是现在的首选解决方案。如果出现错误.contains is not a function,可能是因为您将ref prop传递给了自定义组件,而不是像<div>
willma

对于那些试图将refprop 传递给自定义组件的人,您可能想看看React.forwardRef
onoya

4

这是我的方法(演示-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 leaks
    document.body.removeEventListener('click', this.handleClick);
  }

  handleClick(event) {
    const {container} = this.refs; // get container that we'll wait to be clicked outside
    const {onClickOutside} = this.props; // get click outside callback
    const {target} = event; // get direct click event target

    // if there is no proper callback - no point of checking
    if (typeof onClickOutside !== 'function') {
      return;
    }

    // if target is container - container was not clicked outside
    // if container contains clicked target - click was not outside of it
    if (target !== container && !container.contains(target)) {
      onClickOutside(event); // clicked outside - fire callback
    }
  }

  render() {
    return (
      <div ref="container">
        {this.props.children}
      </div>
    );
  }
}

很棒的解决方案-就我的使用而言,在页面内容简短的情况下,我改为听文档而不是正文,并将ref从字符串更改为dom参考:jsfiddle.net/agymay93/9
Billy Moon

并使用span而不是div,因为它不太可能更改布局,并添加了处理体外点击的演示:jsfiddle.net/agymay93/10
Billy Moon

ref不建议使用字符串,应使用ref回调:reactjs.org/docs/refs-and-the-dom.html
dain

4

这已经有很多答案,但他们没有解决 e.stopPropagation()并且无法单击您要关闭的元素之外的react链接。

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

3

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

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

现在,如果您在内容上添加单击处理程序,则该事件还将传播到上层div,因此触发该处理程序OutsideClick。如果这不是您想要的行为,只需停止处理程序上的事件传播。

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

`


这种方法的问题在于,您无法对内容进行任何点击-div会获得点击。
rbonick '17

由于内容位于div中,因此如果您不停止事件传播,则两者都将获得点击。这通常是理想的行为,但是如果您不希望将点击传播到div,只需在内容onClick处理程序上停止eventPropagation。我已更新我的答案以显示操作方法。
Anthony Garant

3

扩展Ben Bud接受的答案如果您使用的是样式组件,那么请继续这种方式传递ref将会给您带来错误,例如“ this.wrapperRef.contains不是函数”。

建议的修复方法(在注释中)将样式化的组件与div封装在一起,并在其中传递ref起作用。话虽如此,他们已经在他们的文档中解释了这样做的原因以及在styled-components中正确使用ref的原因:

将ref prop传递给样式化组件将为您提供StyledComponent包装器的实例,但不会传递给基础DOM节点。这是由于ref的工作方式。无法直接在我们的包装器上调用DOM方法(例如focus)。要获得对实际包装的DOM节点的引用,请将回调传递给innerRef属性。

像这样:

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

然后,您可以直接在“ handleClickOutside”函数中访问它:

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(
        <StyledDiv
            onBlur={this.blurHandler}
            tabIndex="0"
            innerRef={el => { this.el = el }}
        />
    )
}


2

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

范例程式码

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

1
嗯,很漂亮。它可以用于多个实例吗?还是每个固定元素都可能阻止其他元素的点击?
Thijs Koerselman'5

2
componentWillMount(){

  document.addEventListener('mousedown', this.handleClickOutside)
}

handleClickOutside(event) {

  if(event.path[0].id !== 'your-button'){
     this.setState({showWhatever: false})
  }
}

活动path[0]是最后点击的项目


3
请确保您删除的事件监听器当组件卸装防止内存管理问题,等等
agm1984

2

我使用了这个模块(与作者没有任何关系)

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

它做得很好。


这棒极了!比这里的许多其他答案要简单得多,并且与这里的至少3个其他解决方案不同,我收到的所有答案都可以立即解决"Support for the experimental syntax 'classProperties' isn't currently enabled"。对于尝试此解决方案的任何人,请记住在尝试其他解决方案时擦除可能已复制的所有挥之不去的代码。例如,添加事件侦听器似乎与此冲突。
Frikster

2

我这样做的部分原因是遵循此要求,并遵循React官方文档中关于处理需要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 => {
    if (this.inputRef.current === e.target) {
      return;
    }
    this.handleclickOutside();
  };
handleClickOutside(){
...***code to handle what to do when clicked outside***...
}
render(){
return(
<div>
...***code for what's outside***...
<span ref={this.inputRef}>
...***code for what's "inside"***...
</span>
...***code for what's outside***
)}}

1

策略的一个例子

我喜欢提供的解决方案,这些解决方案通过在组件周围创建包装器来执行相同的操作。

由于这更多是一种行为,因此我想到了Strategy并提出了以下建议。

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

请查看并告诉我您的想法。

ClickOutside行为

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
这将从用户的行为中删除启用/禁用样板,但是看起来不太好


1

在我的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 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>
  );
}

0

如果要使用此功能已经存在的微小组件(466字节压缩),则可以检出此库react-outclick

关于库的好处是,它还使您可以检测组件外部和组件内部的点击。它还支持检测其他类型的事件。


0

如果要使用此功能已经存在的微小组件(gzip压缩为466 Byte),则可以签出该库react-outclick。它捕获了React组件或jsx元素之外的事件。

关于库的好处是,它还使您可以检测组件外部和组件内部的点击。它还支持检测其他类型的事件。


0

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

例:

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

0
import { useClickAway } from "react-use";

useClickAway(ref, () => console.log('OUTSIDE CLICKED'));

0

您可以通过简单的方法解决您的问题,我向您展示:

....

const [dropDwonStatus , setDropDownStatus] = useState(false)

const openCloseDropDown = () =>{
 setDropDownStatus(prev => !prev)
}

const closeDropDown = ()=> {
 if(dropDwonStatus){
   setDropDownStatus(false)
 }
}
.
.
.
<parent onClick={closeDropDown}>
 <child onClick={openCloseDropDown} />
</parent>

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


-1

我从下面的文章中发现了这一点:

render(){return({this.node = node;}}>切换Popover {this.state.popupVisible &&(我是popover!)}});}}

这是有关此问题的精彩文章: “在React组件外部处理点击” https://larsgraubner.com/handle-outside-clicks-react/


欢迎使用Stack Overflow!仅仅是链接的答案通常会被皱眉:meta.stackoverflow.com/questions/251006/…。将来,请提供一个报价或代码示例以及与该问题相关的信息。干杯!
弗雷德·史塔克

-1

onClick处理程序添加到顶级容器中,并在用户单击时增加状态值。将该值传递给相关组件,只要该值发生更改,您就可以工作。

在这种情况下,this.closeDropdown()只要clickCount值更改,我们就会调用。

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

不确定谁在投票,但是与绑定/取消绑定事件侦听器相比,此解决方案非常干净。我已经实现了零问题。
wdm

-1

为了使“焦点”解决方案适用于事件侦听器的下拉菜单,您可以使用onMouseDown事件而不是onClick来添加它们。这样,事件将触发,然后弹出窗口将关闭,如下所示:

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

-1
import ReactDOM from 'react-dom' ;

class SomeComponent {

  constructor(props) {
    // First, add this to your constructor
    this.handleClickOutside = this.handleClickOutside.bind(this);
  }

  componentWillMount() {
    document.addEventListener('mousedown', this.handleClickOutside, false); 
  }

  // Unbind event on unmount to prevent leaks
  componentWillUnmount() {
    window.removeEventListener('mousedown', this.handleClickOutside, false);
  }

  handleClickOutside(event) {
    if(!ReactDOM.findDOMNode(this).contains(event.path[0])){
       console.log("OUTSIDE");
    }
  }
}

别忘了import ReactDOM from 'react-dom';
dipole_moment19'01-25

-1

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

您应该使用“高阶组件”来包装您想听其外部点击的组件。

该组件示例只有一个道具:“ onClickedOutside”,它接收一个函数。

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 component 
    if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
      // A props callback for the ClikedClickedOutside
      this.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>;
  }
}

-1

UseOnClickOutside Hook-React 16.8 +

创建一个通用的useOnOutsideClick函数

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答案的启发。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.