reactjs - React.js Dropdown 组件不会在按钮单击时隐藏
问题描述
我在制作可点击Dropdown
组件时遇到了问题。我的任务是在单击按钮时显示菜单,并在用户单击文档中的任何位置或单击同一个按钮时隐藏菜单,所有组件也应该是功能组件。
我正在使用名为的 3rd 方包classnames
,它有助于有条件地加入CSS
类,还使用 aReact ContextAPI
将道具传递给Dropdown
子组件。
Dropdown
组件依赖于 2 个子组件。
DropdownToggle - (呈现一个可点击的按钮)
DropdownMenu - (呈现带有菜单项的 div)
问题:
每当我打开菜单并单击文档菜单中的任意位置时,它都可以正常工作,但是当我打开菜单并想用按钮隐藏时,它就不起作用了。我认为问题useEffect
出在组件的钩子内部Dropdown
。
演示:
这是App
渲染所有组件的主要组件。
应用程序.js
import React, { Component } from "react";
import Dropdown from "./Dropdown";
import DropdownToggle from "./DropdownToggle";
import DropdownMenu from "./DropdownMenu";
import "./dropdown.css";
// App component
class App extends Component {
state = {
isOpen: false
};
toggle = () => {
alert("Button is clicked");
this.setState({
isOpen: !this.state.isOpen
});
};
render() {
return (
<div className="app">
<Dropdown isOpen={this.state.isOpen} toggle={this.toggle}>
<DropdownToggle>Dropdown</DropdownToggle>
<DropdownMenu>
<div>Item 1</div>
<div>Item 2</div>
</DropdownMenu>
</Dropdown>
</div>
);
}
}
export default App;
主要src代码:
DropdownContext.js
import {createContext} from 'react';
// It is used on child components.
export const DropdownContext = createContext({});
// Wrap Dropdown with this Provider.
export const DropdownProvider = DropdownContext.Provider;
Dropdown.js
import React, { useEffect } from "react";
import classNames from "classnames";
import { DropdownProvider } from "./DropdownContext";
/**
* Returns a new object with the key/value pairs from `obj` that are not in the array `omitKeys`.
* @param obj
* @param omitKeys
*/
const omit = (obj, omitKeys) => {
const result = {};
// Get object properties as an array
const propsArray = Object.keys(obj);
propsArray.forEach(key => {
// Searches the array for the specified item, if the item is not found it returns -1 then
// construct a new object and return it.
if (omitKeys.indexOf(key) === -1) {
result[key] = obj[key];
}
});
return result;
};
// Dropdown component
const Dropdown = props => {
// Populate context value based on the props
const getContextValue = () => {
return {
toggle: props.toggle,
isOpen: props.isOpen
};
};
// toggle function
const toggle = e => {
// Execute toggle function which is came from the parent component
return props.toggle(e);
};
// handle click for the document object
const handleDocumentClick = e => {
// Execute toggle function of the parent
toggle(e);
};
// Remove event listeners
const removeEvents = () => {
["click", "touchstart"].forEach(event =>
document.removeEventListener(event, handleDocumentClick, true)
);
};
// Add event listeners
const addEvents = () => {
["click", "touchstart"].forEach(event =>
document.addEventListener(event, handleDocumentClick, true)
);
};
useEffect(() => {
const handleProps = () => {
if (props.isOpen) {
addEvents();
} else {
removeEvents();
}
};
// mount
handleProps();
// unmount
return () => {
removeEvents();
};
}, [props.isOpen]);
// Condense all other attributes except toggle `prop`.
const { className, isOpen, ...attrs } = omit(props, ["toggle"]);
// Conditionally join all classes
const classes = classNames(className, "dropdown", { show: isOpen });
return (
<DropdownProvider value={getContextValue()}>
<div className={classes} {...attrs} />
</DropdownProvider>
);
};
export default Dropdown;
Dropdown
组件有一个父组件,即Provider
每当Provider
值发生变化时,子组件将访问这些值。其次,在 DOM 上,它将呈现一个div
由Dropdown
标记结构组成的结构。
DropdownToggle.js
import React, {useContext} from 'react';
import classNames from 'classnames';
import {DropdownContext} from './DropdownContext';
// DropdownToggle component
const DropdownToggle = (props) => {
const {toggle} = useContext(DropdownContext);
const onClick = (e) => {
// If props onClick is not undefined
if (props.onClick) {
// execute the function
props.onClick(e);
}
toggle(e);
};
const {className, ...attrs} = props;
const classes = classNames(className);
return (
// All children would be render inside this. e.g. `svg` & `text`
<button type="button" className={classes} onClick={onClick} {...attrs}/>
);
};
export default DropdownToggle;
下拉菜单.js
import React, { useContext } from "react";
import classNames from "classnames";
import { DropdownContext } from "./DropdownContext";
// DropdownMenu component
const DropdownMenu = props => {
const { isOpen } = useContext(DropdownContext);
const { className, ...attrs } = props;
// add show class if isOpen is true
const classes = classNames(className, "dropdown-menu", { show: isOpen });
return (
// All children would be render inside this `div`
<div className={classes} {...attrs} />
);
};
export default DropdownMenu;
解决方案
Jayce444
答案是正确的。当您单击按钮时,它会触发一次,然后事件会冒泡到文档并再次触发。
我只想为您添加另一个替代解决方案。您可以使用useRef
钩子创建Dropdown
节点的引用并检查当前事件目标是否为button
元素。将此代码添加到您的Dropdown.js
文件中。
import React, { useRef } from "react";
const Dropdown = props => {
const containerRef = useRef(null);
// get reference of the current div
const getReferenceDomNode = () => {
return containerRef.current;
};
// handle click for the document object
const handleDocumentClick = e => {
const container = getReferenceDomNode();
if (container.contains(e.target) && container !== e.target) {
return;
}
toggle(e);
};
//....
return (
<DropdownProvider value={getContextValue()}>
<div className={classes} {...attrs} ref={containerRef} />
</DropdownProvider>
);
};
export default Dropdown;
推荐阅读
- python - Pop OS 升级到 21.04 后 python3.8-venv 不再工作
- visual-studio-2013 - 添加 Web 参考 - Visual Studio 2013 中的“基础连接已关闭:发送时发生意外错误”
- android - 如何在 Android 上关闭所有需要的通知?
- c - EXTI中断STM32错误HardFault_Handler
- spotfire - 在表格可视化行上单击并突出显示 Spotfire 操作控制按钮
- puppet - 如何在epp中打印带引号的数组?
- docker - VSCode docker 连接到自托管 GitLab
- scala - 遇到错误时,我可以让 Akka HTTP 背压吗?
- c# - 集成测试 multipart/form-data c#
- sql - 有没有办法在 postgres 中创建具有值的 CTE?