【问题标题】:Detect click outside React component检测 React 组件外部的点击
【发布时间】:2015-09-13 18:34:00
【问题描述】:

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

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

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

【问题讨论】:

  • 您能否将点击处理程序附加到父级而不是窗口?
  • 如果您将点击处理程序附加到父级,您知道该元素或其子级之一何时被点击,但我需要检测所有被点击的 other 位置,所以处理程序需要附加到窗口。
  • 我看了上一篇回复之后的文章。如何在顶部组件中设置一个 clickState 并传递孩子们的点击操作。然后你会检查孩子们的道具来管理打开关闭状态。
  • 顶部组件将是我的应用程序。但是监听组件有好几个层次,在dom中没有严格的定位。我不可能将点击处理程序添加到我的应用程序中的所有组件,因为其中一个有兴趣知道您是否点击了它之外的某个地方。其他组件不应该知道这个逻辑,因为这会产生可怕的依赖关系和样板代码。
  • 我想向你推荐一个非常好的库。由Airbnb创建:github.com/airbnb/react-outside-click-handler

标签: javascript dom reactjs


【解决方案1】:

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

查看实际效果:

钩子实现:

import React, { useRef, useEffect } from "react";

/**
 * Hook that alerts clicks outside of the passed ref
 */
function useOutsideAlerter(ref) {
  useEffect(() => {
    /**
     * Alert if clicked on outside of element
     */
    function handleClickOutside(event) {
      if (ref.current && !ref.current.contains(event.target)) {
        alert("You clicked outside of me!");
      }
    }
    // Bind the event 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>;
}

类实现:

16.3 之后

import React, { Component } from "react";

/**
 * Component that alerts if you click outside of it
 */
export default class OutsideAlerter extends Component {
  constructor(props) {
    super(props);

    this.wrapperRef = React.createRef();
    this.handleClickOutside = this.handleClickOutside.bind(this);
  }

  componentDidMount() {
    document.addEventListener("mousedown", this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClickOutside);
  }

  /**
   * Alert if clicked on outside of element
   */
  handleClickOutside(event) {
    if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {
      alert("You clicked outside of me!");
    }
  }

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

16.3 之前

import React, { Component } from "react";

/**
 * Component that alerts if you click outside of it
 */
export default class OutsideAlerter extends Component {
  constructor(props) {
    super(props);

    this.setWrapperRef = this.setWrapperRef.bind(this);
    this.handleClickOutside = this.handleClickOutside.bind(this);
  }

  componentDidMount() {
    document.addEventListener("mousedown", this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClickOutside);
  }

  /**
   * Set the wrapper ref
   */
  setWrapperRef(node) {
    this.wrapperRef = node;
  }

  /**
   * Alert if clicked on outside of element
   */
  handleClickOutside(event) {
    if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
      alert("You clicked outside of me!");
    }
  }

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

【讨论】:

  • 如何使用 React 的合成事件,而不是这里的 document.addEventListener
  • @polkovnikov.ph 1. context 仅在使用时才需要作为构造函数中的参数。在这种情况下没有使用它。 react 团队建议在构造函数中使用props 作为参数的原因是因为在调用super(props) 之前使用this.props 将是未定义的并且可能导致错误。 context 在内部节点上仍然可用,这就是 context 的全部目的。这样你就不必像使用 props 那样将它从一个组件传递到另一个组件。 2. 这只是一种风格偏好,在这种情况下不值得争论。
  • 我收到以下错误:“this.wrapperRef.contains 不是函数”
  • @Bogdan ,我在使用样式组件时遇到了同样的错误。考虑在顶层使用
  • 谢谢。它适用于我的某些情况,但不适用于 display: absolute 的弹出窗口 - 当您单击任何位置(包括内部)时,它总是会触发 handleClickOutside。最终切换到 react-outside-click-handler,这似乎以某种方式涵盖了这种情况
【解决方案2】:

2021 年更新:

自从我添加此响应以来已经有一段时间了,而且由于它似乎仍然引起了一些兴趣,我想我会将它更新到更新的 React 版本。在 2021 年,我会这样写这个组件:

import React, { useState } from "react";
import "./DropDown.css";

export function DropDown({ options, callback }) {
    const [selected, setSelected] = useState("");
    const [expanded, setExpanded] = useState(false);

    function expand() {
        setExpanded(true);
    }

    function close() {
        setExpanded(false);
    }

    function select(event) {
        const value = event.target.textContent;
        callback(value);
        close();
        setSelected(value);
    }

    return (
        <div className="dropdown" tabIndex={0} onFocus={expand} onBlur={close} >
            <div>{selected}</div>
            {expanded ? (
                <div className={"dropdown-options-list"}>
                    {options.map((O) => (
                        <div className={"dropdown-option"} onClick={select}>
                            {O}
                        </div>
                    ))}
                </div>
            ) : null}
        </div>
    );
}

原答案(2016 年):

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

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

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

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

这就是我用来制作自定义下拉菜单的目的。

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

【讨论】:

  • 这不会造成可访问性问题吗?由于您使一个额外的元素可聚焦只是为了检测它何时进入/离开焦点,但交互应该在下拉值上吗?
  • 我在某种程度上有这个“工作”,它是一个很酷的小技巧。我的问题是我的下拉内容是display:absolute,因此下拉内容不会影响父 div 的大小。这意味着当我单击下拉列表中的某个项目时,会触发 onblur。
  • 我遇到了这种方法的问题,下拉内容中的任何元素都不会触发诸如 onClick 之类的事件。
  • 如果下拉内容包含一些可聚焦的元素,例如表单输入,您可能会遇到一些其他问题。他们会偷走你的注意力,onBlur 将在你的下拉容器上触发,expanded 将被设置为 false。
  • 要单击目标内的元素,请参阅此答案:stackoverflow.com/a/44378829/642287
【解决方案3】:

我被困在同一个问题上。我在这里聚会有点晚了,但对我来说,这是一个非常好的解决方案。希望它对其他人有帮助。你需要从react-dom导入findDOMNode

import ReactDOM from 'react-dom';
// ... ✂

componentDidMount() {
    document.addEventListener('click', this.handleClickOutside, true);
}

componentWillUnmount() {
    document.removeEventListener('click', this.handleClickOutside, true);
}

handleClickOutside = event => {
    const domNode = ReactDOM.findDOMNode(this);

    if (!domNode || !domNode.contains(event.target)) {
        this.setState({
            visible: false
        });
    }
}

React Hooks 方法 (16.8 +)

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

import { useState, useEffect, useRef } from 'react';

export default function useComponentVisible(initialIsVisible) {
    const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);
    const ref = useRef(null);

    const handleClickOutside = (event) => {
        if (ref.current && !ref.current.contains(event.target)) {
            setIsComponentVisible(false);
        }
    };

    useEffect(() => {
        document.addEventListener('click', handleClickOutside, true);
        return () => {
            document.removeEventListener('click', handleClickOutside, true);
        };
    }, []);

    return { ref, isComponentVisible, setIsComponentVisible };
}

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

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

在此处查找codesandbox 示例。

【讨论】:

  • 这应该是公认的答案。非常适合具有绝对定位的下拉菜单。
  • ReactDOM.findDOMNode 已弃用,应使用 ref 回调:github.com/yannickcr/eslint-plugin-react/issues/…
  • 伟大而干净的解决方案
  • 这很棒,因为它可以重复使用。完美。
  • 这应该是公认的答案
【解决方案4】:

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

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

import onClickOutside from 'react-onclickoutside'

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

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

当导出我的组件时,我将它包裹在 onClickOutside():

export default onClickOutside(NameOfComponent)

就是这样。

【讨论】:

  • tabIndex/onBlur approach proposed by Pablo相比有什么具体的优势吗?实现如何工作,其行为与onBlur 方法有何不同?
  • 最好使用专用组件,然后使用 tabIndex hack。点赞!
  • @MarkAmery - 我对tabIndex/onBlur 方法发表了评论。当下拉菜单为position:absolute 时不起作用,例如悬停在其他内容上的菜单。
  • 使用它而不是 tabindex 的另一个优点是,如果您关注子元素,tabindex 解决方案也会触发模糊事件
  • @BeauSmith - 非常感谢!拯救了我的一天!
【解决方案5】:

基于 Tanner Linsley 优秀的 talk at JSConf Hawaii 2020 的 Hook 实现:

useOuterClickAPI

const Client = () => {
  const innerRef = useOuterClick(ev => {/*event handler code on outer click*/});
  return <div ref={innerRef}> Inside </div> 
};

实施

function useOuterClick(callback) {
  const callbackRef = useRef(); // initialize mutable ref, which stores callback
  const innerRef = useRef(); // returned to client, who marks "border" element

  // update cb on each render, so second useEffect has access to current value 
  useEffect(() => { callbackRef.current = callback; });
  
  useEffect(() => {
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);
    function handleClick(e) {
      if (innerRef.current && callbackRef.current && 
        !innerRef.current.contains(e.target)
      ) callbackRef.current(e);
    }
  }, []); // no dependencies -> stable click listener
      
  return innerRef; // convenience for client (doesn't need to init ref himself) 
}

这是一个工作示例:

/*
  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 利用mutable refs 提供精益Client API
  • 稳定 包含组件生命周期的点击监听器 ([] deps)
  • Client 可以设置回调,无需useCallback 记忆
  • 回调主体可以访问最新的道具和状态 - no stale closure values

(iOS 的附注)

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

【讨论】:

  • 没有错误,但不适用于 chrome、iphone 7+。它没有检测到外面的水龙头。在移动设备上的 chrome 开发工具中它可以工作,但在真正的 ios 设备中它不能工作。
  • @Omar 似乎是一个非常具体的 IOS 怪事。看这里:quirksmode.org/blog/archives/2014/02/mouse_event_bub.htmlstackoverflow.com/questions/18524177/…gravitydept.com/blog/js-click-event-bubbling-on-ios。我能想象到的最简单的解决方法是:在代码框示例中,为根 div 设置一个空的点击处理程序,如下所示:&lt;div onClick={() =&gt; {}}&gt; ... &lt;/div&gt;,以便 IOS 注册来自外部的所有点击。它适用于 Iphone 6+。这能解决你的问题吗?
  • 我需要测试一下。但感谢您的解决方案。不过看起来是不是很笨重?
  • @ford04 感谢您的挖掘,我偶然发现了另一个 thread,它暗示了以下技巧。 @media (hover: none) and (pointer: coarse) { body { cursor:pointer } } 在我的全局样式中添加这个,似乎解决了问题。
  • @ford04 是的,顺便说一句,根据this thread 有一个更具体的媒体查询@supports (-webkit-overflow-scrolling: touch) 仅针对IOS,即使我不知道更多!我刚刚测试了它,它可以工作。
【解决方案6】:

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

【讨论】:

  • 这是现在的首选解决方案。如果您收到错误 .contains is not a function,可能是因为您将 ref 属性传递给自定义组件,而不是像 &lt;div&gt; 这样的真实 DOM 元素
  • 对于那些试图将ref prop 传递给自定义组件的人,您可能想看看React.forwardRef
  • 这就像一个魅力,拯救了我的一天:)
  • 当你点击或按住滚动条时,它也会影响。如何让它检测到外部点击而不是滚动条?
【解决方案7】:

这里没有其他答案对我有用。我试图隐藏模糊的弹出窗口,但由于内容是绝对定位的,即使点击内部内容,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。
  • 谢谢!帮我很多。
  • 我比那些附加到document 的方法更喜欢这个。谢谢。
  • 为此,您需要确保单击的元素(relatedTarget)是可聚焦的。见stackoverflow.com/questions/42764494/…
  • 老实说应该是公认的答案,最干净的解决方案 ITT
【解决方案8】:

感谢 Ben Alpert 在discuss.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>
    )
  }
}

【讨论】:

  • 这对我来说不再适用于 React 0.14.7 - 也许 React 改变了一些东西,或者我在调整代码以适应对 React 的所有更改时出错。我改用github.com/Pomax/react-onclickoutside,它就像一个魅力。
  • 嗯。我看不出这应该起作用的任何理由。如果document 上的处理程序不是,为什么要保证应用程序根 DOM 节点上的处理程序在另一个处理程序触发重新渲染之前触发?
  • 纯用户体验点:mousedown 可能比click 更好。在大多数应用程序中,单击外部关闭菜单的行为发生在您按下鼠标的那一刻,而不是在您释放时。例如,使用 Stack Overflow 的 flagshare 对话框或浏览器顶部菜单栏中的下拉菜单之一进行尝试。
  • ReactDOM.findDOMNode 和字符串 ref 已弃用,应使用 ref 回调:github.com/yannickcr/eslint-plugin-react/issues/…
  • 这是最简单的解决方案,即使在附加到 document 时也能完美地为我工作
【解决方案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
            }
        }
    }
}, []);

然后将 ref 放在你的组件上

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

【讨论】:

  • 有了这个我不需要任何反应包。谢谢兄弟。
【解决方案10】:

MUI 有一个小组件来解决这个问题:https://mui.com/components/click-away-listener/#unstyled,你可以挑选它。压缩后的重量低于 1 kB,支持移动设备、IE 11 和门户网站。

【讨论】:

    【解决方案11】:

    或者:

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

    【讨论】:

      【解决方案12】:

      对于那些需要绝对定位的人,我选择的一个简单选项是添加一个包装器组件,该组件的样式可以用透明背景覆盖整个页面。然后你可以在这个元素上添加一个 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();
                                              }}/>
      

      `

      【讨论】:

      • 这种方法的问题是您不能对内容进行任何点击 - div 将收到点击。
      • 由于内容在 div 中,如果您不停止事件传播,两者都会收到点击。这通常是一种理想的行为,但如果您不希望将点击传播到 div,只需在您的内容 onClick 处理程序上停止 eventPropagation。我已经更新了我的答案以显示如何。
      • 这将不允许您与页面上的其他元素进行交互,因为它们将被包装器 div 覆盖
      • 这个解决方案应该得到更多的支持。除了易于实现之外,它的主要优势之一是它禁用了与其他元素的交互。这是本机警报和下拉菜单的默认行为,因此应该用于模式和下拉菜单的所有自定义实现。
      • 为了能够在包装器处于活动状态时与任何所需组件进行交互,只需为您想要交互的那些组件设置更高的 z-index。
      【解决方案13】:

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

      由于 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;
      

      【讨论】:

        【解决方案14】:
        import { useClickAway } from "react-use";
        
        useClickAway(ref, () => console.log('OUTSIDE CLICKED'));
        

        【讨论】:

        【解决方案15】:

        我这样做的部分原因是关注 this 并遵循 React 官方文档处理需要 React ^16.3 的 refs。在尝试了这里的其他一些建议之后,这是唯一对我有用的东西......

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

        【讨论】:

        • 更正这个 this.handleclickOutside();应该是 this.handleClickOutside()
        【解决方案16】:

        这是我的方法(演示 - 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>
            );
          }
        }
        

        【讨论】:

        【解决方案17】:

        为了扩展Ben Bud 所接受的答案,如果您使用的是样式化组件,那么以这种方式传递 refs 会给您一个错误,例如“this.wrapperRef.contains is not a function”。

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

        将 ref prop 传递给样式化组件将为您提供 StyledComponent 包装器的实例,但不会传递给底层 DOM 节点。这是由于 refs 的工作方式。无法直接在我们的包装器上调用 DOM 方法,例如 focus。 要获取实际包装的 DOM 节点的 ref,请将回调传递给 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 }}
                />
            )
        }
        

        【讨论】:

          【解决方案18】:

          我用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 个其他解决方案不同,我收到的所有其他解决方案都收到了"Support for the experimental syntax 'classProperties' isn't currently enabled",这只是开箱即用。对于尝试此解决方案的任何人,请记住删除您在尝试其他解决方案时可能复制的任何挥之不去的代码。例如添加事件监听器似乎与此冲突。
          • 这个here有一个更新版本
          【解决方案19】:

          带钩子的打字稿

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

          下拉组件:

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

          【讨论】:

          • 来到这里是为了 TypeScript 解决方案...没有其他方法可以绕过 event.target as Node 部分吗?
          【解决方案20】:

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

          示例代码

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

          【讨论】:

          • 嗯,很漂亮。它适用于多个实例吗?还是每个固定元素都可能阻止其他元素的点击?
          【解决方案21】:
          componentWillMount(){
          
            document.addEventListener('mousedown', this.handleClickOutside)
          }
          
          handleClickOutside(event) {
          
            if(event.path[0].id !== 'your-button'){
               this.setState({showWhatever: false})
            }
          }
          

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

          【讨论】:

          • 确保在组件卸载时移除事件监听器以防止内存管理问题等。
          【解决方案22】:

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

          【讨论】:

          • 非常有用的答案。在我分离两个 ref 的那一刻,当我单击两个组件中的任何一个时,不会触发 onClickOutside 函数
          【解决方案23】:

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

          这是组件现在的样子:

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

          【讨论】:

            【解决方案24】:

            策略示例

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

            由于这更像是一种行为,我想到了策略并想出了以下内容。

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

            【讨论】:

              【解决方案25】:

              UseOnClickOutside Hook - React 16.8 +

              创建一个通用的OnOutsideClick函数

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

              【讨论】:

              【解决方案26】:

              我知道这是一个老问题,但我不断遇到这个问题,我很难以简单的格式解决这个问题。因此,如果这会让任何人的生活更轻松,请使用 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>
               )
              }
              

              【讨论】:

                【解决方案27】:

                我喜欢@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】:

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

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

                  @exceptions:点击后忽略的类

                  @idException: id 如果点击则忽略

                  import React from "react"
                  import ReactDOM from "react-dom"
                  type Func1<T1, R> = (a1: T1) => R
                  
                  
                  export function closest(
                      el: Element,
                      fn: (el: Element) => boolean
                    ): Element | undefined {
                      let el_: Element | null = el;
                    
                      while (el_) {
                        if (fn(el_)) {
                          return el_;
                        }
                    
                        el_ = el_.parentElement;
                      }
                    }
                  let instances: ClickOutside[] = []
                  
                  type Props = {
                    idException?: string,
                    exceptions?: (string | Func1<MouseEvent, boolean>)[]
                    handleClickOutside: Func1<MouseEvent, void>
                  
                  }
                  
                  
                  export default class ClickOutside extends React.Component<Props> {
                    static defaultProps = {
                      exceptions: []
                    };
                  
                    componentDidMount() {
                      if (instances.length === 0) {
                        document.addEventListener("mousedown", this.handleAll, true)
                        window.parent.document.addEventListener(
                          "mousedown",
                          this.handleAll,
                          true
                        )
                      }
                      instances.push(this)
                    }
                  
                    componentWillUnmount() {
                      instances.splice(instances.indexOf(this), 1)
                      if (instances.length === 0) {
                        document.removeEventListener("mousedown", this.handleAll, true)
                        window.parent.document.removeEventListener(
                          "mousedown",
                          this.handleAll,
                          true
                        )
                      }
                    }
                  
                    handleAll = (e: MouseEvent) => {
                  
                      const target: HTMLElement = e.target as 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)
                    }
                  }
                  

                  用法

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

                  【讨论】:

                    【解决方案29】:

                    这是我解决问题的方法

                    我从我的自定义钩子返回一个布尔值,当这个值改变时(如果点击在我作为 arg 传递的 ref 之外,则为 true),这样我可以用 useEffect 钩子捕捉这个变化,我希望你很清楚。

                    这是一个活生生的例子: Live Example on codesandbox

                    import { useEffect, useRef, useState } from "react";
                    
                    const useOutsideClick = (ref) => {
                      const [outsieClick, setOutsideClick] = useState(null);
                    
                      useEffect(() => {
                        const handleClickOutside = (e) => {
                          if (!ref.current.contains(e.target)) {
                            setOutsideClick(true);
                          } else {
                            setOutsideClick(false);
                          }
                    
                          setOutsideClick(null);
                        };
                    
                        document.addEventListener("mousedown", handleClickOutside);
                    
                        return () => {
                          document.removeEventListener("mousedown", handleClickOutside);
                        };
                      }, [ref]);
                    
                      return outsieClick;
                    };
                    
                    export const App = () => {
                      const buttonRef = useRef(null);
                      const buttonClickedOutside = useOutsideClick(buttonRef);
                    
                      useEffect(() => {
                        // if the the click was outside of the button
                        // do whatever you want
                        if (buttonClickedOutside) {
                          alert("hey you clicked outside of the button");
                        }
                      }, [buttonClickedOutside]);
                    
                      return (
                        <div className="App">
                          <button ref={buttonRef}>click outside me</button>
                        </div>
                      );
                    }
                    

                    【讨论】:

                      【解决方案30】:

                      我是从下面的文章中找到的:

                      渲染(){ 返回 ( { this.node = 节点; }} > 切换弹出框 {this.state.popupVisible && ( 我是爆米花! )} ); } }

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

                      【讨论】:

                        猜你喜欢
                        相关资源
                        最近更新 更多
                        热门标签