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

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

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


当前回答

import { useClickAway } from "react-use";

useClickAway(ref, () => console.log('OUTSIDE CLICKED'));

其他回答

所有提出的解决方案都假设可以将事件添加到文档中,并依赖于本机方法.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} />;
}

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

https://stackoverflow.com/a/42234988/9536897解决方案在手机上不起作用。

您可以尝试:

  // returns true if the element or one of its parents has the class classname
  hasSomeParentTheClass(element, classname) {
    if(element.target)
    element=element.target;
    
    if (element.className&& element.className.split(" ").indexOf(classname) >= 0) return true;
    return (
      element.parentNode &&
      this.hasSomeParentTheClass(element.parentNode, classname)
    );
  }
  componentDidMount() {
    const fthis = this;

    $(window).click(function (element) {
      if (!fthis.hasSomeParentTheClass(element, "myClass"))
        fthis.setState({ pharmacyFocus: null });
    });
  }

在视图中,将className赋给特定元素。

我有一个类似的用例,我必须开发一个自定义下拉菜单。当用户在外面单击时,它应该自动关闭。以下是最近的React Hooks实现-

从“react”导入{useEffect,useRef,useState};导出常量应用程序=()=>{const-ref=useRef();const[isMenuOpen,setIsMenuOpen]=useState(false);使用效果(()=>{常量checkIfClickedOutside=(e)=>{//如果菜单是打开的并且点击的目标不在菜单内,//然后关闭菜单if(isMenuOpen&&ref.current&&!ref.current.contents(e.target)){setIsMenuOpen(false);}};document.addEventListener(“mousedown”,checkIfClickedOutside);返回()=>{//清理事件侦听器document.removeEventListener(“mousedown”,checkIfClickedOutside);};},[isMenuOpen]);返回(<div className=“wrapper”ref={ref}><按钮className=“button”onClick={()=>setIsMenuOpen((oldState)=>!oldState)}>单击我</按钮>{isMenuOpen&&(<ul className=“list”><li className=“list item”>下拉选项1</li><li className=“list item”>下拉选项2</li><li className=“list item”>下拉选项3</li><li className=“list item”>下拉选项4</li></ul>)}</div>);}

所以我遇到了一个类似的问题,但在我的案例中,这里选择的答案不起作用,因为我有一个下拉菜单按钮,这是文档的一部分。因此,单击该按钮也会触发handleClickOutside函数。为了防止触发,我必须向按钮和这个添加一个新的引用!menuBtnRef.current.contents(e.target)设置为条件。如果有人像我一样面临同样的问题,我就把它留在这里。

下面是组件现在的样子:


const Component = () => {

    const [isDropdownOpen, setIsDropdownOpen] = useState(false);
    const menuRef     = useRef(null);
    const menuBtnRef  = useRef(null);

    const handleDropdown = (e) => {
        setIsDropdownOpen(!isDropdownOpen);
    }

    const handleClickOutside = (e) => {
        if (menuRef.current && !menuRef.current.contains(e.target) && !menuBtnRef.current.contains(e.target)) {
            setIsDropdownOpen(false);
        }
    }

    useEffect(() => {
        document.addEventListener('mousedown', handleClickOutside, true);
        return () => {
            document.removeEventListener('mousedown', handleClickOutside, true);
        };
    }, []);

    return (

           <button ref={menuBtnRef} onClick={handleDropdown}></button>

           <div ref={menuRef} className={`${isDropdownOpen ? styles.dropdownMenuOpen : ''}`}>
                // ...dropdown items
           </div>
    )
}