我正在寻找一种方法来检测单击事件是否发生在组件之外,如本文所述。jQueryclosest()用于查看单击事件的目标是否将dom元素作为其父元素之一。如果存在匹配项,则单击事件属于其中一个子项,因此不被视为在组件之外。
因此,在我的组件中,我想将一个单击处理程序附加到窗口。当处理程序启动时,我需要将目标与组件的dom子级进行比较。
click事件包含类似“path”的财产,它似乎保存了事件经过的dom路径。我不知道该比较什么,或者如何最好地遍历它,我想肯定有人已经把它放在了一个聪明的效用函数中。。。不
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>
);
}
});
我有一个需要有条件地将孩子插入模态的例子。像这样,贝娄。
const [view, setView] = useState(VIEWS.SomeView)
return (
<Modal onClose={onClose}>
{VIEWS.Result === view ? (
<Result onDeny={() => setView(VIEWS.Details)} />
) : VIEWS.Details === view ? (
<Details onDeny={() => setView(VIEWS.Result) /> />
) : null}
</Modal>
)
所以parent.contains(event.target)在这里不起作用,因为一旦分离了子级,parent(modal)就不再包含event.targe。
我的解决方案(到目前为止有效,没有任何问题)是这样写:
const listener = (event: MouseEvent) => {
if (parentNodeRef && !event.path.includes(parentNodeRef)) callback()
}
若父级包含已经分离的树中的元素,那个么它不会触发回调。
编辑:event.path是新的,尚未在所有浏览器中退出。请改用composedPath。
这是我解决问题的方法
我从我的自定义钩子返回一个布尔值,当这个值发生变化时(如果单击在我作为参数传递的ref之外,则为true),这样我就可以用useEffect钩子捕捉到这个变化,我希望你能清楚。
下面是一个活生生的例子: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>
);
}
我为所有场合制定了解决方案。
你应该使用一个高阶组件来包装你想要监听的组件。
这个组件示例只有一个属性:“onClickedOutside”,它接收函数。
ClickedOutside.js
import React, { Component } from "react";
export default class ClickedOutside extends Component {
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
handleClickOutside = event => {
// IF exists the Ref of the wrapped component AND his dom children doesnt have the clicked component
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
// A props callback for the ClikedClickedOutside
this.props.onClickedOutside();
}
};
render() {
// In this piece of code I'm trying to get to the first not functional component
// Because it wouldn't work if use a functional component (like <Fade/> from react-reveal)
let firstNotFunctionalComponent = this.props.children;
while (typeof firstNotFunctionalComponent.type === "function") {
firstNotFunctionalComponent = firstNotFunctionalComponent.props.children;
}
// Here I'm cloning the element because I have to pass a new prop, the "reference"
const children = React.cloneElement(firstNotFunctionalComponent, {
ref: node => {
this.wrapperRef = node;
},
// Keeping all the old props with the new element
...firstNotFunctionalComponent.props
});
return <React.Fragment>{children}</React.Fragment>;
}
}