我正在寻找一种方法来检测单击事件是否发生在组件之外,如本文所述。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>
);
}
});
所有提出的解决方案都假设可以将事件添加到文档中,并依赖于本机方法.contains()来区分事件是在当前组件内部还是外部触发的
ref.current.contains(event.target)
但这在React中并不总是有效的。事实上,在React中,React.createPortal API允许从一个组件中指定一个新的真实父组件,JSX在该组件中被呈现,但同时,事件冒泡被模拟为组件在声明的位置呈现(即React.cCreatePortal被调用的位置)。
这是通过将所有事件附加到应用程序根元素并在Javascript中模拟事件来实现的。
因此,在这种情况下,所提出的解决方案被打破了,因为在门户元素内部的单击,对于标准HTML来说,在当前元素之外,实际上应该像在内部一样进行处理。
所以我重写了这个问题的一个注释中提出的解决方案,并对其进行了重构以使用功能组件。这也适用于一个或多个嵌套门户。
export default function OutsideClickDetector({onOutsideClick, Component ='div', ...props} : OutsideClickDetectorProps) {
const isClickInside = useRef<boolean>(false);
const onMouseDown = () => {
isClickInside.current = true;
};
const handleBodyClick = useCallback((e) => {
if(!isClickInside.current) {
onOutsideClick?.(e);
}
isClickInside.current = false;
}, [isClickInside, onOutsideClick]);
useEffect(() => {
document.addEventListener('mousedown', handleBodyClick);
return () => document.removeEventListener('mousedown', handleBodyClick);
});
return <Component onMouseDown={onMouseDown} onMouseUp={() => isClickInside.current = false}{...props} />;
}
这是我的方法(演示-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>
);
}
}
@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)
}
所以我遇到了一个类似的问题,但在我的案例中,这里选择的答案不起作用,因为我有一个下拉菜单按钮,这是文档的一部分。因此,单击该按钮也会触发handleClickOutside函数。为了防止触发,我必须向按钮和这个添加一个新的引用!menuBtnRef.current.contents(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>
)
}