检测React组件外部的单击

时间:2015-09-13 18:34:01

标签: javascript dom reactjs

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

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

点击事件包含"路径"等属性。这似乎占据了事件所走过的dom路径。我不确定要比较什么或如何最好地遍历它,我认为某人必须已经把它放在一个聪明的效用函数中......不是吗?

43 个答案:

答案 0 :(得分:387)

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

要查看它的实际效果:Code Sandbox demo

// Decorate with reduxForm(). It will read the initialValues prop provided by connect()
const reduxFormDecorator = reduxForm({
  form: 'initializeFromState'  // a unique identifier for this form
});

const reduxConnector = connect(
  state => ({
    initialValues: state.account.data // pull initial values from account reducer
  }),
  { load: loadAccount }               // bind account loading action creator
);

export default reduxConnector(reduxFormDecorator(InitializeFromStateForm));

答案 1 :(得分:121)

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

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

要为任何元素提供焦点容量,只需确保其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>
        );
    }
});

答案 2 :(得分:84)

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

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

import onClickOutside from 'react-onclickoutside'

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

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

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

export default onClickOutside(NameOfComponent)

就是这样。

答案 3 :(得分:68)

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

答案 4 :(得分:38)

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

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

主:

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

答案 5 :(得分: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.
    }
},

希望这有帮助。

答案 6 :(得分:15)

[更新] Hacts

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;

答案 7 :(得分:14)

使用React Hooks(16.8 +)的解决方案

Codesandbox

单击外部通知挂钩

function useOuterClickNotifier(onOuterClick, innerRef) {
  useEffect(
    () => {
      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.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


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

  • 注册外部点击的副作用/状态逻辑可以完全抽象出来,并简化“内部”组件
  • 在任何地方重复使用useOuterClickNotifier
  • 没有多余的包装器组件(由挂钩逻辑替换)

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

希望,会有所帮助。

答案 8 :(得分:9)

Ez 方式...

const componentRef = useRef();

useEffect(() => {
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);
    function handleClick(e: any) {
        if(componentRef && componentRef.current){
            const ref: any = componentRef.current
            if(!ref.contains(e.target)){
                // put your action here
            }
        }
    }
}, []);

然后将引用放在您的组件上

<div ref={componentRef as any}> My Component </div>

答案 9 :(得分: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>
    );
  }
}

答案 10 :(得分:4)

这已经有很多答案,但是它们没有解决e.stopPropagation()的问题,因此无法单击您要关闭的元素之外的React链接。

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

答案 11 :(得分:4)

Material-UI有一个解决此问题的小组件:https://material-ui.com/components/click-away-listener/,您可以对其进行挑选。压缩后的重量为1.5 kB,支持移动,IE 11和门户。

答案 12 :(得分:3)

componentWillMount(){

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

handleClickOutside(event) {

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

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

答案 13 :(得分:3)

或者:

const onClickOutsideListener = () => {
    alert("click outside")
    document.removeEventListener("click", onClickOutsideListener)
  }

...

return (
  <div
    onMouseLeave={() => {
          document.addEventListener("click", onClickOutsideListener)
        }}
  >
   ...
  </div>

答案 14 :(得分:3)

要扩展Ben Bud所接受的答案,如果您使用的是样式组件,则以这种方式传递ref会给您带来错误,例如“ this.wrapperRef.contains不是函数”。 >

建议的修复方法(在注释中)将带div的样式化组件包装并在其中传递引用起作用。 话虽如此,他们已经在他们的docs中解释了这样做的原因以及在样式组件中正确使用引用的原因:

  

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

答案 15 :(得分: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>
    );
  }
}

很好地完成了这项工作。

答案 16 :(得分: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;
    }
}

答案 17 :(得分: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();
                                        }}/>

`

答案 18 :(得分:2)

我这样做的部分原因是跟随this并遵循React官方文档处理需要反应的refs ^ 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***
)}}

答案 19 :(得分: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)
    }
  }
}

样本用法

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构造函数的组件 这将删除此行为的用户的启用/禁用样板,但它看起来不是很好

答案 20 :(得分:2)

import { useClickAway } from "react-use";

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

答案 21 :(得分:1)

在我的DROPDOWN情况下,Ben Bud's solution运作良好,但是我有一个单独的切换按钮和一个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>
  );
}

答案 22 :(得分:1)

或者,也可以使用.closest方法。当您要检查单击是否在id =“ apple”元素之外时,我可以使用:

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

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

答案 23 :(得分:1)

带钩的打字稿

注意:我正在使用带有React.createRef的React版本16.3。对于其他版本,请使用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 listener
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener("mousedown", handleClickOutside);
    };
  });

  return (
    <div ref={ref}>
        ...
    </div>
  );
}

答案 24 :(得分:0)

https://stackoverflow.com/a/42234988/9536897在移动模式下不起作用。

比您可以尝试的:

  // returns true if the element or one of its parents has the class classname
  hasSomeParentTheClass(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 });
    });
  }
  • 在视图上,将className赋予您的特定元素。

答案 25 :(得分:0)

您可以通过简单的方法解决您的问题,我告诉您:

....

const [dropDwonStatus , setDropDownStatus] = useState(false)

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

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

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

答案 26 :(得分:0)

我遇到一种情况,当我需要有条件地将孩子插入到情态中时。像这样,吼叫。

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)在这里不起作用,因为一旦分离孩子,父(模态)就不再包含event.target

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

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

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

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

答案 27 :(得分:0)

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

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

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

import { useEffect } from 'react'

export function useOnClickRectOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      const targetEl = ref.current
      if (targetEl) {
        const clickedX = event.clientX
        const clickedY = event.clientY
        const rect = targetEl.getBoundingClientRect()
        const targetElTop = rect.top
        const targetElBottom = rect.top + rect.height
        const targetElLeft = rect.left
        const targetElRight = rect.left + rect.width

        if (
          // check X Coordinate
          targetElLeft < clickedX &&
          clickedX < targetElRight &&
          // check Y Coordinate
          targetElTop < clickedY &&
          clickedY < targetElBottom
        ) {
          return
        }

        // trigger event when the clickedX,Y is outside of the targetEl
        handler(event)
      }
    }

    document.addEventListener('mousedown', listener)
    document.addEventListener('touchstart', listener)

    return () => {
      document.removeEventListener('mousedown', listener)
      document.removeEventListener('touchstart', listener)
    }
  }, [ref, handler])
}

答案 28 :(得分: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>
 )
}

答案 29 :(得分:0)

非侵入性方式,无需添加另一个 DIV EL。

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

@exceptions:点击时忽略的类

@idException: 如果点击它要忽略的 id

TypeError: message.member.roles.some is not a function

用法

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 HTMLElement
    if (!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)
                break
              case "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 HTMLDivElement
        if (document.getElementById(idException)!.contains(target)) {
          exceptionsCount++
        }
      }

      if (exceptionsCount === 0) {
        // eslint-disable-next-line react/no-find-dom-node
        const node = ReactDOM.findDOMNode(instance)

        if (node && !node.contains(target)) {
          onClickOutside(e)
        }
      }
    })
  };

  render() {
    return React.Children.only(this.props.children)
  }
}

答案 30 :(得分:0)

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

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

答案 31 :(得分:0)

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

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

答案 32 :(得分:0)

我不知道这是否可以解决问题,但是我制作了一个“关闭弹出式系统”,创建了一个“动态div”,其位置为:“绝对”,覆盖了所有页面,仅当用户单击“用户菜单按钮”。

export default class Header extends Component {

state = {
    isClosed: true,
}

此方法通过“下拉列表div”进行处理

handleDropdown = () => {
    let accountDropdown = document.querySelector('.account-dropdown');
    let divHide = document.querySelector('.hide-menu');

当用户单击按钮时,会显示一个下拉列表以及动态div,并使用一种状态来控制打开和关闭

    const show = () => {
        divHide.style.display = "block";
        accountDropdown.style.display = "block";
        this.setState({isClosed: false});
    }

    const hide = () => {
        divHide.style.display = "none";
        accountDropdown.style.display = "none";
        this.setState({isClosed: true});
    }

    if (this.state.isClosed) {
        show();
    } else {
        hide();
    } 
}

现在,动态div覆盖了整个页面,如果用户单击任意位置,hidebyDiv函数将被触发并隐藏下拉列表和覆盖该页面的div

hidebyDiv = () => {
    document.querySelector('.hide-menu').style.display = "none";
    document.querySelector('.account-dropdown').style.display = "none";
    this.setState({isClosed: true});
}

HTML代码

render() {

    const hideMenu = {
        width: '100%', 
        minHeight: "900px",
        position: "absolute",
        display: "none",
    }

    return (

        <div className="hide-menu" style={hideMenu} onClick={this.hidebyDiv} />

    );

答案 33 :(得分:0)

参加聚会有点晚,但是我遇到了让所有这些都无法与React一起使用的问题。选择下拉列表,因为在onClick被点击时,我一直想从中单击的父项不再包含在单击的选项中被解雇。

我通过使用以下方法解决了这个问题:

componentDidMount() {
    document.addEventListener('mousedown', this.onClick );
}

componentWillUnmount() {
    document.removeEventListener('mousedown', this.onClick );
}

onClick = (event) => {
    if(!event.path.includes(this.detectOutsideClicksDiv)) {
        // Do stuff here
    }
}

答案 34 :(得分:0)

以上所有答案均不适用于我,因此这是我最终要做的事情:

从'react'导入React,{组件};

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

答案 35 :(得分:0)

UseOnClickOutside挂钩-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>
  );

};

Link to demo

部分受@ pau1fitzgerald答案的启发。

答案 36 :(得分:0)

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

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

此组件示例只有一个道具:“ 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>;
  }
}

答案 37 :(得分:0)

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

答案 38 :(得分:0)

要使'焦点'解决方案适用于事件侦听器的下拉菜单,可以使用 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>
                }

答案 39 :(得分:0)

向顶级容器添加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>
        );
    }
}

答案 40 :(得分:0)

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

render(){     回来(        {this.node = node; }}       &GT;                    切换Popover                  {this.state.popupVisible&amp;&amp; (                       我是一个人!                     )}            );   } }

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

答案 41 :(得分:-2)

这是最适合制作下拉菜单的方法:

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

答案 42 :(得分:-7)

您可以在主体上安装双击处理程序,在此元素上安装另一个处理程序。在此元素的处理程序中,只返回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>