列表框 (选择)
列表框是构建应用程序自定义、无障碍选择菜单的绝佳基础,并提供对键盘导航的强大支持。
要开始使用,请通过 npm 安装无头 UI。
请注意,此库仅支持 Vue 3。
npm install @headlessui/vue
列表框是使用 Listbox
、ListboxButton
、ListboxOptions
、ListboxOption
和 ListboxLabel
组件构建的。
ListboxButton
会在单击时自动打开/关闭 ListboxOptions
,当菜单打开时,项目列表会获得焦点并自动可通过键盘导航。
<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person" :disabled="person.unavailable" > {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { id: 1, name: 'Durward Reynolds', unavailable: false }, { id: 2, name: 'Kenton Towne', unavailable: false }, { id: 3, name: 'Therese Wunsch', unavailable: false }, { id: 4, name: 'Benedict Kessler', unavailable: true }, { id: 5, name: 'Katelyn Rohan', unavailable: false }, ] const selectedPerson = ref(people[0]) </script>
无头 UI 会跟踪有关每个组件的许多状态,例如当前选择了哪个列表框选项、弹出框是打开还是关闭,或者列表框中的哪个项目当前通过键盘处于活动状态。
但由于组件是无头的,并且在开箱即用时完全没有样式,因此在您自己提供每个状态所需的样式之前,您无法在 UI 中看到这些信息。
每个组件都通过插槽属性公开其当前状态的信息,您可以使用这些属性有条件地应用不同的样式或呈现不同的内容。
例如,ListboxOption
组件公开了 active
状态,该状态告诉您该项目当前是否通过鼠标或键盘处于活动状态。
<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <!-- Use the `active` state to conditionally style the active option. --> <!-- Use the `selected` state to conditionally style the selected option. --> <ListboxOption v-for="person in people" :key="person.id" :value="person" as="template"
v-slot="{ active, selected }"> <li :class="{'bg-blue-500 text-white': active,'bg-white text-black': !active,}" ><CheckIcon v-show="selected" />{{ person.name }} </li> </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' import { CheckIcon } from '@heroicons/vue/20/solid' const people = [ { id: 1, name: 'Durward Reynolds' }, { id: 2, name: 'Kenton Towne' }, { id: 3, name: 'Therese Wunsch' }, { id: 4, name: 'Benedict Kessler' }, { id: 5, name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>
有关所有可用插槽属性的完整列表,请参阅组件 API 文档。
每个组件还通过 data-headlessui-state
属性公开其当前状态的信息,您可以使用这些属性有条件地应用不同的样式。
当插槽属性 API中的任何状态为 true
时,它们将作为以空格分隔的字符串列在这个属性中,以便您可以使用CSS 属性选择器以 [attr~=value]
的形式对它们进行定位。
例如,以下是当列表框打开且第二个项目处于 active
状态时,带有某些子 ListboxOption
组件的 ListboxOptions
组件的渲染内容
<!-- Rendered `ListboxOptions` --> <ul data-headlessui-state="open"> <li data-headlessui-state="">Wade Cooper</li> <li data-headlessui-state="active selected">Arlene Mccoy</li> <li data-headlessui-state="">Devon Webb</li> </ul>
如果您使用的是Tailwind CSS,您可以使用@headlessui/tailwindcss 插件使用修饰符(如 ui-open:*
和 ui-active:*
)来定位此属性
<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"
class="ui-active:bg-blue-500 ui-active:text-white ui-not-active:bg-white ui-not-active:text-black"><CheckIcon class="hidden ui-selected:block" />{{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' import { CheckIcon } from '@heroicons/vue/20/solid' const people = [ { id: 1, name: 'Durward Reynolds' }, { id: 2, name: 'Kenton Towne' }, { id: 3, name: 'Therese Wunsch' }, { id: 4, name: 'Benedict Kessler' }, { id: 5, name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>
与仅允许您提供字符串作为值的本机 HTML 表单控件不同,无头 UI 还支持绑定复杂对象。
<template>
<Listbox v-model="selectedPerson"><ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id":value="person":disabled="person.unavailable" > {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue'const people = [{ id: 1, name: 'Durward Reynolds', unavailable: false },{ id: 2, name: 'Kenton Towne', unavailable: false },{ id: 3, name: 'Therese Wunsch', unavailable: false },{ id: 4, name: 'Benedict Kessler', unavailable: true },{ id: 5, name: 'Katelyn Rohan', unavailable: false },]const selectedPerson = ref(people[1]) </script>
将对象绑定为值时,务必确保您使用同一实例的该对象作为 Listbox
的 value
以及相应的 ListboxOption
,否则它们将无法相等,并会导致列表框行为异常。
为了更轻松地处理同一对象的多个实例,您可以使用 by
属性按特定字段比较对象,而不是比较对象标识
<template> <Listbox :modelValue="modelValue" @update:modelValue="value => emit('update:modelValue', value)"
by="id"> <ListboxButton>{{ modelValue.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="department in departments" :key="department.id" :value="department" > {{ department.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const props = defineProps({ modelValue: Object }) const emit = defineEmits(['update:modelValue']) const departments = [ { id: 1, name: 'Marketing', contact: 'Durward Reynolds' }, { id: 2, name: 'HR', contact: 'Kenton Towne' }, { id: 3, name: 'Sales', contact: 'Therese Wunsch' }, { id: 4, name: 'Finance', contact: 'Benedict Kessler' }, { id: 5, name: 'Customer service', contact: 'Katelyn Rohan' }, ] </script>
如果您希望完全控制对象的比较方式,也可以将自己的比较函数传递给 by
属性
<template> <Listbox :modelValue="modelValue" @update:modelValue="value => emit('update:modelValue', value)"
:by="compareDepartments"> <ListboxButton>{{ modelValue.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="department in departments" :key="department.id" :value="department" > {{ department.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const props = defineProps({ modelValue: Object }) const emit = defineEmits(['update:modelValue'])function compareDepartments(a, b) {return a.name.toLowerCase() === b.name.toLowerCase()}const departments = [ { id: 1, name: 'Marketing', contact: 'Durward Reynolds' }, { id: 2, name: 'HR', contact: 'Kenton Towne' }, { id: 3, name: 'Sales', contact: 'Therese Wunsch' }, { id: 4, name: 'Finance', contact: 'Benedict Kessler' }, { id: 5, name: 'Customer service', contact: 'Katelyn Rohan' }, ] </script>
要允许在列表框中选择多个值,请使用 multiple
属性并将数组传递给 v-model
,而不是传递单个选项。
<template>
<Listbox v-model="selectedPeople" multiple><ListboxButton> {{ selectedPeople.map((person) => person.name).join(', ') }} </ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { id: 1, name: 'Durward Reynolds' }, { id: 2, name: 'Kenton Towne' }, { id: 3, name: 'Therese Wunsch' }, { id: 4, name: 'Benedict Kessler' }, { id: 5, name: 'Katelyn Rohan' }, ]const selectedPeople = ref([people[0], people[1]])</script>
这将使列表框在您选择选项时保持打开状态,选择选项将将其切换到位。
每当添加或删除选项时,您的 v-model
绑定将使用包含所有选中选项的数组进行更新。
默认情况下,Listbox
将使用按钮内容作为屏幕阅读器的标签。如果您希望更详细地控制向辅助技术宣布的内容,请使用 ListboxLabel
组件。
<template> <Listbox v-model="selectedPerson">
<ListboxLabel>Assignee:</ListboxLabel><ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { id: 1, name: 'Durward Reynolds' }, { id: 2, name: 'Kenton Towne' }, { id: 3, name: 'Therese Wunsch' }, { id: 4, name: 'Benedict Kessler' }, { id: 5, name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>
如果您将 name
属性添加到列表框中,则会渲染隐藏的 input
元素,并与您选择的 value 保持同步。
<template> <form action="/projects/1/assignee" method="post">
<Listbox v-model="selectedPerson" name="assignee"><ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> <button>Submit</button> </form> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { id: 1, name: 'Durward Reynolds' }, { id: 2, name: 'Kenton Towne' }, { id: 3, name: 'Therese Wunsch' }, { id: 4, name: 'Benedict Kessler' }, { id: 5, name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>
这使您可以在本机 HTML <form>
内使用列表框,并进行传统的表单提交,就好像您的列表框是本机 HTML 表单控件一样。
像字符串这样的基本值将被渲染为包含该值的单个隐藏输入,但像对象这样的复杂值将使用方括号表示法对名称进行编码,从而被编码为多个输入
<input type="hidden" name="assignee[id]" value="1" /> <input type="hidden" name="assignee[name]" value="Durward Reynolds" />
如果您向 Listbox
提供 defaultValue
属性而不是 value
属性,无头 UI 将为您在内部跟踪其状态,使您可以将其用作非受控组件。
您可以通过 Listbox
和 ListboxButton
组件上的 value
插槽属性访问当前选中的选项。
<template> <form action="/projects/1/assignee" method="post">
<Listbox name="assignee" :defaultValue="people[0]"><ListboxButton v-slot="{ value }">{{ value.name }}</ListboxButton><ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> <button>Submit</button> </form> </template> <script setup> import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { id: 1, name: 'Durward Reynolds' }, { id: 2, name: 'Kenton Towne' }, { id: 3, name: 'Therese Wunsch' }, { id: 4, name: 'Benedict Kessler' }, { id: 5, name: 'Katelyn Rohan' }, ] </script>
当与 HTML 表单一起使用或使用通过FormData 收集其状态而不是使用 React 状态来跟踪其状态的表单 API 时,这可以简化您的代码。
您提供的任何 @update:modelValue
属性都将在组件的值更改时被调用(以防您需要运行任何副作用),但您无需使用它来自己跟踪组件的状态。
默认情况下,您的 ListboxOptions
实例将根据 Listbox
组件本身内部跟踪的 open
状态自动显示/隐藏。
<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <!-- By default, the `ListboxOptions` will automatically show/hide when the `ListboxButton` is pressed. --> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { name: 'Durward Reynolds' }, { name: 'Kenton Towne' }, { name: 'Therese Wunsch' }, { name: 'Benedict Kessler' }, { name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>
如果您希望自己处理此操作(也许是因为出于某种原因您需要添加额外的包装元素),则可以向 ListboxOptions
实例添加 static
属性以告诉它始终渲染,并检查 Listbox
提供的 open
插槽属性以控制哪个元素自己显示/隐藏。
<template>
<Listbox v-model="selectedPerson" v-slot="{ open }"><ListboxButton>{{ selectedPerson.name }}</ListboxButton><div v-show="open"><!-- Using the `static` prop, the `ListboxOptions` are always rendered and the `open` state is ignored. --><ListboxOptions static><ListboxOption v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </div> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { name: 'Durward Reynolds' }, { name: 'Kenton Towne' }, { name: 'Therese Wunsch' }, { name: 'Benedict Kessler' }, { name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>
使用 disabled
属性禁用 ListboxOption
。这将使其无法通过鼠标和键盘选择,并且在按下向上/向下箭头时将被跳过。
<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <!-- Disabled options will be skipped by keyboard navigation. --> <ListboxOption v-for="person in people" :key="person.name" :value="person"
:disabled="person.unavailable"> <span :class='{ "opacity-75": person.unavailable }'> {{ person.name }} </span> </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { name: 'Durward Reynolds', unavailable: true }, { name: 'Kenton Towne', unavailable: false }, { name: 'Therese Wunsch', unavailable: false }, { name: 'Benedict Kessler', unavailable: true }, { name: 'Katelyn Rohan', unavailable: false }, ] const selectedPerson = ref(people[0]) </script>
要为列表框的打开/关闭设置动画,您可以使用 Vue 的内置 <transition>
组件。您所需要做的就是将您的 ListboxOptions
实例包装在一个 <transition>
中,过渡将自动应用。
<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <!-- Use Vue's built-in `transition` component 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"><ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </transition> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { id: 1, name: 'Durward Reynolds' }, { id: 2, name: 'Kenton Towne' }, { id: 3, name: 'Therese Wunsch' }, { id: 4, name: 'Benedict Kessler' }, { id: 5, name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>
如果您希望为列表框的不同子项协调多个过渡,请查看无头 UI 中包含的过渡组件。
默认情况下,Listbox
及其子组件分别渲染一个适合该组件的默认元素。
例如,ListboxLabel
默认渲染一个 label
,ListboxButton
渲染一个 button
,ListboxOptions
渲染一个 ul
,ListboxOption
渲染一个 li
。相比之下,Listbox
不会渲染元素,而是直接渲染其子项。
这很容易使用 as
属性更改,该属性存在于每个组件上。
<template> <!-- Render a `div` instead of nothing -->
<Listbox as="div" v-model="selectedPerson"><ListboxButton>{{ selectedPerson.name }}</ListboxButton> <!-- Render a `div` instead of a `ul` --><ListboxOptions as="div"><!-- Render a `span` instead of a `li` --> <ListboxOptionas="span"v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { id: 1, name: 'Durward Reynolds' }, { id: 2, name: 'Kenton Towne' }, { id: 3, name: 'Therese Wunsch' }, { id: 4, name: 'Benedict Kessler' }, { id: 5, name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>
要告诉元素直接渲染其子项,而没有包装元素,请使用 as="template"
。
<template> <Listbox v-model="selectedPerson"> <!-- Render children directly instead of a `ListboxButton` -->
<ListboxButton as="template"><button>{{ selectedPerson.name }}</button> </ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { id: 1, name: 'Durward Reynolds' }, { id: 2, name: 'Kenton Towne' }, { id: 3, name: 'Therese Wunsch' }, { id: 4, name: 'Benedict Kessler' }, { id: 5, name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>
如果您已将 ListboxOptions
设置为水平显示,请在 Listbox
组件上使用 horizontal
属性以启用使用左右箭头键而不是上下箭头键导航项目,并更新辅助技术的 aria-orientation
属性。
<template>
<Listbox v-model="selectedPerson" horizontal><ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions class="flex flex-row"> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { id: 1, name: 'Durward Reynolds' }, { id: 2, name: 'Kenton Towne' }, { id: 3, name: 'Therese Wunsch' }, { id: 4, name: 'Benedict Kessler' }, { id: 5, name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>
当列表框切换打开时,ListboxOptions
会获得焦点。焦点会一直停留在项目列表中,直到按下Escape或用户单击选项之外的区域。关闭列表框会将焦点返回到 ListboxButton
。
单击 ListboxButton
会切换选项列表的打开和关闭。单击选项列表之外的任何位置都会关闭列表框。
命令 | 描述 |
Enter, 空格, 向下箭头, 或者 向上箭头当 | 打开列表框并使选定项获得焦点 |
Esc 当列表框打开时 | 关闭列表框 |
向下箭头 或者 向上箭头当列表框打开时 | 使前一个/下一个非禁用项获得焦点 |
向左箭头 或者 向右箭头当列表框打开且 | 使前一个/下一个非禁用项获得焦点 |
Home 或者 PageUp 当列表框打开时 | 使第一个非禁用项获得焦点 |
End 或者 PageDown 当列表框打开时 | 使最后一个非禁用项获得焦点 |
Enter 或者 空格 当列表框打开时 | 选择当前项 |
A–Z 或者 a–z 当列表框打开时 | 使与键盘输入匹配的第一个项获得焦点 |
属性 | 默认值 | 描述 |
as | template | String | Component
|
v-model | — | T 选定的值。 |
defaultValue | — | T 使用无控制组件时的默认值。 |
by | — | keyof T | ((a: T, z: T) => boolean) 使用此属性可以通过特定字段比较对象,或传递您自己的比较函数来完全控制对象比较方式。 |
disabled | false | Boolean 使用此属性可禁用整个列表框组件及其相关子组件。 |
horizontal | false | Boolean 如果为 true,则 |
name | — | String 在表单中使用此组件时的名称。 |
multiple | false | Boolean 是否可以选中多个选项。 |
插槽属性 | 描述 |
value |
选定的值。 |
open |
列表框是否打开。 |
disabled |
列表框是否禁用。 |
属性 | 默认值 | 描述 |
as | button | String | Component
|
插槽属性 | 描述 |
value |
选定的值。 |
open |
列表框是否打开。 |
disabled |
列表框是否禁用。 |
属性 | 默认值 | 描述 |
as | label | String | Component
|
插槽属性 | 描述 |
open |
列表框是否打开。 |
disabled |
列表框是否禁用。 |
属性 | 默认值 | 描述 |
as | ul | String | Component
|
static | false | Boolean 元素是否应该忽略内部管理的打开/关闭状态。 |
unmount | true | Boolean 元素是否应该根据打开/关闭状态卸载或隐藏。 |
插槽属性 | 描述 |
open |
列表框是否打开。 |
属性 | 默认值 | 描述 |
value | — | T 选项值。 |
as | li | String | Component
|
disabled | false | Boolean 选项是否应该针对键盘导航和 ARIA 处于禁用状态。 |
插槽属性 | 描述 |
active |
选项是否为活动/焦点选项。 |
selected |
选项是否为选中选项。 |
disabled |
选项是否针对键盘导航和 ARIA 处于禁用状态。 |