Javascript 检测 React 组件外的点击
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/32553158/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Detect click outside React component
提问by Thijs Koerselman
I'm looking for a way to detect if a click event happened outside of a component, as described in this article. jQuery closest() is used to see if the the target from a click event has the dom element as one of its parents. If there is a match the click event belongs to one of the children and is thus not considered to be outside of the component.
我正在寻找一种方法来检测单击事件是否发生在组件之外,如本文所述。jQuery Nearest() 用于查看来自单击事件的目标是否将 dom 元素作为其父元素之一。如果存在匹配,则单击事件属于其中一个子项,因此不会被视为在组件之外。
So in my component I want to attach a click handler to window. When the handler fires I need to compare the target with the dom children of my component.
所以在我的组件中,我想将一个点击处理程序附加到窗口。当处理程序触发时,我需要将目标与组件的 dom 子项进行比较。
The click event contains properties like "path" which seems to hold the dom path that the event has travelled. I'm not sure what to compare or how to best traverse it, and I'm thinking someone must have already put that in a clever utility function... No?
click 事件包含诸如“path”之类的属性,它似乎保存了事件经过的 dom 路径。我不确定要比较什么或如何最好地遍历它,我想一定有人已经把它放在一个聪明的效用函数中......不是吗?
回答by Ben Bud
The following solution uses ES6 and follows best practices for binding as well as setting the ref through a method.
以下解决方案使用 ES6 并遵循绑定的最佳实践以及通过方法设置 ref。
To see it in action:
要查看它的实际效果:
Class Implementation:
类实现:
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>;
}
}
OutsideAlerter.propTypes = {
children: PropTypes.element.isRequired,
};
Hooks Implementation:
钩子实现:
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>;
}
回答by Pablo Barría Urenda
Here is the solution that best worked for me without attaching events to the container:
这是最适合我的解决方案,无需将事件附加到容器:
Certain HTML elements can have what is known as "focus", for example input elements. Those elements will also respond to the blurevent, when they lose that focus.
某些 HTML 元素可以具有所谓的“焦点”,例如输入元素。当这些元素失去焦点时,它们也会响应模糊事件。
To give any element the capacity to have focus, just make sure its tabindex attribute is set to anything other than -1. In regular HTML that would be by setting the tabindex
attribute, but in React you have to use tabIndex
(note the capital I
).
要使任何元素具有获得焦点的能力,只需确保将其 tabindex 属性设置为 -1 以外的任何值。在常规 HTML 中,这将通过设置tabindex
属性来实现,但在 React 中您必须使用tabIndex
(注意大写I
)。
You can also do it via JavaScript with element.setAttribute('tabindex',0)
您也可以通过 JavaScript 使用 element.setAttribute('tabindex',0)
This is what I was using it for, to make a custom DropDown menu.
这就是我用它来制作自定义下拉菜单的目的。
var DropDownMenu = React.createClass({
getInitialState: function(){
return {
expanded: false
}
},
expand: function(){
this.setState({expanded: true});
},
collapse: function(){
this.setState({expanded: false});
},
render: function(){
if(this.state.expanded){
var dropdown = ...; //the dropdown content
} else {
var dropdown = undefined;
}
return (
<div className="dropDownMenu" tabIndex="0" onBlur={ this.collapse } >
<div className="currentValue" onClick={this.expand}>
{this.props.displayValue}
</div>
{dropdown}
</div>
);
}
});
回答by Paul Fitzgerald
I was stuck on the same issue. I am a bit late to the party here, but for me this is a really good solution. Hopefully it will be of help to someone else. You need to import findDOMNode
from react-dom
我被困在同样的问题上。我参加聚会有点晚了,但对我来说这是一个非常好的解决方案。希望它会对其他人有所帮助。您需要findDOMNode
从react-dom
import ReactDOM from 'react-dom';
// ... ?
componentDidMount() {
document.addEventListener('click', this.handleClickOutside, true);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClickOutside, true);
}
handleClickOutside = event => {
const domNode = ReactDOM.findDOMNode(this);
if (!domNode || !domNode.contains(event.target)) {
this.setState({
visible: false
});
}
}
React Hooks Approach (16.8 +)
React Hooks 方法 (16.8 +)
You can create a reusable hook called useComponentVisible
.
您可以创建一个名为useComponentVisible
.
import { useState, useEffect, useRef } from 'react';
export default function useComponentVisible(initialIsVisible) {
const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);
const ref = useRef(null);
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
setIsComponentVisible(false);
}
};
useEffect(() => {
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
});
return { ref, isComponentVisible, setIsComponentVisible };
}
Then in the component you wish to add the functionality to do the following:
然后在您希望添加功能的组件中执行以下操作:
const DropDown = () => {
const { ref, isComponentVisible } = useComponentVisible(true);
return (
<div ref={ref}>
{isComponentVisible && (<p>Dropdown Component</p>)}
</div>
);
}
Find a codesandboxexample here.
在此处查找代码沙盒示例。
回答by Beau Smith
After trying many methods here, I decided to use github.com/Pomax/react-onclickoutsidebecause of how complete it is.
在这里尝试了很多方法后,我决定使用github.com/Pomax/react-onclickoutside,因为它很完整。
I installed the module via npm and imported it into my component:
我通过 npm 安装了模块并将其导入到我的组件中:
import onClickOutside from 'react-onclickoutside'
Then, in my component class I defined the handleClickOutside
method:
然后,在我的组件类中,我定义了handleClickOutside
方法:
handleClickOutside = () => {
console.log('onClickOutside() method called')
}
And when exporting my component I wrapped it in onClickOutside()
:
在导出我的组件时,我将其包裹在onClickOutside()
:
export default onClickOutside(NameOfComponent)
That's it.
就是这样。
回答by Thijs Koerselman
I found a solution thanks to Ben Alpert on discuss.reactjs.org. The suggested approach attaches a handler to the document but that turned out to be problematic. Clicking on one of the components in my tree resulted in a rerender which removed the clicked element on update. Because the rerender from React happens beforethe document body handler is called, the element was not detected as "inside" the tree.
感谢 Ben Alpert 在Discussion.reactjs.org上找到了一个解决方案。建议的方法将处理程序附加到文档,但结果证明这是有问题的。单击我的树中的一个组件会导致重新渲染,从而在更新时删除了单击的元素。由于 React 的重新渲染发生在调用文档正文处理程序之前,因此未检测到该元素在树的“内部”。
The solution to this was to add the handler on the application root element.
对此的解决方案是在应用程序根元素上添加处理程序。
main:
主要的:
window.__myapp_container = document.getElementById('app')
React.render(<App/>, window.__myapp_container)
component:
成分:
import { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
export default class ClickListener extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
onClickOutside: PropTypes.func.isRequired
}
componentDidMount () {
window.__myapp_container.addEventListener('click', this.handleDocumentClick)
}
componentWillUnmount () {
window.__myapp_container.removeEventListener('click', this.handleDocumentClick)
}
/* using fat arrow to bind to instance */
handleDocumentClick = (evt) => {
const area = ReactDOM.findDOMNode(this.refs.area);
if (!area.contains(evt.target)) {
this.props.onClickOutside(evt)
}
}
render () {
return (
<div ref='area'>
{this.props.children}
</div>
)
}
}
回答by ford04
Hook implementation based on Tanner Linsley's excellent talk at JSConf Hawaii 2020:
基于 Tanner Linsley在 JSConf Hawaii 2020上的精彩演讲的Hook 实现:
useOuterClick
Hook API
useOuterClick
钩子API
const Client = () => {
const innerRef = useOuterClick(ev => { /* what you want do on outer click */ });
return <div ref={innerRef}> Inside </div>
};
Implementation
执行
/*
Custom Hook
*/
function useOuterClick(callback) {
const innerRef = useRef();
const callbackRef = useRef();
// set current callback in ref, before second useEffect uses it
useEffect(() => { // useEffect wrapper to be safe for concurrent mode
callbackRef.current = callback;
});
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
// read most recent callback and innerRef dom node from refs
function handleClick(e) {
if (
innerRef.current &&
callbackRef.current &&
!innerRef.current.contains(e.target)
) {
callbackRef.current(e);
}
}
}, []); // no need for callback + innerRef dep
return innerRef; // return ref; client can omit `useRef`
}
/*
Usage
*/
const Client = () => {
const [counter, setCounter] = useState(0);
const innerRef = useOuterClick(e => {
// counter state is up-to-date, when handler is called
alert(`Clicked outside! Increment counter to ${counter + 1}`);
setCounter(c => c + 1);
});
return (
<div>
<p>Click outside!</p>
<div id="container" ref={innerRef}>
Inside, counter: {counter}
</div>
</div>
);
};
ReactDOM.render(<Client />, document.getElementById("root"));
#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, useState} = React</script>
<div id="root"></div>
useOuterClick
makes use of mutable refsto create a lean API for the Client
. A callback can be set without having to memoize it via useCallback
. The callback body still has access to the most recent props and state (no stale closure values).
useOuterClick
使用可变引用为Client
. 可以设置回调而无需通过useCallback
. 回调主体仍然可以访问最新的 props 和 state(没有陈旧的闭包值)。
Note for iOS users
iOS 用户注意事项
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.organd this answer
iOS 仅将某些元素视为可点击的。要规避此行为,请选择不同的外部点击侦听器document
- 不包括body
. 例如,您可以div
在上面的示例中在 React 根上添加一个侦听器并扩展其高度(height: 100vh
或类似的高度)以捕获所有外部点击。资料来源:quirksmode.org和这个答案
回答by Niyaz
None of the other answers here worked for me. I was trying to hide a popup on blur, but since the contents were absolutely positioned, the onBlur was firing even on the click of inner contents too.
这里的其他答案都不适合我。我试图隐藏一个关于模糊的弹出窗口,但由于内容是绝对定位的,即使点击内部内容,onBlur 也会触发。
Here is an approach that did work for me:
这是一种对我有用的方法:
// 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.
}
},
Hope this helps.
希望这可以帮助。
回答by onoya
[Update]Solution with React ^16.8using Hooks
[更新]使用Hooks使用React ^16.8 的解决方案
import React, { useEffect, useRef, useState } from 'react';
const SampleComponent = () => {
const [clickedOutside, setClickedOutside] = useState(false);
const myRef = useRef();
const handleClickOutside = e => {
if (!myRef.current.contains(e.target)) {
setClickedOutside(true);
}
};
const handleClickInside = () => setClickedOutside(false);
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
});
return (
<button ref={myRef} onClick={handleClickInside}>
{clickedOutside ? 'Bye!' : 'Hello!'}
</button>
);
};
export default SampleComponent;
Solution with React ^16.3:
React ^16.3 的解决方案:
import React, { Component } from "react";
class SampleComponent extends Component {
state = {
clickedOutside: false
};
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
myRef = React.createRef();
handleClickOutside = e => {
if (!this.myRef.current.contains(e.target)) {
this.setState({ clickedOutside: true });
}
};
handleClickInside = () => this.setState({ clickedOutside: false });
render() {
return (
<button ref={this.myRef} onClick={this.handleClickInside}>
{this.state.clickedOutside ? "Bye!" : "Hello!"}
</button>
);
}
}
export default SampleComponent;
回答by pie6k
Here is my approach (demo - https://jsfiddle.net/agymay93/4/):
这是我的方法(演示 - https://jsfiddle.net/agymay93/4/):
I've created special component called WatchClickOutside
and it can be used like (I assume JSX
syntax):
我创建了一个特殊的组件WatchClickOutside
,它可以像这样使用(我假设JSX
语法):
<WatchClickOutside onClickOutside={this.handleClose}>
<SomeDropdownEtc>
</WatchClickOutside>
Here is code of WatchClickOutside
component:
这是WatchClickOutside
组件的代码:
import React, { Component } from 'react';
export default class WatchClickOutside extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
componentWillMount() {
document.body.addEventListener('click', this.handleClick);
}
componentWillUnmount() {
// remember to remove all events to avoid memory leaks
document.body.removeEventListener('click', this.handleClick);
}
handleClick(event) {
const {container} = this.refs; // get container that we'll wait to be clicked outside
const {onClickOutside} = this.props; // get click outside callback
const {target} = event; // get direct click event target
// if there is no proper callback - no point of checking
if (typeof onClickOutside !== 'function') {
return;
}
// if target is container - container was not clicked outside
// if container contains clicked target - click was not outside of it
if (target !== container && !container.contains(target)) {
onClickOutside(event); // clicked outside - fire callback
}
}
render() {
return (
<div ref="container">
{this.props.children}
</div>
);
}
}
回答by Richard Herries
This already has many answers but they don't address e.stopPropagation()
and preventing clicking on react links outside of the element you wish to close.
这已经有很多答案,但它们没有解决e.stopPropagation()
和阻止点击您希望关闭的元素之外的反应链接。
Due to the fact that React has it's own artificial event handler you aren't able to use document as the base for event listeners. You need to e.stopPropagation()
before this as React uses document itself. If you use for example document.querySelector('body')
instead. You are able to prevent the click from the React link. Following is an example of how I implement click outside and close.
This uses ES6and React 16.3.
由于 React 拥有自己的人工事件处理程序,因此您无法将文档用作事件侦听器的基础。您需要e.stopPropagation()
在此之前,因为 React 使用文档本身。如果您使用例如document.querySelector('body')
代替。您可以阻止来自 React 链接的点击。以下是我如何实现点击外部和关闭的示例。
这使用ES6和React 16.3。
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
isOpen: false,
};
this.insideContainer = React.createRef();
}
componentWillMount() {
document.querySelector('body').addEventListener("click", this.handleClick, false);
}
componentWillUnmount() {
document.querySelector('body').removeEventListener("click", this.handleClick, false);
}
handleClick(e) {
/* Check that we've clicked outside of the container and that it is open */
if (!this.insideContainer.current.contains(e.target) && this.state.isOpen === true) {
e.preventDefault();
e.stopPropagation();
this.setState({
isOpen: false,
})
}
};
togggleOpenHandler(e) {
e.preventDefault();
this.setState({
isOpen: !this.state.isOpen,
})
}
render(){
return(
<div>
<span ref={this.insideContainer}>
<a href="#open-container" onClick={(e) => this.togggleOpenHandler(e)}>Open me</a>
</span>
<a href="/" onClick({/* clickHandler */})>
Will not trigger a click when inside is open.
</a>
</div>
);
}
}
export default App;