检测React组件外部的单击

问题描述 投票:253回答:24

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

所以在我的组件中,我想将一个单击处理程序附加到窗口。当处理程序触发时,我需要将目标与我的组件的dom子项进行比较。

click事件包含“path”之类的属性,它似乎保存了事件所经过的dom路径。我不确定要比较什么或如何最好地遍历它,我认为有人必须已经把它放在一个聪明的实用功能中......不是吗?

javascript dom reactjs
24个回答
401
投票

以下解决方案使用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) {
  /**
   * Alert if clicked on outside of element
   */
  function handleClickOutside(event) {
    if (ref.current && !ref.current.contains(event.target)) {
      alert("You clicked outside of me!");
    }
  }

  useEffect(() => {
    // Bind the event listener
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener("mousedown", handleClickOutside);
    };
  });
}

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

4
投票

这已经有很多答案,但它们没有解决e.stopPropagation()并阻止点击你想要关闭的元素之外的反应链接。

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

我对所有其他答案的最大关注是必须从root / parent中过滤点击事件。我发现最简单的方法是简单地设置一个兄弟元素,其位置为: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;
    }
}

3
投票

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

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

现在,如果您在内容上添加单击处理程序,则事件也将传播到上部div,从而触发handlerOutsideClick。如果这不是您想要的行为,只需停止处理程序上的事件进程。

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

`


3
投票
componentWillMount(){

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

handleClickOutside(event) {

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

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


3
投票

我用this module(我与作者没有关联)

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
投票

为了扩展Ben Bud所接受的答案,如果你使用样式组件,那么传递refs会给你一个错误,例如“this.wrapperRef.contains不是一个函数”。

在注释中建议修复,用div包装样式组件并将ref传递到那里,起作用。话虽如此,在他们的docs中他们已经解释了这个的原因以及在样式组件中正确使用refs:

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

像这样:

<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
投票

战略的一个例子

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

因为这更像是一种我认为是战略的行为,并提出了以下建议。

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

请检讨并告诉我你的想法。

ClickOutsideBehavior

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

Sample Usage

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() {...}
}

Notes

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

component

const baseDidMount = component.componentDidMount; component.componentDidMount = () => { this.enable(); baseDidMount.call(component) } 是传递给component构造函数的组件。 这将删除此行为的用户的启用/禁用样板,但它看起来不是很好


2
投票

我这样做的部分是通过跟随ClickOutsideBehavior并遵循React官方文档来处理需要反应的refs ^ 16.3。在尝试了其他一些建议后,这是唯一有用的东西......

this

0
投票

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

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

这是一篇关于这个问题的精彩文章:“处理React组件之外的点击”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*** )}}


0
投票

https://larsgraubner.com/handle-outside-clicks-react/处理程序添加到顶级容器,并在用户单击时增加状态值。将该值传递给相关组件,每当值发生变化时,您都可以正常工作。

在这种情况下,只要onClick值发生变化,我们就会调用this.closeDropdown()

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

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

event.stopPropagation()

121
投票

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

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

要赋予任何元素具有焦点的能力,只需确保其tabindex属性设置为-1以外的任何值。在常规HTML中,可以通过设置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>
        );
    }
});

0
投票

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

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

0
投票
<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>
                }

0
投票

我为所有场合制定了解决方案。

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

这个组件示例只有一个prop:“onClickedOutside”,它接收一个函数。

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");
    }
  }
}
ClickedOutside.js

0
投票

UseOnClickOutside Hook - React 16.8 +

创建一个通用useOnOutsideClick函数

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

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

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的回答。


-7
投票

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

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

Link to demo

更新:没有jQuery:

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

86
投票

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

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

import onClickOutside from 'react-onclickoutside'

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

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

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

export default onClickOutside(NameOfComponent)

而已。


71
投票

我被困在同一个问题上。我在这里参加派对有点晚了,但对我来说这是一个非常好的解决方案。希望它对别人有帮助。你需要从findDOMNode导入react-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>
    );

}

在这里找到一个codesandbox示例。


38
投票

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

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

主要:

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

27
投票

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

希望这可以帮助。


17
投票

React Hooks解决方案(16.8 +)

Codesandbox

外部单击通知Hook

function useOuterClickNotifier(onOuterClick, innerRef) {
  useEffect(
    () => {
      // only add listener, if the element exists
      if (innerRef.current) {
        document.addEventListener("click", handleClick);
      }

      // unmount previous first in case inputs have changed
      return () => document.removeEventListener("click", handleClick);

      function handleClick(e) {
        innerRef.current && !innerRef.current.contains(e.target) && onOuterClick(e);
      }
    },
    [onOuterClick, innerRef] // invoke again, if inputs have changed
  );
}

“内在”组件

const InnerComp = () => {
  const innerRef = useRef(null);
  useOuterClickNotifier(
    // if you want to optimize performance a bit,
    // don't provide an anonymous function here
    // See link down under (*1)
    e => alert("clicked outside of this component!"),
    innerRef
  );
  return (
    <div ref={innerRef}>
      inside component
    </div>
  );
}

* 1个Tip: Optimizing Performance by Skipping Effects

IOS与可点击元素的怪癖(影响mousedownclick

内部监督办公室只将某些元素视为可点击 - 有关quirksmodeApple docs的更多信息。要绕过此行为,请选择其他外部单击侦听器元素(不包括body)。例如。您可以注册反应根<div id="root"></div>的点击次数,并将其高度扩展到整个视口(看看IOS Codesandbox)。甚至更好,更多的反应方式:完全避免全局变量并将显式元素传递给useOuterClickNotifier钩子,可以用来注册外部点击。

感谢@Omar提示。

背景

我的用例是一个弹出菜单,在菜单容器外单击时会自动关闭。这里使用钩子的最大优点是:

  • 注册外部点击的副作用/有状态逻辑可以完全抽象出来并简化“内部”组件
  • 到处都重用useOuterClickNotifier
  • 没有额外的包装器组件(由钩子逻辑代替)

然后,根据您的用例,您可以在外部单击回调中执行某些操作,为了简单起见,这里使用alert("clicked outside of this component!")存根。例如。使用useState钩子设置一些状态或调用切换回调(在我的例子中)来打开/关闭弹出菜单。

希望,这有帮助。


16
投票

[更新]使用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;

5
投票

这是我的方法(演示 - 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>
    );
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.