在React中创建一个多级下拉菜单教程

3,446 阅读10分钟

多级下拉菜单是网页设计的一个主要部分。由于能够提供多种选择,它们使导航栏变得动态和有组织。

对于任何在React或任何基于React的项目(如Gatsby或Next.js)中工作的开发者,本教程涵盖了如何在React项目中实现下拉功能的逐步过程。在本指南的最后,我们将拥有下面的菜单。

Implementing A Dropdown Feature In A React Project

要学习本教程,请确保你对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设计分解成独立的和可重用的组件,如下所述。

Decompose Your Project UI Design Into Independent And Reusable Components

父组件,App ,持有标志和Navbar 组件,Navbar 持有MenuItems 组件。在MenuItems ,我们有各个项目和Dropdown 组件。Dropdown 中也有MenuItems ,其中也可能有Dropdowns。

从这个细分中,我们将创建四个不同的组件。

创建项目文件

前往src 文件夹,删除所有的文件,除了index.js 。接下来,在src 内创建一个名为components 的文件夹,并添加以下组件文件:App.js,Dropdown.js,MenuItems.jsNavbar.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> 标签作为内部链接。通常情况下,我们会使用LinkNavLink (用于菜单导航)从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;

保存该文件并查看前台。它应该看起来像这样。

Navbar And Menu Items

这是一个基本的导航菜单。让我们更进一步,接下来显示一个单层下拉菜单。

渲染一个单级下拉菜单

让我们前往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; ,菜单应该看起来像这样。

Services Dropdown Menu

很好。我们正在取得进展!

切换下拉菜单

现在让我们定义一下检测下拉菜单项被点击时的逻辑,这样我们就可以动态地显示或隐藏下拉框。要做到这一点,我们必须添加一个状态并在点击下拉菜单时更新它。

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;
}

让我们保存我们的文件。我们现在应该能够切换我们的菜单下拉。

Toggling Menu Dropdown

多级下拉菜单

像单级下拉菜单一样,为了添加一个多级下拉菜单,让我们打开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;

如果我们保存文件并测试下拉菜单,它可以工作,但我们会注意到下拉菜单互相重叠了。

Overlapping Dropdowns

通过点击网页开发子菜单,我们希望将其下拉菜单逻辑地定位到右边。我们可以通过检测下拉深度级别来实现这一点。

检测菜单深度级别

知道了菜单的深度级别,我们就可以做几件事。首先,我们可以动态地添加不同的箭头来显示一个下拉菜单的存在。其次,我们可以用它来检测 "第二层及以上 "的下拉菜单,从而在逻辑上将它们定位到子菜单的右边。

打开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>&raquo;</span> : <span className="arrow" />}
     </button>
     <Dropdown
      depthLevel={depthLevel}
      // ...
     />

对于depthLevel 大于0 ,我们使用一个HTML实体名称&raquo; ,显示一个右箭头,否则我们添加一个.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;

让我们保存文件并测试该项目。

Testing Multiple Dropdown Menus

现在我们可以切换菜单了,我们需要一种方法来在我们点击下拉菜单之外的地方关闭下拉菜单。

当用户在下拉菜单外点击时关闭下拉菜单

通过点击下拉菜单之外,我们要关闭它。我们可以通过将dropdown 状态设置为默认值false 来做到这一点。我们将定义一个逻辑来检测在下拉菜单外的点击。

让我们打开components/MenuItems.js 文件,并更新import ,像这样包括useEffectuseRef 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 ,以包括onMouseEnteronMouseLeave 事件。

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.