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

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

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


当前回答

以上答案中没有一个对我有效,所以我最终做了如下:

从“React”导入React,{Component};

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

其他回答

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;

所有提出的解决方案都假设可以将事件添加到文档中,并依赖于本机方法.contains()来区分事件是在当前组件内部还是外部触发的

ref.current.contains(event.target)

但这在React中并不总是有效的。事实上,在React中,React.createPortal API允许从一个组件中指定一个新的真实父组件,JSX在该组件中被呈现,但同时,事件冒泡被模拟为组件在声明的位置呈现(即React.cCreatePortal被调用的位置)。

这是通过将所有事件附加到应用程序根元素并在Javascript中模拟事件来实现的。

因此,在这种情况下,所提出的解决方案被打破了,因为在门户元素内部的单击,对于标准HTML来说,在当前元素之外,实际上应该像在内部一样进行处理。

所以我重写了这个问题的一个注释中提出的解决方案,并对其进行了重构以使用功能组件。这也适用于一个或多个嵌套门户。

export default function OutsideClickDetector({onOutsideClick, Component ='div', ...props} : OutsideClickDetectorProps) {
    const isClickInside = useRef<boolean>(false);

    const onMouseDown = () => {
        isClickInside.current = true;
    };
    
    const handleBodyClick = useCallback((e) => {
        if(!isClickInside.current) {
            onOutsideClick?.(e);
        }
        isClickInside.current = false;
    }, [isClickInside, onOutsideClick]);

    useEffect(() => {
        document.addEventListener('mousedown', handleBodyClick);
        return () => document.removeEventListener('mousedown', handleBodyClick);
    });

    return <Component onMouseDown={onMouseDown} onMouseUp={() => isClickInside.current = false}{...props} />;
}

以下解决方案使用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之外,则为true),这样我就可以用useEffect钩子捕捉到这个变化,我希望你能清楚。

下面是一个活生生的例子: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>
  );
}

将onClick处理程序添加到顶级容器中,并在用户单击时增加状态值。将该值传递给相关组件,每当该值发生变化时,您就可以执行操作。

在本例中,每当clickCount值更改时,我们都会调用this.closeDropdown()。

incrementClickCount方法在.app容器中激发,而不是在下拉列表中激发,因为我们使用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>
        );
    }
}