列表框

列表框是为您的应用构建定制的可访问选择菜单的绝佳基础,并完全支持键盘导航。

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

npm install @headlessui/react

列表框使用 ListboxListboxButtonListboxSelectedOptionListboxOptionsListboxOption 组件构建。

当单击 ListboxButton 时,会自动打开/关闭 ListboxOptions,当列表框处于打开状态时,选项列表获得焦点并可通过键盘自动浏览。

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
      <ListboxOptions anchor="bottom">
        {people.map((person) => (
          <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
            {person.name}
          </ListboxOption>
        ))}
      </ListboxOptions>
    </Listbox>
  )
}

Headless UI 会跟踪每个组件的许多状态,例如哪个列表框选项当前处于选中状态、弹出窗口是处于打开还是关闭状态,或菜单中的哪个项目当前通过键盘获得了焦点。

但是,因为这些组件是无头组件,且在开箱即用的状态下没有经过完全样式化,所以在您为自己提供的各个状态设置样式之前,您无法在您的 UI 中看到这些信息。

对 Headless UI 组件的不同状态进行样式化的最简单方式是使用每个组件公开的 data-* 属性。

例如,ListboxOption 组件公开了一个 data-focus 属性,用于告知您选项当前是否通过鼠标或键盘获得了焦点,以及一个 data-selected 属性,用于告知您该选项是否匹配 Listbox 的当前 value

<!-- Rendered `ListboxOption` -->
<div data-focus data-selected>Arlene Mccoy</div>

使用 CSS 属性选择器 根据这些数据属性的存在有条件地应用样式。如果您正在使用 Tailwind CSS,数据属性修饰符 使此操作变得简单。

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { CheckIcon } from '@heroicons/react/20/solid'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
      <ListboxOptions anchor="bottom">
        {people.map((person) => (
<ListboxOption key={person.id} value={person} className="group flex gap-2 bg-white data-[focus]:bg-blue-100">
<CheckIcon className="invisible size-5 group-data-[selected]:visible" />
{person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

请参阅 组件 API 以获取所有可用数据属性的列表。

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

例如,ListboxOption 组件公开了一个 focus 状态,用于告知您选项当前是否通过鼠标或键盘获得了焦点,以及一个 selected 状态,用于告知您该选项是否匹配 Listbox 的当前 value

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { CheckIcon } from '@heroicons/react/20/solid'
import clsx from 'clsx'
import { Fragment, useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
      <ListboxOptions anchor="bottom">
        {people.map((person) => (
<ListboxOption key={person.id} value={person} as={Fragment}>
{({ focus, selected }) => (
<div className={clsx('flex gap-2', focus && 'bg-blue-100')}>
<CheckIcon className={clsx('size-5', !selected && 'invisible')} />
{person.name}
</div>
)}
</ListboxOption>
))} </ListboxOptions> </Listbox> ) }

请参阅 组件 API 以获取所有可用渲染属性的列表。

LabelListboxField 组件包装起来,使用一个生成的 ID 自动化关联这两个组件

import { Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
<Field>
<Label>Assignee:</Label>
<Listbox value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox>
</Field>
) }

Field 中使用 Description 组件,使用 aria-describedby 属性将它自动关联到 Listbox

import { Description, Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
<Field>
<Label>Assignee:</Label>
<Description>This person will have full access to this project.</Description>
<Listbox value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox>
</Field>
) }

Field 组件添加 disabled 道具,禁用 Listbox 及其关联的 LabelDescription

import { Description, Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
<Field disabled>
<Label>Assignee:</Label> <Description>This person will have full access to this project.</Description> <Listbox value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> </Field> ) }

你还可以在 Field 外禁用列表框,方法是直接向 Listbox 本身添加 disabled 道具。

使用 disabled 道具禁用 ListboxOption,阻止它被选中

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

const people = [
  { id: 1, name: 'Durward Reynolds', available: true },
  { id: 2, name: 'Kenton Towne', available: true },
  { id: 3, name: 'Therese Wunsch', available: true },
{ id: 4, name: 'Benedict Kessler', available: false },
{ id: 5, name: 'Katelyn Rohan', available: true }, ] function Example() { const [selectedPerson, setSelectedPerson] = useState(people[0]) return ( <Listbox value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person}
disabled={!person.available}
className="data-[focus]:bg-blue-100 data-[disabled]:opacity-50"
>
{person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

如果你向 Listbox 中添加 name 道具,它会渲染并同步维护一个隐藏的 input 元素和列表框状态。

import { Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
<form action="/projects/1" method="post">
<Field> <Label>Assignee:</Label>
<Listbox name="assignee" value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> </Field> <button>Submit</button>
</form>
) }

这让你可以在原生 HTML <form> 中使用列表框,并像使用原生 HTML 表单控件一样进行传统表单提交。

基本值(如字符串)将渲染为包含该值的单个隐藏输入,但复杂值(如对象)将使用方括号表示法对名称进行编码,并编码为多个输入

<!-- Rendered hidden inputs -->
<input type="hidden" name="assignee[id]" value="1" />
<input type="hidden" name="assignee[name]" value="Durward Reynolds" />

如果你省略 value 道具,Headless UI 将内部跟踪其状态,让你可以将其用作 不受控组件

在不受控的情况下,使用 defaultValue 道具为 Listbox 提供一个初始值。

import { Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'

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' },
]

function Example() {
  return (
    <form action="/projects/1" method="post">
      <Field>
        <Label>Assignee:</Label>
<Listbox name="assignee" defaultValue={people[0]}>
<ListboxButton>{({ value }) => value.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> </Field> <button>Submit</button> </form> ) }

当将列表框 配合 HTML 表单使用,或者使用通过 FormData 代替使用 React 状态跟踪其状态的表单 API 时,它可以简化你的代码。

如果您需要运行任何副作用,那么当组件的值更改时,您提供的任何 onChange 属性仍将被调用,但您不必使用它来自己跟踪组件的状态。

默认情况下,ListboxOptions 下拉菜单没有设置宽度,但您可以使用 CSS 添加宽度

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" className="w-52">
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

如果您希望下拉菜单宽度与 ListboxButton 宽度匹配,请使用 ListboxOptions 元素上公开的 --button-width CSS 变量

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" className="w-[var(--button-width)]">
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

anchor 属性添加到 ListboxOptions 以根据 ListboxButton 自动放置下拉菜单

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom start">
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

使用值 toprightbottomleft 将下拉菜单居中对齐到适当的边缘,或将其与 startend 组合,以将下拉菜单对齐到特定角,例如 top startbottom end

若要控制按钮与下拉菜单之间的间隙,请使用 --anchor-gap CSS 变量

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom start" className="[--anchor-gap:4px] sm:[--anchor-gap:8px]">
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

此外,您可以使用 --anchor-offset 控制下拉菜单与原始位置偏移的距离,并使用 --anchor-padding 控制下拉菜单与视口之间应存在的最小空间。

anchor 属性还支持一个对象 API,它允许您使用 JavaScript 控制 gapoffsetpadding

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor={{ to: 'bottom start', gap: '4px' }}>
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

请参阅 ListboxOptions API 了解有关这些选项的更多信息。

如果您已经设置 ListboxOptions 以水平显示,请在 Listbox 组件上使用 horizontal 属性,以使用左右箭头键(而不是上下键)导航选项,并更新辅助技术使用的 aria-orientation 属性。

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
<Listbox horizontal value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" className="flex flex-row">
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

若要对列表框下拉菜单的打开和关闭进行动画处理,请将 transition 属性添加到 ListboxOptions 组件,然后使用 CSS 设置过渡的不同阶段的样式

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
      <ListboxOptions
        anchor="bottom"
transition
className="origin-top transition duration-200 ease-out data-[closed]:scale-95 data-[closed]:opacity-0"
>
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

在内部,transition prop 的实现与 Transition 组件完全相同。有关更多详情,请参阅 Transition 文档

Headless UI 还可以很好地与 React 生态系统中的其他动画库配合使用,例如 Framer MotionReact Spring。只需向这些库公开一些状态即可。

例如,要使用 Framer Motion 对列表框制作动画,可将 static prop 添加到 ListboxOptions 组件,然后根据 open render prop 有条件地渲染它

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
{({ open }) => (
<> <ListboxButton>{selectedPerson.name}</ListboxButton> <AnimatePresence>
{open && (
<ListboxOptions
static
as={motion.div} initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} anchor="bottom" className="origin-top" >
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions>
)}
</AnimatePresence> </>
)}
</Listbox> ) }

与仅允许你提供字符串作为值的原生 HTML 表单控件不同,Headless UI 还支持绑定复杂对象。

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]
function Example() { const [selectedPerson, setSelectedPerson] = useState(people[0]) return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption> ))} </ListboxOptions> </Listbox> ) }

将对象绑定为值时,务必确保你将对象的同一个实例用作 Listboxvalue 以及对应的 ListboxOption,否则它们将无法相等并导致列表框行为不正确。

为了更轻松地使用同一对象的多个实例,你可以使用 by prop 来根据特定字段来比较对象,而不是比较对象标识。

当你将对象传递给 value prop 时,by 的默认值为 id(如果存在),但你可以将其设置为你喜欢的任何字段

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'

const departments = [
  { name: 'Marketing', contact: 'Durward Reynolds' },
  { name: 'HR', contact: 'Kenton Towne' },
  { name: 'Sales', contact: 'Therese Wunsch' },
  { name: 'Finance', contact: 'Benedict Kessler' },
  { name: 'Customer service', contact: 'Katelyn Rohan' },
]

function Example({ selectedDepartment, onChange }) {
  return (
<Listbox value={selectedDepartment} by="name" onChange={onChange}>
<ListboxButton>{selectedDepartment.name}</ListboxButton> <ListboxOptions anchor="bottom"> {departments.map((department) => ( <ListboxOption key={department.name} value={department} className="data-[focus]:bg-blue-100"> {department.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

如果你希望完全控制比较对象的方式,还可以将你自己的比较函数传递给 by prop

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'

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' },
]

function compareDepartments(a, b) {
return a.name.toLowerCase() === b.name.toLowerCase()
}
function Example({ selectedDepartment, onChange }) { return (
<Listbox value={selectedDepartment} by={compareDepartments} onChange={onChange}>
<ListboxButton>{selectedDepartment.name}</ListboxButton> <ListboxOptions anchor="bottom"> {departments.map((department) => ( <ListboxOption key={department.id} value={department} className="data-[focus]:bg-blue-100"> {department.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

若要允许在你的列表框中选择多个值,请使用 multiple prop,并将数组传递给 value,而不是传递单个选项。

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

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' },
]

function Example() {
const [selectedPeople, setSelectedPeople] = useState([people[0], people[1]])
return (
<Listbox value={selectedPeople} onChange={setSelectedPeople} multiple>
<ListboxButton>{selectedPeople.map((person) => person.name).join(', ')}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

这样会在你选择选项时保持列表框处于打开状态,选择选项将原地切换。

每当添加或删除选项时,你的 onChange 处理程序都会被调用一次,并带有包含所有所选选项的数组。

默认情况下,Listbox及其子组件各自渲染对该组件有意义的默认元素。

例如,ListboxButton 呈现一个 buttonListboxOptions 呈现一个 div,并且 ListboxOption 呈现一个 div。相比之下,Listbox 不呈现一个元素,而是直接呈现其子元素。

使用 as 属性将组件呈现为不同的元素或你自己的自定义组件,确保你的自定义组件 转发引用,以便 Headless UI 能够正确连接。

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { forwardRef, useState } from 'react'

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' },
]

let MyCustomButton = forwardRef(function (props, ref) {
return <button className="..." ref={ref} {...props} />
})
function Example() { const [selectedPerson, setSelectedPerson] = useState(people[0]) return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton as={MyCustomButton}>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" as="ul"> {people.map((person) => (
<ListboxOption as="li" key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

要指示元素无包装元素直接呈现其子元素,请使用 Fragment

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { Fragment, useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton as={Fragment}>
<button>{selectedPerson.name}</button>
</ListboxButton>
<ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

虽然在构建自定义列表框时需要 ListboxButton 组件,但是可以按这样的方式构建列表框,使得按钮默认包含在内,因此你每次使用列表框时都不需要它。例如,如下所示的 API

<MyListbox name="status">
  <MyListboxOption value="active">Active</MyListboxOption>
  <MyListboxOption value="paused">Paused</MyListboxOption>
  <MyListboxOption value="delayed">Delayed</MyListboxOption>
  <MyListboxOption value="canceled">Canceled</MyListboxOption>
</MyListbox>

要实现此目的,请在你的 ListboxButton 内使用 ListboxSelectedOption 组件来呈现当前选中的列表框选项。

为此,你必须将自定义列表框的 children(所有 ListboxOption 实例)同时传递给 ListboxOptions 作为其子元素,同时通过 options 属性传递给 ListboxSelectedOption

然后,要根据 ListboxOptionListboxButton 中呈现还是在 ListboxOptions 中呈现,对其进行样式设置,请使用 selectedOption 渲染属性有条件地应用不同的样式或呈现不同的内容。

import { Listbox, ListboxButton, ListboxOption, ListboxOptions, ListboxSelectedOption } from '@headlessui/react'
import { Fragment, useState } from 'react'

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' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <MyListbox value={selectedPerson} onChange={setSelectedPerson} placeholder="Select a person&hellip;">
      {people.map((person) => (
        <MyListboxOption key={person.id} value={person}>
          {person.name}
        </MyListboxOption>
      ))}
    </MyListbox>
  )
}

function MyListbox({ placeholder, children, ...props }) {
  return (
    <Listbox {...props}>
      <ListboxButton>
<ListboxSelectedOption options={children} placeholder={<span className="opacity-50">{placeholder}</span>} />
</ListboxButton>
<ListboxOptions anchor="bottom">{children}</ListboxOptions>
</Listbox> ) } function MyListboxOption({ children, ...props }) { return ( <ListboxOption as={Fragment} {...props}>
{({ selectedOption }) => {
return selectedOption ? children : <div className="data-[focus]:bg-blue-100">{children}</div>
}}
</ListboxOption> ) }

ListboxSelectedOption 组件还具有 placeholder 属性,当未选择选项时,你可以使用此属性来呈现占位符。

命令描述

空格键向下箭头或者向上箭头ListboxButton 获得焦点时

打开列表框并聚焦选定的项目

回车键ListboxButton 获得焦点并且 Listbox 处于关闭状态时

提交父表单(如果存在)

Esc当列表框处于打开状态时

关闭列表框

ArrowDownArrowUp当列表框处于打开状态时

聚焦前一个/下一个未禁用的项目

ArrowLeftArrowRight当列表框处于打开状态并且设置了horizontal属性时

聚焦前一个/下一个未禁用的项目

HomePageUp当列表框处于打开状态时

聚焦第一个未禁用的项目

EndPageDown当列表框处于打开状态时

聚焦最后一个未禁用的项目

EnterSpace当列表框处于打开状态时

选择当前项目

A–Za–z当列表框处于打开状态时

聚焦与键盘输入匹配的第一个项目

主列表框组件。

属性默认值描述
asFragment
String | Component

元素或组件列表框应该呈现为。

invalidfalse
Boolean

是否列表框无效。

disabledfalse
Boolean

用它来禁用整个Listbox组件&相关子组件。

value
T

选定的值。

defaultValue
T

当控件设置为非受控组件时,为其使用的默认值。

by
keyof T | ((a: T, z: T) => boolean)

用它通过特定字段比较对象,也可以传入自有比较函数,对对象比较方式实行完全控制。

当将对象传递给 `value` prop 时,出现 `id` 时,`by` 将默认为 `id`。

onChange
(value: T) => void

当选中新的选项时,要调用的函数。

horizontalfalse
Boolean

为 true 时,`ListboxOptions` 的方向为 `horizontal`,否则为 `vertical`。

multiplefalse
Boolean

是否可以选择多个选项。

name
String

用于使用列表框时所采用的名称。

form
String

所属列表框表单的 id。

如果提供了 `name` 但未提供 `form`,列表框将向其最近的祖先 `form` 元素添加其状态。

数据属性Render Prop描述
value

T

选定的值。

data-openopen

Boolean

是否列表框处于打开状态。

data-invalidinvalid

Boolean

是否列表框无效。

data-disableddisabled

Boolean

是否列表框处于禁用状态。

Listbox 的按钮。

属性默认值描述
asbutton
String | Component

元素或组件listbox 按钮应该呈现为。

数据属性Render Prop描述
value

T

选定的值。

data-openopen

Boolean

是否列表框处于打开状态。

data-invalidinvalid

Boolean

是否列表框无效。

data-disableddisabled

Boolean

是否listbox 按钮处于禁用状态。

data-focusfocus

Boolean

是否listbox 按钮处于焦点状态。

data-hoverhover

Boolean

是否listbox 按钮处于悬停状态。

data-activeactive

Boolean

是否listbox 按钮处于活动或按压状态。

data-autofocusautofocus

Boolean

是否将 `autoFocus` prop 设为 `true`。

渲染当前选中的选项,如果未选中任何选项,则渲染占位符。设计为 `ListboxButton` 的子级。

属性默认值描述
asFragment
String | Component

元素或组件listbox 选中的选项应该呈现为。

placeholder
ReactNode

当未选中任何选项时,要渲染的 React 元素。

options
ReactNode[]

完整的 `ListboxOption` React 元素数组。`ListboxSelectedOption` 将过滤此列表,以查找并渲染当前选中的选项。

在自定义 Listbox 中直接包裹选项列表的组件。

属性默认值描述
asdiv
String | Component

元素或组件listbox 选项应该呈现为。

transitionfalse
Boolean

该元素是否应渲染转换属性,如 `data-closed`、 data-enter 和 `data-leave`。

anchor
Object

配置下拉菜单锚定到按钮的方式。

anchor.tobottom
String

位置listbox 选项相对触发因素。

使用值 `top`、`right`、`bottom`、`left` 使 listbox 选项沿着适当边缘居中,或将其与 `start` 或 `end` 结合使用,使listbox 选项到指定角落,如 top startbottom end

或者,使用 selection 选项直接将当前选定的选项置于上方listbox 按钮.

anchor.gap0
数字 | 字符串

之间的空间listbox 按钮listbox 选项.

还可以使用 --anchor-gap CSS 变量进行控制。

anchor.offset0
数字 | 字符串

之间的距离listbox 选项应从其原始位置轻推。

还可以使用 --anchor-offset CSS 变量进行控制。

anchor.padding0
数字 | 字符串

之间最小间隔listbox 选项和视口。

还可以使用 --anchor-padding CSS 变量进行控制。

静态false
Boolean

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

卸载true
Boolean

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

门户false
Boolean

元素是否应在门户中呈现。

在设置 anchor 属性时自动设置为 true

模态true
Boolean

是启用滚动锁定、焦点锁定等无障碍功能,以及使其他元素 无作用.

数据属性Render Prop描述
data-openopen

Boolean

是否列表框处于打开状态。

用于包装 Listbox 中的每个项。

属性默认值描述
asdiv
String | Component

元素或组件列表选项应该呈现为。

value
T

选项值。

disabledfalse
Boolean

是否列表选项被禁用用于键盘导航和 ARIA.

数据属性Render Prop描述
data-选择选择

Boolean

是否列表选项选择。

data-disableddisabled

Boolean

是否列表选项处于禁用状态。

data-focusfocus

Boolean

是否列表选项处于焦点状态。

data-selectedOptionselectedOption

Boolean

Listbox 选项是否是 ListboxSelectedOption 的子级。

如果您对使用 Headless UI 的预先设计的 Tailwind CSS 选择菜单和列表框示例 有兴趣,查看 Tailwind UI — 我们构建的一系列设计精良且制作精良的组件。

这是支持我们开展此类开源项目工作的好方法,可以让我们的项目得到完善,并保持良好的维护。