多级下拉菜单是网页设计的一个主要部分。由于能够提供多种选择,它们使导航栏变得动态和有组织。
对于任何在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
,其中也可能有Dropdown
s。
从这个细分中,我们将创建四个不同的组件。
创建项目文件
前往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
文件并修改数据以包括多级submenu
s,像这样。
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.