我正在寻找一种方法来检测点击事件是否发生在组件之外,如本 article 中所述。 jQuery最接近()用于查看来自单击事件的目标是否将dom元素作为其父元素之一。如果存在匹配项,则单击事件属于其中一个子项,因此不被视为在组件之外。
所以在我的组件中,我想将一个点击处理程序附加到窗口。当处理程序触发时,我需要将目标与组件的 dom 子项进行比较。
click 事件包含像“path”这样的属性,它似乎保存了事件经过的 dom 路径。我不确定要比较什么或如何最好地遍历它,我想有人一定已经把它放在一个聪明的实用函数中......不是吗?
以下解决方案使用 ES6 并遵循最佳实践来绑定以及通过方法设置 ref。
要查看它的实际效果:
挂钩实现
React 16.3 之后的类实现
React 16.3 之前的类实现
挂钩实现:
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>;
}
类实现:
16.3之后
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.wrapperRef = React.createRef();
this.handleClickOutside = this.handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
/**
* Alert if clicked on outside of element
*/
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {
alert("You clicked outside of me!");
}
}
render() {
return <div ref={this.wrapperRef}>{this.props.children}</div>;
}
}
16.3 之前
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>;
}
}
2021 年更新:
自从我添加此响应以来已经有一段时间了,而且由于它似乎仍然引起了一些兴趣,我想我会将它更新到更新的 React 版本。在 2021 年,这就是我编写此组件的方式:
import React, { useState } from "react";
import "./DropDown.css";
export function DropDown({ options, callback }) {
const [selected, setSelected] = useState("");
const [expanded, setExpanded] = useState(false);
function expand() {
setExpanded(true);
}
function close() {
setExpanded(false);
}
function select(event) {
const value = event.target.textContent;
callback(value);
close();
setSelected(value);
}
return (
<div className="dropdown" tabIndex={0} onFocus={expand} onBlur={close} >
<div>{selected}</div>
{expanded ? (
<div className={"dropdown-options-list"}>
{options.map((O) => (
<div className={"dropdown-option"} onClick={select}>
{O}
</div>
))}
</div>
) : null}
</div>
);
}
原始答案(2016):
这是最适合我的解决方案,无需将事件附加到容器:
某些 HTML 元素可以具有所谓的“焦点”,例如输入元素。当这些元素失去焦点时,它们也会响应模糊事件。
要使任何元素具有焦点的能力,只需确保其 tabindex 属性设置为 -1 以外的任何值。在常规 HTML 中,可以通过设置 tabindex
属性,但在 React 中您必须使用 tabIndex
(注意大写 I
)。
您也可以通过 JavaScript 使用 element.setAttribute('tabindex',0)
这就是我用来制作自定义下拉菜单的目的。
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>
);
}
});
display:absolute
,因此下拉内容不会影响父 div 的大小。这意味着当我单击下拉列表中的项目时,会触发 onblur。
onBlur
将在您的下拉容器上触发,并且 expanded
将设置为 false。
我被困在同一个问题上。我在这里参加聚会有点晚了,但对我来说,这是一个非常好的解决方案。希望它对其他人有帮助。您需要从 react-dom
导入 findDOMNode
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 方法 (16.8 +)
您可以创建一个名为 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 };
}
然后在您希望添加功能的组件中执行以下操作:
const DropDown = () => {
const { ref, isComponentVisible } = useComponentVisible(true);
return (
<div ref={ref}>
{isComponentVisible && (<p>Dropdown Component</p>)}
</div>
);
}
在此处查找 codesandbox 示例。
在这里尝试了很多方法后,我决定使用github.com/Pomax/react-onclickoutside,因为它有多完整。
我通过 npm 安装了模块并将其导入到我的组件中:
import onClickOutside from 'react-onclickoutside'
然后,在我的组件类中,我定义了 handleClickOutside
方法:
handleClickOutside = () => {
console.log('onClickOutside() method called')
}
在导出我的组件时,我将它包装在 onClickOutside()
中:
export default onClickOutside(NameOfComponent)
而已。
tabIndex
/onBlur
approach proposed by Pablo 相比,这有什么具体优势吗?实现如何工作,其行为与 onBlur
方法有何不同?
tabIndex/onBlur
方法发表了评论。当下拉菜单为 position:absolute
时,它不起作用,例如悬停在其他内容上的菜单。
基于 Tanner Linsley 优秀的 talk at JSConf Hawaii 2020 的 Hook 实现:
使用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)
}
这是一个工作示例:
/* 自定义 Hook */ function useOuterClick(callback) { const innerRef = useRef();常量 callbackRef = useRef(); // 在 ref 中设置当前回调,在第二个 useEffect 使用它之前 useEffect(() => { // useEffect 包装器对并发模式是安全的 callbackRef.current = callback; }); useEffect(() => { document.addEventListener("click", handleClick); return () => document.removeEventListener("click", handleClick); // 从 refs 函数中读取最近的回调和 innerRef dom 节点函数 handleClick(e ) { if ( innerRef.current && callbackRef.current && !innerRef.current.contains(e.target) ) { callbackRef.current(e); } } }, []); // 不需要回调 + innerRef dep return innerRef; //返回参考;客户端可以省略`useRef` } /* 用法 */ const Client = () => { const [counter, setCounter] = useState(0); const innerRef = useOuterClick(e => { // 计数器状态是最新的,当处理程序被调用时 alert(`Clicked outside! Increment counter to ${counter + 1}`); setCounter(c => c + 1 ); }); return (
点击外部!
关键点
useOuterClick 利用可变引用提供精益客户端 API
包含组件生命周期的稳定点击监听器([] deps)
客户端可以通过 useCallback 设置回调而不需要记忆它
回调主体可以访问最新的道具和状态 - 没有陈旧的闭包值
(iOS 的附注)
iOS 通常只将某些元素视为可点击的。要使外部点击起作用,请选择与 document
不同的点击侦听器 - 不包括 body
在内的任何其他点击侦听器。例如,在 React 根 div
上添加一个侦听器并扩展其高度,如 height: 100vh
,以捕获所有外部点击。来源:quirksmode.org
<div onClick={() => {}}> ... </div>
,以便 IOS 注册来自外部的所有点击。它适用于 Iphone 6+。这能解决你的问题吗?
@media (hover: none) and (pointer: coarse) { body { cursor:pointer } }
在我的全局样式中添加这个,似乎解决了这个问题。
@supports (-webkit-overflow-scrolling: touch)
,它只针对 IOS,尽管我不知道更多!我刚刚测试过它并且它有效。
[更新] React ^16.8 使用 Hooks 的解决方案
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;
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;
.contains is not a function
,可能是因为您将 ref prop 传递给自定义组件,而不是像 <div>
这样的真实 DOM 元素
ref
属性传递给自定义组件的人,您可能需要查看 React.forwardRef
.contains
错误(在本例中为 'contains' does not exist on type 'never'
),例如:const ref = useRef<HTMLDivElement>(null)
。
这里没有其他答案对我有用。我试图隐藏模糊的弹出窗口,但由于内容是绝对定位的,即使点击内部内容,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.
}
},
希望这可以帮助。
document
的那些方法相比,我更喜欢这个。谢谢。
感谢 Ben Alpert 在 discuss.reactjs.org 上的帮助,我找到了解决方案。建议的方法将处理程序附加到文档,但结果证明这是有问题的。单击我的树中的一个组件会导致重新渲染,它会在更新时删除单击的元素。因为 React 的重新渲染发生在在文档正文处理程序被调用之前,所以该元素未被检测到树的“内部”。
解决方案是在应用程序根元素上添加处理程序。
主要的:
window.__myapp_container = document.getElementById('app')
React.render(<App/>, window.__myapp_container)
零件:
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>
)
}
}
document
上的处理程序不是,为什么要保证应用程序根 DOM 节点上的处理程序在另一个处理程序触发重新呈现之前触发?
mousedown
可能比这里的 click
更好。在大多数应用程序中,单击外部关闭菜单的行为发生在您按下鼠标的那一刻,而不是在您释放时。例如,尝试使用 Stack Overflow 的 flag 或 share 对话框或浏览器顶部菜单栏中的下拉菜单之一。
document
以兹方式...
const componentRef = useRef();
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
function handleClick(e: any) {
if(componentRef && componentRef.current){
const ref: any = componentRef.current
if(!ref.contains(e.target)){
// put your action here
}
}
}
}, []);
然后将 ref 放在您的组件上
<div ref={componentRef as any}> My Component </div>
MUI 有一个小组件可以解决这个问题:https://mui.com/base/react-click-away-listener/,您可以挑选它。压缩后的重量低于 1 kB,它支持移动设备、IE 11 和门户网站。
或者:
const onClickOutsideListener = () => {
alert("click outside")
document.removeEventListener("click", onClickOutsideListener)
}
...
return (
<div
onMouseLeave={() => {
document.addEventListener("click", onClickOutsideListener)
}}
>
...
</div>
对于那些需要绝对定位的人,我选择的一个简单选项是添加一个包装器组件,该组件的样式可以用透明背景覆盖整个页面。然后你可以在这个元素上添加一个 onClick 来关闭你的内部组件。
<div style={{
position: 'fixed',
top: '0', right: '0', bottom: '0', left: '0',
zIndex: '1000',
}} onClick={() => handleOutsideClick()} >
<Content style={{position: 'absolute'}}/>
</div>
就像现在一样,如果您在内容上添加点击处理程序,该事件也将传播到上层 div 并因此触发 handlerOutsideClick。如果这不是您想要的行为,只需停止处理程序上的事件传播。
<Content style={{position: 'absolute'}} onClick={e => {
e.stopPropagation();
desiredFunctionCall();
}}/>
`
这是我的方法(演示 - https://jsfiddle.net/agymay93/4/):
我创建了名为 WatchClickOutside
的特殊组件,它可以像这样使用(我假设 JSX
语法):
<WatchClickOutside onClickOutside={this.handleClose}>
<SomeDropdownEtc>
</WatchClickOutside>
这是 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>
);
}
}
这已经有很多答案,但它们没有解决 e.stopPropagation()
并阻止点击您希望关闭的元素之外的反应链接。
由于 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;
import { useClickAway } from "react-use";
useClickAway(ref, () => console.log('OUTSIDE CLICKED'));
我部分通过遵循 this 并遵循 React 官方文档处理需要反应 ^16.3 的 refs 来做到这一点。在尝试了这里的其他一些建议之后,这是唯一对我有用的东西......
class App extends Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
componentWillMount() {
document.addEventListener("mousedown", this.handleClick, false);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClick, false);
}
handleClick = e => {
/*Validating click is made inside a component*/
if ( this.inputRef.current === e.target ) {
return;
}
this.handleclickOutside();
};
handleClickOutside(){
/*code to handle what to do when clicked outside*/
}
render(){
return(
<div>
<span ref={this.inputRef} />
</div>
)
}
}
要扩展 Ben Bud 所接受的答案,如果您使用的是样式化组件,那么以这种方式传递 ref 会给您一个错误,例如“this.wrapperRef.contains is not a function”。
建议的修复,在评论中,用 div 包装样式化的组件并将 ref 传递到那里,工作。话虽如此,在他们的 docs 中,他们已经解释了这样做的原因以及在 styled-components 中正确使用 refs:
将 ref 属性传递给样式化组件将为您提供 StyledComponent 包装器的实例,但不会传递给底层 DOM 节点。这是由于 refs 的工作方式。不能直接在我们的包装器上调用 DOM 方法,例如焦点。要获得对实际包装的 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 }}
/>
)
}
所以我遇到了类似的问题,但在我的情况下,这里选择的答案不起作用,因为我有一个下拉按钮,它是文档的一部分。所以点击按钮也触发了handleClickOutside
功能。为了阻止它触发,我必须在按钮中添加一个新的 ref
,并将这个 !menuBtnRef.current.contains(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>
)
}
这是我解决问题的方法
我从我的自定义钩子返回一个布尔值,当这个值改变时(如果点击在我作为 arg 传递的 ref 之外,则为真),这样我可以用 useEffect 钩子捕捉这个变化,我希望它很清楚你。
这是一个实时示例:Live Example on 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>
);
}
我使用了this module(我与作者没有关联)
npm install react-onclickout --save
const ClickOutHandler = require('react-onclickout');类 ExampleComponent 扩展 React.Component { onClickOut(e) { if (hasClass(e.target, 'ignore-me')) return; alert('用户在组件外点击!'); } render() { return (
它很好地完成了这项工作。
"Support for the experimental syntax 'classProperties' isn't currently enabled"
,这只是开箱即用。对于尝试此解决方案的任何人,请记住删除您在尝试其他解决方案时可能复制的任何挥之不去的代码。例如,添加事件监听器似乎与此冲突。
带钩子的打字稿
注意:我使用的是 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>
);
}
event.target as Node
部分没有其他方法了吗?
只需使用来自 mui (material-ui) 的 ClickAwayListener:
<ClickAwayListener onClickAway={handleClickAway}>
{children}
<ClickAwayListener >
有关更多信息,您可以查看:https://mui.com/base/react-click-away-listener/
我对所有其他答案的最大担忧是必须从根/父级向下过滤点击事件。我发现最简单的方法是简单地设置一个同级元素,位置:固定,下拉列表后面的 z-index 1 并处理同一组件内固定元素上的单击事件。将所有内容集中到给定组件。
示例代码
#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;
}
}
componentWillMount(){
document.addEventListener('mousedown', this.handleClickOutside)
}
handleClickOutside(event) {
if(event.path[0].id !== 'your-button'){
this.setState({showWhatever: false})
}
}
事件 path[0]
是最后点击的项目
UseOnClickOutside Hook - React 16.8 +
创建一个通用的OnOutsideClick函数
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>
);
};
部分灵感来自@pau1fitzgerald 的回答。
在我的 DROPDOWN 案例中,Ben Bud's solution 运行良好,但我有一个带有 onClick 处理程序的单独切换按钮。所以外部点击逻辑与按钮 onClick 切换器冲突。以下是我通过传递按钮的 ref 来解决它的方法:
import React, { useRef, useEffect, useState } from "react";
/**
* Hook that triggers onClose when clicked outside of ref and buttonRef elements
*/
function useOutsideClicker(ref, buttonRef, onOutsideClick) {
useEffect(() => {
function handleClickOutside(event) {
/* clicked on the element itself */
if (ref.current && !ref.current.contains(event.target)) {
return;
}
/* clicked on the toggle button */
if (buttonRef.current && !buttonRef.current.contains(event.target)) {
return;
}
/* If it's something else, trigger onClose */
onOutsideClick();
}
// 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 DropdownMenu(props) {
const wrapperRef = useRef(null);
const buttonRef = useRef(null);
const [dropdownVisible, setDropdownVisible] = useState(false);
useOutsideClicker(wrapperRef, buttonRef, closeDropdown);
const toggleDropdown = () => setDropdownVisible(visible => !visible);
const closeDropdown = () => setDropdownVisible(false);
return (
<div>
<button onClick={toggleDropdown} ref={buttonRef}>Dropdown Toggler</button>
{dropdownVisible && <div ref={wrapperRef}>{props.children}</div>}
</div>
);
}
Typescript + @ford04 提案的简化版:
使用OuterClick API
const Client = () => {
const ref = useOuterClick<HTMLDivElement>(e => { /* Custom-event-handler */ });
return <div ref={ref}> Inside </div>
};
执行
export default function useOuterClick<T extends HTMLElement>(callback: Function) {
const callbackRef = useRef<Function>(); // initialize mutable ref, which stores callback
const innerRef = useRef<T>(null); // 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", _onClick);
return () => document.removeEventListener("click", _onClick);
function _onClick(e: any): void {
const clickedOutside = !(innerRef.current?.contains(e.target));
if (clickedOutside)
callbackRef.current?.(e);
}
}, []); // no dependencies -> stable click listener
return innerRef; // convenience for client (doesn't need to init ref himself)
}
用打字稿
函数 Tooltip(): ReactElement { const [show, setShow] = useState(false); const ref = useRef
策略示例
我喜欢提供的解决方案,它们通过在组件周围创建包装器来做同样的事情。
由于这更像是一种行为,因此我想到了策略并提出了以下建议。
我是 React 新手,我需要一些帮助才能在用例中保存一些样板文件
请查看并告诉我您的想法。
点击外部行为
import ReactDOM from 'react-dom';
export default class ClickOutsideBehavior {
constructor({component, appContainer, onClickOutside}) {
// Can I extend the passed component's lifecycle events from here?
this.component = component;
this.appContainer = appContainer;
this.onClickOutside = onClickOutside;
}
enable() {
this.appContainer.addEventListener('click', this.handleDocumentClick);
}
disable() {
this.appContainer.removeEventListener('click', this.handleDocumentClick);
}
handleDocumentClick = (event) => {
const area = ReactDOM.findDOMNode(this.component);
if (!area.contains(event.target)) {
this.onClickOutside(event)
}
}
}
示例使用
import React, {Component} from 'react';
import {APP_CONTAINER} from '../const';
import ClickOutsideBehavior from '../ClickOutsideBehavior';
export default class AddCardControl extends Component {
constructor() {
super();
this.state = {
toggledOn: false,
text: ''
};
this.clickOutsideStrategy = new ClickOutsideBehavior({
component: this,
appContainer: APP_CONTAINER,
onClickOutside: () => this.toggleState(false)
});
}
componentDidMount () {
this.setState({toggledOn: !!this.props.toggledOn});
this.clickOutsideStrategy.enable();
}
componentWillUnmount () {
this.clickOutsideStrategy.disable();
}
toggleState(isOn) {
this.setState({toggledOn: isOn});
}
render() {...}
}
笔记
我想存储传递的 component
生命周期钩子并使用与此类似的方法覆盖它们:
const baseDidMount = component.componentDidMount;
component.componentDidMount = () => {
this.enable();
baseDidMount.call(component)
}
component
是传递给 ClickOutsideBehavior
的构造函数的组件。
这将删除此行为的用户的启用/禁用样板,但它看起来不太好
我知道这是一个老问题,但我一直遇到这个问题,我在以简单的格式解决这个问题时遇到了很多麻烦。因此,如果这会让任何人的生活更轻松,请使用 airbnb 的 OutsideClickHandler。这是一个无需编写自己的代码即可完成此任务的最简单的插件。
例子:
hideresults(){
this.setState({show:false})
}
render(){
return(
<div><div onClick={() => this.setState({show:true})}>SHOW</div> {(this.state.show)? <OutsideClickHandler onOutsideClick={() =>
{this.hideresults()}} > <div className="insideclick"></div> </OutsideClickHandler> :null}</div>
)
}
不定期副业成功案例分享
document.addEventListener
?context
只有在使用时才需要作为构造函数中的参数。在这种情况下没有使用它。 react 团队建议在构造函数中使用props
作为参数的原因是因为在调用super(props)
之前使用this.props
将是未定义的,并且可能导致错误。context
在内部节点上仍然可用,这就是context
的全部目的。这样你就不必像使用 props 那样将它从一个组件传递到另一个组件。 2. 这只是一种风格偏好,在这种情况下不值得争论。display: absolute
的弹出窗口 - 当您点击任何地方(包括内部)时,它总是会触发handleClickOutside
。最终切换到 react-outside-click-handler,这似乎以某种方式涵盖了这种情况