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

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

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


当前回答

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

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

@异常:单击要忽略的类

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

其他回答

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

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

@异常:单击要忽略的类

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

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

npm install react-onclickout --save

const ClickOutHandler=require('act-on-clickout');ExampleComponent类扩展React.Component{单击退出(e){if(hasClass(e.target,'忽略我'))返回;alert('用户在组件外部单击!');}render(){返回(<ClickOutHandler onClickOut={this.onClickOut}><div>点击我的外部</分区></ClickOutHandler>);}}

它做得很好。

我之所以这样做,部分原因是遵循了这一点,并遵循了React官方文件关于处理需要React ^16.3的参考文献。这是我在尝试了其他一些建议之后唯一有效的方法。。。

class App extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  componentWillMount() {
    document.addEventListener("mousedown", this.handleClick, false);
  }
  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClick, false);
  }
  handleClick = e => {
    /*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>
    )
  }
}
import React, { useState, useEffect, useRef } from "react";

const YourComponent: React.FC<ComponentProps> = (props) => {
  const ref = useRef<HTMLDivElement | null>(null);
  const [myState, setMyState] = useState(false);
  useEffect(() => {
    const listener = (event: MouseEvent) => {
      // we have to add some logic to decide whether or not a click event is inside of this editor
      // if user clicks on inside the div we dont want to setState
      // we add ref to div to figure out whether or not a user is clicking inside this div to determine whether or not event.target is inside the div
      if (
        ref.current &&
        event.target &&
        // contains is expect other: Node | null
        ref.current.contains(event.target as Node)
      ) {
        return;
      }
      // if we are outside
      setMyState(false);
    };
    // anytime user clics anywhere on the dom, that click event will bubble up into our body element
    // without { capture: true } it might not work
    document.addEventListener("click", listener, { capture: true });
    return () => {
      document.removeEventListener("click", listener, { capture: true });
    };
  }, []);

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

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