菜单(下拉菜单)
菜单提供了一种简单的方法来构建自定义的、无障碍的下拉组件,并对键盘导航提供了强大的支持。
要开始使用,请通过 npm 安装 Headless UI。
请注意,**此库仅支持 Vue 3**。
npm install @headlessui/vue
菜单按钮使用 Menu
、MenuButton
、MenuItems
和 MenuItem
组件构建。
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()
,这会阻止默认行为,因此不会关闭菜单。
Menu
和 MenuItem
公开了一个 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. -->
<transitionenter-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
。相比之下,Menu
和 MenuItem
不渲染元素,而是在默认情况下直接渲染它们的子项。
这很容易使用 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
会切换菜单。单击打开菜单外部的任何位置都会关闭该菜单。
命令 | 描述 |
Enter 或 Space 当 | 打开菜单并将焦点放在第一个未禁用的项目上 |
ArrowDown 或 ArrowUp当 | 打开菜单并将焦点放在第一个/最后一个未禁用的项目上 |
Esc 当菜单打开时 | 关闭任何打开的菜单 |
ArrowDown 或 ArrowUp当菜单打开时 | 将焦点放在上一个/下一个未禁用的项目上 |
Home 或 PageUp 当菜单打开时 | 将焦点放在第一个未禁用的项目上 |
End 或 PageDown 当菜单打开时 | 将焦点放在最后一个未禁用的项目上 |
Enter 或 Space 当菜单打开时 | 激活/单击当前菜单项 |
A–Z 或 a–z 当菜单打开时 | 将焦点放在与键盘输入匹配的第一个项目上 |
所有相关的 ARIA 属性都会自动管理。
有关 `Menu` 中实现的所有无障碍功能的完整参考,请参阅 菜单按钮的 ARIA 规范。
菜单最适合类似于您在大多数操作系统标题栏中找到的菜单的 UI 元素。它们具有特定的无障碍语义,其内容应限于链接或按钮列表。焦点被困在打开的菜单中,因此您无法使用 Tab 键在内容中或从菜单中切换。相反,箭头键在菜单项中导航。
以下是一些您可能使用 Headless UI 中其他类似组件的场景
-
<Popover />
。弹出窗口是通用的浮动菜单。它们出现在触发它们的按钮附近,您可以将任意标记(如图像或不可点击的内容)放入其中。Tab 键像任何其他正常标记一样导航弹出窗口的内容。它们非常适合构建带有可扩展内容和弹出面板的页眉导航项。 -
<Disclosure />
。披露对于展开以显示更多信息的元素很有用,例如可切换的常见问题解答部分。它们通常以内联方式呈现,并在显示或隐藏时重新排列文档。 -
<Dialog />
。对话框旨在完全吸引用户的注意力。它们通常在屏幕中央渲染一个浮动面板,并使用背景遮罩来使应用程序其他内容变暗。它们还会捕获焦点并阻止从对话框内容中切换标签,直到对话框被关闭。
属性 | 默认值 | 描述 |
as | template | 字符串 | 组件 `Menu` 应该呈现的元素或组件。 |
插槽属性 | 描述 |
open |
菜单是否打开。 |
close |
关闭菜单并重新聚焦 `MenuButton`。 |
属性 | 默认值 | 描述 |
as | button | 字符串 | 组件 `MenuButton` 应该呈现的元素或组件。 |
插槽属性 | 描述 |
open |
菜单是否打开。 |
属性 | 默认值 | 描述 |
as | div | 字符串 | 组件 `MenuItems` 应该呈现的元素或组件。 |
static | false | 布尔值 元素是否应该忽略内部管理的打开/关闭状态。 注意:`static` 和 `unmount` 不能同时使用。如果您尝试这样做,将收到 TypeScript 错误。 |
unmount | true | 布尔值 元素是否应该根据打开/关闭状态卸载或隐藏。 注意:`static` 和 `unmount` 不能同时使用。如果您尝试这样做,将收到 TypeScript 错误。 |
插槽属性 | 描述 |
open |
菜单是否打开。 |
属性 | 默认值 | 描述 |
as | template | 字符串 | 组件 `MenuItem` 应该呈现的元素或组件。 |
disabled | false | 布尔值 项是否应该被禁用以进行键盘导航和 ARIA 目的。 |
插槽属性 | 描述 |
active |
项是否为列表中活动/焦点项。 |
disabled |
项是否为禁用以进行键盘导航和 ARIA 目的。 |
close |
关闭菜单并重新聚焦 `MenuButton`。 |