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

因此,在我的组件中,我想将一个单击处理程序附加到窗口。当处理程序启动时,我需要将目标与组件的dom子级进行比较。

click事件包含类似“path”的财产,它似乎保存了事件经过的dom路径。我不知道该比较什么,或者如何最好地遍历它,我想肯定有人已经把它放在了一个聪明的效用函数中。。。不


当前回答

只需使用mui(material ui)中的ClickAwayListener:

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

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

其他回答

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

在注释中,建议的修复方法是用div包装样式化的组件,并将ref传递到那里。尽管如此,在他们的文档中,他们已经解释了这一点的原因,以及在样式化组件中正确使用ref:

将ref属性传递给样式化组件将为您提供StyledComponent包装器的实例,但不会传递给底层DOM节点。这是由于裁判的工作方式。不可能直接在包装器上调用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 }}
        />
    )
}

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

要将其付诸行动:

挂钩实施反应16.3后的类实现反应16.3之前的类实现

挂钩实施:

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

因为对我来说!ref.current.contains(e.target)无法工作,因为ref中包含的DOM元素正在更改,我提出了一个稍微不同的解决方案:

function useClickOutside<T extends HTMLElement>(
  element: T | null,
  onClickOutside: () => void,
) {
  useEffect(() => {
    function handleClickOutside(event: MouseEvent) {
      const xCoord = event.clientX;
      const yCoord = event.clientY;

      if (element) {
        const { right, x, bottom, y } = element.getBoundingClientRect();
        if (xCoord < right && xCoord > x && yCoord < bottom && yCoord > y) {
          return;
        }

        onClickOutside();
      }
    }

    document.addEventListener('click', handleClickOutside);
    return () => {
      document.removeEventListener('click', handleClickOutside);
    };
  }, [element, onClickOutside]);
import { RefObject, useEffect } from 'react';

const useClickOutside = <T extends HTMLElement>(ref: RefObject<T>, fn: () => void) => {
    useEffect(() => {
        const element = ref?.current;
        function handleClickOutside(event: Event) {
            if (element && !element.contains(event.target as Node | null)) {
                fn();
            }
        }
        document.addEventListener('mousedown', handleClickOutside);
        return () => {
            document.removeEventListener('mousedown', handleClickOutside);
        };
    }, [ref]);
};

export default useClickOutside;

我喜欢@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])
}