检测React组件外部的点击

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

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

click事件包含“ path”之类的属性,该属性似乎包含事件经过的dom路径。我不确定要比较什么或如何最好地遍历它,并且我认为有人必须已经将其放在聪明的实用程序函数中了……不?

古一凯Gil2020/03/10 09:22:50

UseOnClickOutside Hook - React 16.8 +

Create a general useOnOutsideClick function

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, []);

Then use the hook in any functional component.

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

};

Link to demo

Partially inspired by @pau1fitzgerald answer.

凯斯丁达蒙2020/03/10 09:22:50

onClick处理程序添加到顶级容器中,并在用户单击时增加状态值。将该值传递给相关组件,只要该值发生更改,您就可以工作。

在这种情况下,this.closeDropdown()只要clickCount值更改,我们就会调用

incrementClickCount方法在.app容器内触发,但不会容器内触发.dropdown因为我们使用这种方法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>
        );
    }
}
小卤蛋村村2020/03/10 09:22:50

我从下面的文章中发现了这一点:

render(){return({this.node = node;}}>切换Popover {this.state.popupVisible &&(我是popover!)}});}}

这是有关此问题的精彩文章: “在React组件之外处理点击” https://larsgraubner.com/handle-outside-clicks-react/

西里Stafan小哥2020/03/10 09:22:50

如果要使用此功能已经存在的很小的组件(gzip压缩为466 Byte),则可以签出该库react-outclick它捕获了React组件或jsx元素之外的事件。

关于库的好处是它还使您可以检测组件外部和组件内部的点击。它还支持检测其他类型的事件。

逆天Green古一2020/03/10 09:22:50

Material-UI有一个解决此问题的小组件:https : //material-ui.com/components/click-away-listener/,您可以对其进行挑选。压缩后的重量为1.5 kB,它支持移动设备,IE 11和门户。

小卤蛋Pro2020/03/10 09:22:50

如果要使用此功能已经存在的很小的组件(gzip压缩为466 Byte),则可以签出该库react-outclick

关于库的好处是它还使您可以检测组件外部和组件内部的点击。它还支持检测其他类型的事件。

小小Eva2020/03/10 09:22:50

对于所有其他答案,我最大的担心是必须从根目录/父目录向下过滤单击事件。我发现最简单的方法是简单地设置一个具有以下位置的同级元素:fixed,下拉列表后面的z-index 1并处理同一组件内fixed元素上的click事件。将所有内容集中到给定的组件。

范例程式码

#HTML
<div className="parent">
  <div className={`dropdown ${this.state.open ? open : ''}`}>
    ...content
  </div>
  <div className="outer-handler" onClick={() => this.setState({open: false})}>
  </div>
</div>

#SASS
.dropdown {
  display: none;
  position: absolute;
  top: 0px;
  left: 0px;
  z-index: 100;
  &.open {
    display: block;
  }
}
.outer-handler {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    opacity: 0;
    z-index: 99;
    display: none;
    &.open {
      display: block;
    }
}
乐蛋蛋2020/03/10 09:22:50

使用React Hooks(16.8 +)的可重用解决方案

/*
  Custom outside click Hook
*/
function useOuterClickNotifier(onOuterClick, innerRef) {
  useEffect(
    () => {
      if (innerRef.current) { // add listener only, if element exists
        document.addEventListener("click", handleClick);
        // unmount previous listener first 
        return () => document.removeEventListener("click", handleClick);
      }
      
      function handleClick(e) {
        innerRef.current && !innerRef.current.contains(e.target) &&
          onOuterClick(e);
      }
    },
    [onOuterClick, innerRef] // invoke again, if deps have changed
  );
}

/*
  Usage 
*/
const InnerComp = () => {
  const innerRef = useRef(null);
  const handleOuterClick = useCallback( // memoized callback for optimized performance
    e => alert("clicked outside!"),
    [] // adjust deps to your needs
  );
  useOuterClickNotifier(handleOuterClick, innerRef);

  return (
      <div id="container" ref={innerRef}>
        Component
      </div>
  );
};

const App = () => (
  <div>
    <p>Click somewhere outside Component!</p>
    <InnerComp />
  </div>
);

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
#container { border: 1px solid red; padding: 20px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js" integrity="sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js" integrity="sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGww=" crossorigin="anonymous"></script>
<script> var {useRef, useEffect, useCallback} = React</script>
<div id="root"></div>


与基于类的解决方案相比具有优势

  • 可以封装外部点击副作用,并简化客户端组件中的逻辑
  • 可重用,而无需包装器组件或渲染道具(
  • React team expects Hooks to be the primary way people write React components (source)

Note for iOS users

iOS treats only certain elements as clickable. To circumvent this behavior, choose a different outer click listener than document - nothing upwards including body. E.g. you could add a listener on the React root div in above example and expand its height (height: 100vh or similar) to catch all outside clicks. Sources: quirksmode.org and this answer

Hope, that helps.

伽罗路易2020/03/10 09:22:50

这里没有其他答案对我有用。我试图隐藏一个模糊的弹出窗口,但是由于内容是绝对定位的,因此即使单击内部内容,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.
    }
},

希望这可以帮助。