列表框
列表框是为您的应用构建定制的可访问选择菜单的绝佳基础,并完全支持键盘导航。
要开始使用,请通过 npm 安装 Headless UI
npm install @headlessui/react
列表框使用 Listbox
、ListboxButton
、ListboxSelectedOption
、ListboxOptions
和 ListboxOption
组件构建。
当单击 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 以获取所有可用渲染属性的列表。
将 Label
和 Listbox
用 Field
组件包装起来,使用一个生成的 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
及其关联的 Label
和 Description
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>
)
}
使用值 top
、right
、bottom
或 left
将下拉菜单居中对齐到适当的边缘,或将其与 start
或 end
组合,以将下拉菜单对齐到特定角,例如 top start
或 bottom 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 控制 gap
、offset
和 padding
值
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 Motion 和 React 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>
)
}
将对象绑定为值时,务必确保你将对象的同一个实例用作 Listbox
的 value
以及对应的 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
呈现一个 button
,ListboxOptions
呈现一个 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
。
然后,要根据 ListboxOption
在 ListboxButton
中呈现还是在 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…">
{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
属性,当未选择选项时,你可以使用此属性来呈现占位符。
命令 | 描述 |
空格键、向下箭头、或者向上箭头当 | 打开列表框并聚焦选定的项目 |
回车键当 | 提交父表单(如果存在) |
Esc当列表框处于打开状态时 | 关闭列表框 |
ArrowDown或ArrowUp当列表框处于打开状态时 | 聚焦前一个/下一个未禁用的项目 |
ArrowLeft或ArrowRight当列表框处于打开状态并且设置了 | 聚焦前一个/下一个未禁用的项目 |
Home或PageUp当列表框处于打开状态时 | 聚焦第一个未禁用的项目 |
End或PageDown当列表框处于打开状态时 | 聚焦最后一个未禁用的项目 |
Enter或Space当列表框处于打开状态时 | 选择当前项目 |
A–Z或a–z当列表框处于打开状态时 | 聚焦与键盘输入匹配的第一个项目 |
属性 | 默认值 | 描述 |
as | Fragment | String | Component 元素或组件列表框应该呈现为。 |
invalid | false | Boolean 是否列表框无效。 |
disabled | false | Boolean 用它来禁用整个 |
value | — | T 选定的值。 |
defaultValue | — | T 当控件设置为非受控组件时,为其使用的默认值。 |
by | — | keyof T | ((a: T, z: T) => boolean) 用它通过特定字段比较对象,也可以传入自有比较函数,对对象比较方式实行完全控制。 当将对象传递给 `value` prop 时,出现 `id` 时,`by` 将默认为 `id`。 |
onChange | — | (value: T) => void 当选中新的选项时,要调用的函数。 |
horizontal | false | Boolean 为 true 时,`ListboxOptions` 的方向为 `horizontal`,否则为 `vertical`。 |
multiple | false | Boolean 是否可以选择多个选项。 |
name | — | String 用于使用列表框时所采用的名称。 |
form | — | String 所属列表框表单的 id。 如果提供了 `name` 但未提供 `form`,列表框将向其最近的祖先 `form` 元素添加其状态。 |
数据属性 | Render Prop | 描述 |
— | value |
选定的值。 |
data-open | open |
是否列表框处于打开状态。 |
data-invalid | invalid |
是否列表框无效。 |
data-disabled | disabled |
是否列表框处于禁用状态。 |
属性 | 默认值 | 描述 |
as | button | String | Component 元素或组件listbox 按钮应该呈现为。 |
数据属性 | Render Prop | 描述 |
— | value |
选定的值。 |
data-open | open |
是否列表框处于打开状态。 |
data-invalid | invalid |
是否列表框无效。 |
data-disabled | disabled |
是否listbox 按钮处于禁用状态。 |
data-focus | focus |
是否listbox 按钮处于焦点状态。 |
data-hover | hover |
是否listbox 按钮处于悬停状态。 |
data-active | active |
是否listbox 按钮处于活动或按压状态。 |
data-autofocus | autofocus |
是否将 `autoFocus` prop 设为 `true`。 |
属性 | 默认值 | 描述 |
as | Fragment | String | Component 元素或组件listbox 选中的选项应该呈现为。 |
placeholder | — | ReactNode 当未选中任何选项时,要渲染的 React 元素。 |
options | — | ReactNode[] 完整的 `ListboxOption` React 元素数组。`ListboxSelectedOption` 将过滤此列表,以查找并渲染当前选中的选项。 |
属性 | 默认值 | 描述 |
as | div | String | Component 元素或组件listbox 选项应该呈现为。 |
transition | false | Boolean 该元素是否应渲染转换属性,如 `data-closed`、 |
anchor | — | Object 配置下拉菜单锚定到按钮的方式。 |
anchor.to | bottom | String 位置listbox 选项相对触发因素。 使用值 `top`、`right`、`bottom`、`left` 使 listbox 选项沿着适当边缘居中,或将其与 `start` 或 `end` 结合使用,使listbox 选项到指定角落,如 或者,使用 |
anchor.gap | 0 | 数字 | 字符串 之间的空间listbox 按钮和listbox 选项. 还可以使用 |
anchor.offset | 0 | 数字 | 字符串 之间的距离listbox 选项应从其原始位置轻推。 还可以使用 |
anchor.padding | 0 | 数字 | 字符串 之间最小间隔listbox 选项和视口。 还可以使用 |
静态 | false | Boolean 元素是否应忽略内部管理的打开/关闭状态。 |
卸载 | true | Boolean 元素是否应根据打开/关闭状态卸载或隐藏。 |
门户 | false | Boolean 元素是否应在门户中呈现。 在设置 |
模态 | true | Boolean 是启用滚动锁定、焦点锁定等无障碍功能,以及使其他元素 |
数据属性 | Render Prop | 描述 |
data-open | open |
是否列表框处于打开状态。 |
属性 | 默认值 | 描述 |
as | div | String | Component 元素或组件列表选项应该呈现为。 |
value | — | T 选项值。 |
disabled | false | Boolean 是否列表选项被禁用用于键盘导航和 ARIA. |
数据属性 | Render Prop | 描述 |
data-选择 | 选择 |
是否列表选项选择。 |
data-disabled | disabled |
是否列表选项处于禁用状态。 |
data-focus | focus |
是否列表选项处于焦点状态。 |
data-selectedOption | selectedOption |
Listbox 选项是否是 |
如果您对使用 Headless UI 的预先设计的 Tailwind CSS 选择菜单和列表框示例 有兴趣,查看 Tailwind UI — 我们构建的一系列设计精良且制作精良的组件。
这是支持我们开展此类开源项目工作的好方法,可以让我们的项目得到完善,并保持良好的维护。