菜单(下拉菜单)

菜单提供了一种简单的方法来构建自定义的、无障碍的下拉组件,并对键盘导航提供了强大的支持。

要开始使用,请通过 npm 安装 Headless UI。

请注意,**此库仅支持 Vue 3**。

npm install @headlessui/vue

菜单按钮使用 MenuMenuButtonMenuItemsMenuItem 组件构建。

MenuButton 在被点击时会自动打开/关闭 MenuItems,并且当菜单打开时,项目列表会获得焦点,并且可以通过键盘自动导航。

<template> <Menu> <MenuButton>More</MenuButton> <MenuItems> <MenuItem v-slot="{ active }"> <a :class='{ "bg-blue-500": active }' href="/account-settings"> Account settings </a> </MenuItem> <MenuItem v-slot="{ active }"> <a :class='{ "bg-blue-500": active }' href="/account-settings"> Documentation </a> </MenuItem> <MenuItem disabled> <span class="opacity-75">Invite a friend (coming soon!)</span> </MenuItem> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

Headless UI 会跟踪每个组件的许多状态信息,例如,当前选择了哪个列表框选项、弹出窗口是打开还是关闭,或者哪个菜单项当前通过键盘处于活动状态。

但由于这些组件是无状态的,并且在开箱即用时完全没有样式,因此您无法在 UI 中看到这些信息,除非您自己为每个状态提供所需的样式。

每个组件都通过 插槽属性 公开有关其当前状态的信息,您可以使用这些属性有条件地应用不同的样式或渲染不同的内容。

例如,MenuItem 组件公开了一个 active 状态,它告诉您该项目当前是否通过鼠标或键盘处于焦点状态。

<template> <Menu> <MenuButton>Options</MenuButton> <MenuItems> <!-- Use the `active` state to conditionally style the active item. --> <MenuItem v-for="link in links" :key="link.href" as="template"
v-slot="{ active }"
>
<a :href="link.href"
:class="{ 'bg-blue-500 text-white': active, 'bg-white text-black': !active }"
>
{{ link.label }} </a> </MenuItem> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' const links = [ { href: '/account-settings', label: 'Account settings' }, { href: '/support', label: 'Support' }, { href: '/license', label: 'License' }, { href: '/sign-out', label: 'Sign out' }, ] </script>

有关所有可用插槽属性的完整列表,请参阅 组件 API 文档

每个组件还通过 data-headlessui-state 属性公开有关其当前状态的信息,您可以使用该属性有条件地应用不同的样式。

插槽属性 API 中的任何状态为 true 时,它们将作为以空格分隔的字符串列在此属性中,因此您可以使用 CSS 属性选择器[attr~=value] 的形式针对它们。

例如,以下是在菜单打开且第二个项目处于 active 状态时,MenuItems 组件及其一些子 MenuItem 组件渲染的内容。

<!-- Rendered `MenuItems` --> <ul data-headlessui-state="open"> <li data-headlessui-state="">Account settings</li> <li data-headlessui-state="active">Support</li> <li data-headlessui-state="">License</li> </ul>

如果您使用的是 Tailwind CSS,您可以使用 @headlessui/tailwindcss 插件使用修饰符(如 ui-open:*ui-active:*)来定位此属性。

<template> <Menu> <MenuButton>Options</MenuButton> <MenuItems> <!-- Use the `active` state to conditionally style the active item. --> <MenuItem v-for="link in links" :key="link.href" :href="link.href" as="a"
class="ui-active:bg-blue-500 ui-active:text-white ui-not-active:bg-white ui-not-active:text-black"
>
{{ link.label }} </MenuItem> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' const links = [ { href: '/account-settings', label: 'Account settings' }, { href: '/support', label: 'Support' }, { href: '/license', label: 'License' }, { href: '/sign-out', label: 'Sign out' }, ] </script>

默认情况下,您的 MenuItems 实例将根据 Menu 组件本身内部跟踪的内部 open 状态自动显示/隐藏。

<template> <Menu> <MenuButton>More</MenuButton> <!-- By default, the `MenuItems` will automatically show/hide when the `MenuButton` is pressed. --> <MenuItems> <MenuItem><!-- ... --></MenuItem> <!-- ... --> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

如果您想自己处理这个问题(可能是因为出于某种原因需要添加一个额外的包装元素),则可以在 MenuItems 实例中添加一个 static 属性来告诉它始终进行渲染,并检查 Menu 提供的 open 插槽属性以控制自己显示/隐藏哪个元素。

<template>
<Menu v-slot="{ open }">
<MenuButton>More</MenuButton>
<div v-show="open">
<!-- Using the `static` prop, the `MenuItems` are always rendered and the `open` state is ignored. -->
<MenuItems static>
<MenuItem><!-- ... --></MenuItem> <!-- ... --> </MenuItems> </div> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

菜单默认情况下会关闭,但是可能会发生第三方 Link 组件使用 event.preventDefault(),这会阻止默认行为,因此不会关闭菜单。

MenuMenuItem 公开了一个 close() 插槽属性,您可以使用它来强制关闭菜单。

<template> <Menu> <MenuButton>Terms</MenuButton> <MenuItems>
<MenuItem v-slot="{ close }">
<MyCustomLink href="/" @click="close">Read and accept</MyCustomLink>
</MenuItem> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' import { MyCustomLink } from './MyCustomLink' </script>

使用 disabled 属性禁用 MenuItem。这将使它无法通过键盘导航进行选择,并且在按下向上/向下箭头时将跳过它。

<template> <Menu> <MenuButton>More</MenuButton> <MenuItems> <!-- ... --> <!-- This item will be skipped by keyboard navigation. -->
<MenuItem disabled>
<span class="opacity-75">Invite a friend (coming soon!)</span> </MenuItem> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

要为菜单面板的打开/关闭设置动画,可以使用 Vue 内置的 <transition> 组件。您只需要将您的 MenuItems 实例包装在 <transition> 中,该过渡将自动应用。

<template> <Menu> <MenuButton>More</MenuButton> <!-- Use Vue's built-in `transition` element to add transitions. -->
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-out"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<MenuItems> <MenuItem><!-- ... --></MenuItem> <!-- ... --> </MenuItems> </transition> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

如果您想协调菜单不同子项的多个过渡,请查看 Headless UI 中包含的 Transition 组件

role="menu" 的无障碍语义非常严格,任何不是 MenuItem 组件的 Menu 子项将自动从辅助技术中隐藏,以确保菜单按屏幕阅读器用户预期的方式工作。

因此,不建议渲染任何除了 MenuItem 组件之外的子项,因为该内容将无法访问使用辅助技术的人员。

如果您想构建一个内容更灵活的下拉菜单,请考虑使用 Popover 代替。

默认情况下,Menu 及其子组件分别渲染一个对于该组件来说合理的默认元素。

例如,MenuButton 默认情况下渲染一个 button,而 MenuItems 渲染一个 div。相比之下,MenuMenuItem 不渲染元素,而是在默认情况下直接渲染它们的子项。

这很容易使用 as 属性更改,该属性存在于每个组件上。

<template> <!-- Render a `div` instead of no wrapper element -->
<Menu as="div">
<MenuButton>More</MenuButton> <!-- Render a `section` instead of a `div` -->
<MenuItems as="section">
<MenuItem v-slot="{ active }"> <a :class='{ "bg-blue-500": active }' href="/account-settings"> Account settings </a> </MenuItem> <!-- ... --> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

要告诉一个元素在没有包装元素的情况下直接渲染其子项,请使用 as="template"

<template> <Menu> <!-- Render no wrapper, instead pass in a `button` manually. -->
<MenuButton as="template">
<button>More</button> </MenuButton> <MenuItems> <MenuItem v-slot="{ active }"> <a :class='{ "bg-blue-500": active }' href="/account-settings"> Account settings </a> </MenuItem> <!-- ... --> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

如果您在 MenuItem 中使用交互式元素(如 <a> 标签),这一点很重要。如果 MenuItem 有一个 as="div",那么 Headless UI 提供的属性将被转发到 div 而不是 a,这意味着您无法再通过键盘转到 <a> 标签提供的 URL。

单击 MenuButton 会切换菜单并将焦点放在 MenuItems 组件上。焦点将被困在打开的菜单中,直到按下 Escape 或用户单击菜单外部。关闭菜单会将焦点返回到 MenuButton

单击 MenuButton 会切换菜单。单击打开菜单外部的任何位置都会关闭该菜单。

命令描述

EnterSpaceMenuButton 处于焦点状态时

打开菜单并将焦点放在第一个未禁用的项目上

ArrowDownArrowUpMenuButton 处于焦点状态时

打开菜单并将焦点放在第一个/最后一个未禁用的项目上

Esc 当菜单打开时

关闭任何打开的菜单

ArrowDownArrowUp当菜单打开时

将焦点放在上一个/下一个未禁用的项目上

HomePageUp 当菜单打开时

将焦点放在第一个未禁用的项目上

EndPageDown 当菜单打开时

将焦点放在最后一个未禁用的项目上

EnterSpace 当菜单打开时

激活/单击当前菜单项

A–Za–z 当菜单打开时

将焦点放在与键盘输入匹配的第一个项目上

所有相关的 ARIA 属性都会自动管理。

有关 `Menu` 中实现的所有无障碍功能的完整参考,请参阅 菜单按钮的 ARIA 规范

菜单最适合类似于您在大多数操作系统标题栏中找到的菜单的 UI 元素。它们具有特定的无障碍语义,其内容应限于链接或按钮列表。焦点被困在打开的菜单中,因此您无法使用 Tab 键在内容中或从菜单中切换。相反,箭头键在菜单项中导航。

以下是一些您可能使用 Headless UI 中其他类似组件的场景

  • <Popover />。弹出窗口是通用的浮动菜单。它们出现在触发它们的按钮附近,您可以将任意标记(如图像或不可点击的内容)放入其中。Tab 键像任何其他正常标记一样导航弹出窗口的内容。它们非常适合构建带有可扩展内容和弹出面板的页眉导航项。

  • <Disclosure />。披露对于展开以显示更多信息的元素很有用,例如可切换的常见问题解答部分。它们通常以内联方式呈现,并在显示或隐藏时重新排列文档。

  • <Dialog />。对话框旨在完全吸引用户的注意力。它们通常在屏幕中央渲染一个浮动面板,并使用背景遮罩来使应用程序其他内容变暗。它们还会捕获焦点并阻止从对话框内容中切换标签,直到对话框被关闭。

属性默认值描述
astemplate
字符串 | 组件

`Menu` 应该呈现的元素或组件。

插槽属性描述
open

布尔值

菜单是否打开。

close

() => void

关闭菜单并重新聚焦 `MenuButton`。

属性默认值描述
asbutton
字符串 | 组件

`MenuButton` 应该呈现的元素或组件。

插槽属性描述
open

布尔值

菜单是否打开。

属性默认值描述
asdiv
字符串 | 组件

`MenuItems` 应该呈现的元素或组件。

staticfalse
布尔值

元素是否应该忽略内部管理的打开/关闭状态。

注意:`static` 和 `unmount` 不能同时使用。如果您尝试这样做,将收到 TypeScript 错误。

unmounttrue
布尔值

元素是否应该根据打开/关闭状态卸载或隐藏。

注意:`static` 和 `unmount` 不能同时使用。如果您尝试这样做,将收到 TypeScript 错误。

插槽属性描述
open

布尔值

菜单是否打开。

属性默认值描述
astemplate
字符串 | 组件

`MenuItem` 应该呈现的元素或组件。

disabledfalse
布尔值

项是否应该被禁用以进行键盘导航和 ARIA 目的。

插槽属性描述
active

布尔值

项是否为列表中活动/焦点项。

disabled

布尔值

项是否为禁用以进行键盘导航和 ARIA 目的。

close

() => void

关闭菜单并重新聚焦 `MenuButton`。

如果您有兴趣使用 Headless UI 和 Tailwind CSS 的预先设计组件示例,请查看 **Tailwind UI** - 由我们精心打造的精美设计和专家制作的组件集合。

这是支持我们对诸如本项目之类的开源项目的绝佳方式,使我们能够改进它们并保持良好的维护。