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

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

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


当前回答

带钩的字体

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

为了扩展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 }}
        />
    )
}

这里的其他答案都不适合我。我试图在模糊上隐藏一个弹出窗口,但由于内容是绝对定位的,所以即使单击内部内容,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.
    }
},

希望这有帮助。

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

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

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

带钩的字体

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