多级下拉菜单是网页设计的一个主要部分。由于能够提供多种选择,它们使导航栏变得动态和有组织。
对于任何在React或任何基于React的项目(如Gatsby或Next.js)中工作的开发者,本教程涵盖了如何在React项目中实现下拉功能的逐步过程。在本指南的最后,我们将拥有下面的菜单。
要学习本教程,请确保你对React有基本了解,并确认你的电脑上安装了Node.js。然后,我们就可以开始了。
设置React项目
让我们首先通过运行以下命令创建一个新的React项目,名为react-multilevel-dropdown-menu 。
npx create-react-app react-multilevel-dropdown-menu
一旦项目生成,使用cd react-multilevel-dropdown-menu ,导航到项目文件夹内,或者直接用代码编辑器打开项目。
然后,运行npm start 内置命令,在开发模式下启动项目。应用程序应该在浏览器中启动,地址是http://localhost:3000。
React项目结构
像每个React项目一样,我们将把我们的项目UI设计分解成独立的和可重用的组件,如下所述。
父组件,App ,持有标志和Navbar 组件,Navbar 持有MenuItems 组件。在MenuItems ,我们有各个项目和Dropdown 组件。Dropdown 中也有MenuItems ,其中也可能有Dropdowns。
从这个细分中,我们将创建四个不同的组件。
创建项目文件
前往src 文件夹,删除所有的文件,除了index.js 。接下来,在src 内创建一个名为components 的文件夹,并添加以下组件文件:App.js,Dropdown.js,MenuItems.js 和Navbar.js 。
在App.js 文件中,添加以下起始代码。
const App = () => {
return (
<header>
<div className="nav-area">
navbar content
</div>
</header>
);
};
export default App;
保存该文件。现在将src/index.js 文件的内容替换为以下内容。
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
// styles
import "./app.css";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
我们已经导入了一个CSS文件来为我们的项目添加 "外观和感觉"。因此,让我们在src 文件夹中创建一个app.css 文件。然后,从multilevel-dropdown-menu项目中复制样式并将其添加到app.css 文件中。
保存所有的文件,然后在浏览器中看到App 组件的内容被渲染出来。
渲染顶层菜单项
让我们从渲染顶级菜单项开始构建。要做到这一点,我们必须通过在src 文件夹中创建一个menuItems.js 文件并添加以下内容来获得菜单数据。
export const menuItems = [
{
title: "Home"
},
{
title: "Services"
},
{
title: "About"
}
];
现在,保存该文件。
如果我们回顾一下项目设计,App.js 文件中保存着标志和渲染菜单项的Navbar 组件。有了这个,让我们更新一下App.js 文件,这样我们就有了以下内容。
import Navbar from "./Navbar";
const App = () => {
return (
<header>
<div className="nav-area">
<a href="/#" className="logo">
Logo
</a>
<Navbar />
</div>
</header>
);
};
export default App;
在代码中,为了简单起见,我们使用<a> 标签作为内部链接。通常情况下,我们会使用Link 或NavLink (用于菜单导航)从react-router-dom ,在内部页面之间进行链接。
继续,注意我们导入了Navbar 组件。因此,前往components/Navbar.js ,添加以下代码。
const Navbar = () => {
return (
<nav>
<ul className="menus">
Nav Items here
</ul>
</nav>
);
};
export default Navbar;
接下来,导入menuItems 数据,循环浏览,然后在JSX中渲染它们。
import { menuItems } from "../menuItems";
const Navbar = () => {
return (
<nav>
<ul className="menus">
{menuItems.map((menu, index) => {
return (
<li className="menu-items" key={index}>
<a href="/#">{menu.title}</a>
</li>
);
})}
</ul>
</nav>
);
};
export default Navbar;
保存该文件并查看前台。它应该看起来像这样。
这是一个基本的导航菜单。让我们更进一步,接下来显示一个单层下拉菜单。
渲染一个单级下拉菜单
让我们前往menuItems.js 文件,修改数据以包括一个submenu ,像这样。
export const menuItems = [
//...
{
title: "Services",
submenu: [
{
title: "web design"
},
{
title: "web development",
},
{
title: "SEO"
}
]
},
//...
];
在这里,我们在Services ,因为我们想让它成为一个下拉菜单,所以添加了一个submenu 。让我们保存该文件。
此刻,Navbar 正在渲染我们代码中的菜单项。如果我们再看一下设计,MenuItems ,它是Navbar 的直接子节点,承担着显示这些项目的责任。
所以,修改Navbar ,这样我们就有了下面的内容。
import { menuItems } from "../menuItems";
import MenuItems from "./MenuItems";
const Navbar = () => {
return (
<nav>
<ul className="menus">
{menuItems.map((menu, index) => {
return <MenuItems items={menu} key={index} />;
})}
</ul>
</nav>
);
};
export default Navbar;
在代码中,我们通过items 的道具将菜单项数据传递给MenuItems 组件。这是一个叫做道具钻取的过程,是一个基本的React原则。
在MenuItems 组件中,我们将接收项目道具并显示菜单项目。我们还将检查这些项目是否有submenu ,然后显示一个下拉菜单。因此,打开components/MenuItems.js 文件并添加以下代码。
import Dropdown from "./Dropdown";
const MenuItems = ({ items }) => {
return (
<li className="menu-items">
{items.submenu ? (
<>
<button type="button" aria-haspopup="menu">
{items.title}{" "}
</button>
<Dropdown submenus={items.submenu} />
</>
) : (
<a href="/#">{items.title}</a>
)}
</li>
);
};
export default MenuItems;
在代码中,我们使用button 元素来打开下拉菜单。如果我们使用一个链接标签来代替,如果我们想要屏幕阅读器等辅助技术,我们必须添加一个role="button" 。
同样在代码中,我们导入了Dropdown 组件并通过道具传递了submenu 项目。
现在让我们打开components/Dropdown.js 文件并访问道具,这样我们就可以像这样渲染submenu 。
const Dropdown = ({ submenus }) => {
return (
<ul className="dropdown">
{submenus.map((submenu, index) => (
<li key={index} className="menu-items">
<a href="/#">{submenu.title}</a>
</li>
))}
</ul>
);
};
export default Dropdown;
记住要保存所有的文件。
要看到下拉菜单,请打开src/app.css 文件并暂时注释掉CSS的display: none; 部分。
.dropdown {
...
/* display: none; */
}
我们给下拉菜单添加了一个display: none; ,默认情况下隐藏它,只有当我们与菜单交互时才打开它。
一旦我们删除了display: none; ,菜单应该看起来像这样。
很好。我们正在取得进展!
切换下拉菜单
现在让我们定义一下检测下拉菜单项被点击时的逻辑,这样我们就可以动态地显示或隐藏下拉框。要做到这一点,我们必须添加一个状态并在点击下拉菜单时更新它。
在components/MenuItems.js 文件中,让我们更新代码以包括状态。
import { useState } from "react";
// ...
const MenuItems = ({ items }) => {
const [dropdown, setDropdown] = useState(false);
return (
<li className="menu-items">
{items.submenu ? (
<>
<button
// ...
aria-expanded={dropdown ? "true" : "false"}
onClick={() => setDropdown((prev) => !prev)}
>
{items.title}{" "}
</button>
<Dropdown
// ...
dropdown={dropdown}
/>
</>
) : (
// ...
)}
</li>
);
};
在代码中,我们定义了一个名为dropdown 的状态变量,默认值为false ,还有一个setDropdown 更新器,用于在点击下拉按钮时切换状态,正如在onClick 事件中看到的那样。
这使我们能够动态地给aria-expanded 属性加值,以指示下拉框是展开还是折叠,这对屏幕阅读器是有益的。我们还把dropdown 变量作为一个道具传给了Dropdown 组件,这样我们就可以处理下拉框的切换。
让我们打开components/Dropdown.js ,访问dropdown 的道具,并使用它在点击下拉菜单时动态地添加一个类名。
const Dropdown = ({ submenus, dropdown }) => {
return (
<ul className={`dropdown ${dropdown ? "show" : ""}`}>
{/* ... */}
</ul>
);
};
export default Dropdown;
当下拉菜单被激活时,show 的类名被添加。而且,我们已经添加了一个样式display: block; ,在我们的CSS中显示下拉菜单。
现在,我们可以将display: none; 返回到.dropdown 类选择器中的src/app.css 。
.dropdown {
...
display: none;
}
让我们保存我们的文件。我们现在应该能够切换我们的菜单下拉。
多级下拉菜单
像单级下拉菜单一样,为了添加一个多级下拉菜单,让我们打开menuItems.js 文件并修改数据以包括多级submenus,像这样。
export const menuItems = [
// ...
{
title: "web development",
submenu: [
{
title: "Frontend",
},
{
title: "Backend",
submenu: [
{
title: "NodeJS",
},
{
title: "PHP",
},
],
},
],
},
// ...
];
在网页开发选项中添加一个submenu ,在后台选项中添加另一个submenu ,然后保存该文件。
渲染一个多级下拉菜单
正如在设计中看到的,一个下拉菜单也可以有菜单项,另一个下拉菜单,等等。为此,在Dropdown 组件中,我们必须将菜单项的渲染工作委托给MenuItems 组件。
打开components/Dropdown.js 文件,导入MenuItems ,并通过items 道具传递给submenu 。
import MenuItems from "./MenuItems";
const Dropdown = ({ submenus, dropdown }) => {
return (
<ul className={`dropdown ${dropdown ? "show" : ""}`}>
{submenus.map((submenu, index) => (
<MenuItems items={submenu} key={index} />
))}
</ul>
);
};
export default Dropdown;
如果我们保存文件并测试下拉菜单,它可以工作,但我们会注意到下拉菜单互相重叠了。
通过点击网页开发子菜单,我们希望将其下拉菜单逻辑地定位到右边。我们可以通过检测下拉深度级别来实现这一点。
检测菜单深度级别
知道了菜单的深度级别,我们就可以做几件事。首先,我们可以动态地添加不同的箭头来显示一个下拉菜单的存在。其次,我们可以用它来检测 "第二层及以上 "的下拉菜单,从而在逻辑上将它们定位到子菜单的右边。
打开components/Navbar.js 文件,在return 语句上方添加以下内容。
const depthLevel = 0;
另外,让我们确保我们通过一个道具将值传递给MenuItems 。我们的代码现在看起来像这样。
// ...
return (
// ...
{menuItems.map((menu, index) => {
const depthLevel = 0;
return <MenuItems items={menu} key={index} depthLevel={depthLevel} />;
})}
// ...
);
// ...
接下来,在MenuItems 组件中,我们访问depthLevel ,用它来显示下拉箭头。
const MenuItems = ({ items, depthLevel }) => {
// ...
return (
<li className="menu-items">
{items.submenu ? (
<>
<button
// ...
>
{items.title}{" "}
{depthLevel > 0 ? <span>»</span> : <span className="arrow" />}
</button>
<Dropdown
depthLevel={depthLevel}
// ...
/>
对于depthLevel 大于0 ,我们使用一个HTML实体名称» ,显示一个右箭头,否则我们添加一个.arrow 的类名,来样式一个自定义的下拉箭头。在我们的样式表中,我们添加了向下箭头的样式。
还请注意,我们通过道具将depthLevel 传递给Dropdown ;在那里我们将为下拉菜单增加它。
在components/Dropdown.js 文件中,访问depthLevel prop,将其递增,并检查该值是否大于1 ,这样我们就可以为下拉菜单添加一个自定义类。我们现在在我们的样式表中对该类进行了样式设计。
同时,确保我们将depthLevel 作为一个道具传递给MenuItems 。
const Dropdown = ({ submenus, dropdown, depthLevel }) => {
depthLevel = depthLevel + 1;
const dropdownClass = depthLevel > 1 ? "dropdown-submenu" : "";
return (
<ul className={`dropdown ${dropdownClass} ${dropdown ? "show" : ""}`}>
{submenus.map((submenu, index) => (
<MenuItems
// ...
depthLevel={depthLevel}
/>
))}
</ul>
);
};
export default Dropdown;
让我们保存文件并测试该项目。
现在我们可以切换菜单了,我们需要一种方法来在我们点击下拉菜单之外的地方关闭下拉菜单。
当用户在下拉菜单外点击时关闭下拉菜单
通过点击下拉菜单之外,我们要关闭它。我们可以通过将dropdown 状态设置为默认值false 来做到这一点。我们将定义一个逻辑来检测在下拉菜单外的点击。
让我们打开components/MenuItems.js 文件,并更新import ,像这样包括useEffect 和useRef Hook。
import { useState, useEffect, useRef } from "react";
接下来,我们将使用useRef ,通过向目标节点传递一个引用对象来访问下拉的DOM元素。
const MenuItems = ({ items, depthLevel }) => {
// ...
let ref = useRef();
return (
<li className="menu-items" ref={ref}>
{/* ... */}
</li>
);
};
export default MenuItems;
然后,在return 语句上方添加以下代码。
useEffect(() => {
const handler = (event) => {
if (dropdown && ref.current && !ref.current.contains(event.target)) {
setDropdown(false);
}
};
document.addEventListener("mousedown", handler);
document.addEventListener("touchstart", handler);
return () => {
// Cleanup the event listener
document.removeEventListener("mousedown", handler);
document.removeEventListener("touchstart", handler);
};
}, [dropdown]);
让我们保存我们的文件并测试我们的项目。它成功了!
在useEffect Hook中,我们检查一个下拉菜单是否打开,然后检查被点击的DOM节点是否在下拉菜单之外,然后我们关闭下拉菜单。你可以在这里阅读更多关于在React中检测外部点击的信息。
在鼠标悬停时为大屏幕切换下拉菜单
让我们添加当用户将鼠标移到菜单项上时显示下拉的功能。
在components/MenuItems.js ,更新JSX中的li ,以包括onMouseEnter 和onMouseLeave 事件。
const MenuItems = ({ items, depthLevel }) => {
// ...
return (
<li
// ...
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{/* ... */}
</li>
);
};
export default MenuItems;
然后,在return 语句上方添加以下事件处理函数。
const onMouseEnter = () => {
window.innerWidth > 960 && setDropdown(true);
};
const onMouseLeave = () => {
window.innerWidth > 960 && setDropdown(false);
};
保存该文件并测试你的项目。
通过该代码,当鼠标指针移动到一个菜单项上时,onMouseEnter 处理程序被调用。从那里,我们检查窗口的内部宽度是否大于960px ,然后,我们打开下拉菜单。
每当鼠标离开菜单项时,我们调用onMouseLeave 处理程序,然后关闭下拉菜单。
结论
我很高兴我们到了这里。现在我们可以在我们的React项目中实现一个多级下拉菜单。通过本教程中的实现,我们可以在数据文件中添加尽可能多的菜单和子菜单,多级下拉就会神奇地出现在前端。
然而,我们应该注意我们添加的下拉菜单的级别,这样用户就不会认为它很烦人。
如果你有问题和或贡献,我在评论区。如果你喜欢这个教程,确保你在网络上分享它。
你可以从这个GitHub仓库找到项目的源代码。
The postCreating a multilevel dropdown menu in Reactappeared first onLogRocket Blog.