我正在寻找一种方法来检测单击事件是否发生在组件之外,如本文所述。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>
);
}
});
将onClick处理程序添加到顶级容器中,并在用户单击时增加状态值。将该值传递给相关组件,每当该值发生变化时,您就可以执行操作。
在本例中,每当clickCount值更改时,我们都会调用this.closeDropdown()。
incrementClickCount方法在.app容器中激发,而不是在下拉列表中激发,因为我们使用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>
);
}
}
在这里尝试了许多方法之后,我决定使用github.com/Pomax/react-onclickoutside,因为它非常完整。
我通过npm安装了模块并将其导入到组件中:
import onClickOutside from 'react-onclickoutside'
然后,在组件类中,我定义了handleClickOutside方法:
handleClickOutside = () => {
console.log('onClickOutside() method called')
}
导出组件时,我将其包装在onClickOutside()中:
export default onClickOutside(NameOfComponent)
就是这样。
非侵入性方式无需添加另一个DIV EL。
注意:React可能会说findDomNode已弃用,但到目前为止,我还没有遇到任何问题
@异常:单击要忽略的类
@idException:单击时忽略的id
import React from "react"
import ReactDOM from "react-dom"
type Func1<T1, R> = (a1: T1) => R
export function closest(
el: Element,
fn: (el: Element) => boolean
): Element | undefined {
let el_: Element | null = el;
while (el_) {
if (fn(el_)) {
return el_;
}
el_ = el_.parentElement;
}
}
let instances: ClickOutside[] = []
type Props = {
idException?: string,
exceptions?: (string | Func1<MouseEvent, boolean>)[]
handleClickOutside: Func1<MouseEvent, void>
}
export default class ClickOutside extends React.Component<Props> {
static defaultProps = {
exceptions: []
};
componentDidMount() {
if (instances.length === 0) {
document.addEventListener("mousedown", this.handleAll, true)
window.parent.document.addEventListener(
"mousedown",
this.handleAll,
true
)
}
instances.push(this)
}
componentWillUnmount() {
instances.splice(instances.indexOf(this), 1)
if (instances.length === 0) {
document.removeEventListener("mousedown", this.handleAll, true)
window.parent.document.removeEventListener(
"mousedown",
this.handleAll,
true
)
}
}
handleAll = (e: MouseEvent) => {
const target: HTMLElement = e.target as HTMLElement
if (!target) return
instances.forEach(instance => {
const { exceptions, handleClickOutside: onClickOutside, idException } = instance.props as Required<Props>
let exceptionsCount = 0
if (exceptions.length > 0) {
const { functionExceptions, stringExceptions } = exceptions.reduce(
(acc, exception) => {
switch (typeof exception) {
case "function":
acc.functionExceptions.push(exception)
break
case "string":
acc.stringExceptions.push(exception)
break
}
return acc
},
{ functionExceptions: [] as Func1<MouseEvent, boolean>[], stringExceptions: [] as string[] }
)
if (functionExceptions.length > 0) {
exceptionsCount += functionExceptions.filter(
exception => exception(e) === true
).length
}
if (exceptionsCount === 0 && stringExceptions.length > 0) {
const el = closest(target, (e) => stringExceptions.some(ex => e.classList.contains(ex)))
if (el) {
exceptionsCount++
}
}
}
if (idException) {
const target = e.target as HTMLDivElement
if (document.getElementById(idException)!.contains(target)) {
exceptionsCount++
}
}
if (exceptionsCount === 0) {
// eslint-disable-next-line react/no-find-dom-node
const node = ReactDOM.findDOMNode(instance)
if (node && !node.contains(target)) {
onClickOutside(e)
}
}
})
};
render() {
return React.Children.only(this.props.children)
}
}
用法
<ClickOutside {...{ handleClickOutside: () => { alert('Clicked Outside') } }}>
<div >
<div>Breathe</div>
</div>
</ClickOutside>