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

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

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


当前回答

基于Tanner Linsley在2020年夏威夷联合会议上的精彩演讲:

使用OuterClick API

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

下面是一个工作示例:

/*自定义挂钩*/函数useOuterClick(回调){const innerRef=useRef();const callbackRef=useRef();//在ref中设置当前回调,然后第二个useEffect使用它useEffect(()=>{//useEffect包装器对于并发模式是安全的callbackRef.current=回调;});使用效果(()=>{document.addEventListener(“单击”,handleClick);return()=>document.removeEventListener(“单击”,handleClick);//从ref中读取最近的回调和innerRefdom节点函数句柄Click(e){如果(内部参考当前&&回调参考当前&&!innerRef.current.contains(e.target)) {callbackRef.current(e);}}}, []); // 无需回调+innerRef-depreturn innerRef;//返回参考;客户端可以省略`useRef`}/*用法*/常量客户端=()=>{const[counter,setCounter]=useState(0);const innerRef=useOuterClick(e=>{//调用处理程序时,计数器状态是最新的alert(`Clicked outside!Increment counter to${counter+1}`);设置计数器(c=>c+1);});返回(<div><p>点击外面</p><div id=“container”ref={innerRef}>内部,计数器:{counter}</div></div>);};ReactDOM.render(<Client/>,document.getElementById(“root”));#容器{边框:1px纯红色;填充:20px;}<script src=“https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js“integrity=”sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=“crossrorigin=”匿名“></script><script src=“https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js“integrity=”sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGw=“crossrorigin=”匿名“></script><script>var{useRef,useEffect,useCallback,useState}=反应</script><div id=“root”></div>

要点

useOuterClick利用可变引用提供瘦客户端API包含组件([]deps)的生命周期的稳定单击侦听器客户端可以设置回调,而无需使用callback将其记忆回调主体可以访问最新的属性和状态-没有过时的闭包值

(iOS的侧注)

iOS通常只将某些元素视为可点击的。要使外部单击有效,请选择一个不同于文档的单击侦听器-不向上包括正文。例如,在React根div上添加一个监听器,并扩展其高度,如height:100vh,以捕捉所有外部点击。来源:quicksmod.org

其他回答

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

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

它做得很好。

使用OnClickOutside Hook-反应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>
  );

};

链接到演示

部分灵感来自于@pau1itzgerald的回答。

这是我解决问题的方法

我从我的自定义钩子返回一个布尔值,当这个值发生变化时(如果单击在我作为参数传递的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>
  );
}

带钩的字体

注意:我使用的是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>
  );
}

因为对我来说!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]);