下拉菜单(自动完成)

下拉菜单是您应用程序中可访问的自动完成和命令面板的基础,并提供强大的键盘导航支持。

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

npm install @headlessui/react

下拉菜单使用 ComboboxCombobox.InputCombobox.ButtonCombobox.OptionsCombobox.OptionCombobox.Label 组件构建。

Combobox.Input 将在搜索时自动打开/关闭 Combobox.Options

您可以完全控制如何过滤结果,无论是使用模糊搜索库进行客户端操作,还是向 API 发出服务器端请求。在此示例中,我们将为了演示目的简化逻辑。

import { useState } from 'react' import { Combobox } from '@headlessui/react' const people = [ 'Durward Reynolds', 'Kenton Towne', 'Therese Wunsch', 'Benedict Kessler', 'Katelyn Rohan', ] function MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.toLowerCase().includes(query.toLowerCase()) }) return ( <Combobox value={selectedPerson} onChange={setSelectedPerson}> <Combobox.Input onChange={(event) => setQuery(event.target.value)} /> <Combobox.Options> {filteredPeople.map((person) => ( <Combobox.Option key={person} value={person}> {person} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

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

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

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

例如,Combobox.Option 组件公开了一个 active 状态,它告诉您选项当前是否通过鼠标或键盘获得焦点,以及一个 selected 状态,它告诉您该选项是否与 Combobox 的当前 value 相匹配。

import { useState, Fragment } from 'react' import { Combobox } from '@headlessui/react' import { CheckIcon } from '@heroicons/react/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' }, ] function MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return ( <Combobox value={selectedPerson} onChange={setSelectedPerson}> <Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} /> <Combobox.Options> {filteredPeople.map((person) => ( /* Use the `active` state to conditionally style the active option. */ /* Use the `selected` state to conditionally style the selected option. */ <Combobox.Option key={person.id} value={person} as={Fragment}>
{({ active, selected }) => (
<li className={`${
active ? 'bg-blue-500 text-white' : 'bg-white text-black'
}
`
}
>
{selected && <CheckIcon />}
{person.name} </li> )} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

有关每个组件的完整渲染道具 API,请参阅 组件 API 文档

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

渲染道具 API 中的任何状态为 true 时,它们将作为空格分隔的字符串列在这个属性中,因此您可以使用 CSS 属性选择器[attr~=value] 的形式定位它们。

例如,以下是如何渲染 Combobox.Options 组件,其中包含一些子 Combobox.Option 组件,当下拉菜单处于打开状态且第二个选项同时处于 selectedactive 状态时。

<!-- Rendered `Combobox.Options` --> <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:*)定位此属性。

import { useState, Fragment } from 'react' import { Combobox } from '@headlessui/react' import { CheckIcon } from '@heroicons/react/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' }, ] function MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return ( <Combobox value={selectedPerson} onChange={setSelectedPerson}> <Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} /> <Combobox.Options> {filteredPeople.map((person) => ( <Combobox.Option key={person.id} value={person}
className="ui-active:bg-blue-500 ui-active:text-white ui-not-active:bg-white ui-not-active:text-black"
>
<CheckIcon className="hidden ui-selected:block" />
{person.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

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

当绑定对象时,请确保在您的 Combobox.Input 上设置 displayValue,以便可以在输入中渲染所选选项的字符串表示形式。

import { useState } from 'react' import { Combobox } from '@headlessui/react'
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 },
]
function MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return (
<Combobox value={selectedPerson} onChange={setSelectedPerson}>
<Combobox.Input onChange={(event) => setQuery(event.target.value)}
displayValue={(person) => person.name}
/>
<Combobox.Options> {filteredPeople.map((person) => ( <Combobox.Option key={person.id}
value={person}
disabled={person.unavailable} >
{person.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

当绑定对象作为值时,务必确保将对象的同一实例用作 Comboboxvalue 以及相应的 Combobox.Option,否则它们将无法相等,导致下拉菜单行为不正确。

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

import { useState } from 'react' import { Combobox } 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 DepartmentPicker({ selectedDepartment, onChange }) {
const [query, setQuery] = useState('') const filteredDepartments = query === '' ? departments : departments.filter((department) => { return department.name.toLowerCase().includes(query.toLowerCase()) }) return (
<Combobox value={selectedDepartment} by="id" onChange={onChange}>
<Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(department) => department.name} /> <Combobox.Options> {filteredDepartments.map((department) => ( <Combobox.Option key={department.id} value={department}> {department.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

如果您希望完全控制对象比较方式,也可以将自己的比较函数传递给 by 道具。

import { useState } from 'react' import { Combobox } 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 DepartmentPicker({ selectedDepartment, onChange }) { const [query, setQuery] = useState('') const filteredDepartments = query === '' ? departments : departments.filter((department) => { return department.name.toLowerCase().includes(query.toLowerCase()) }) return ( <Combobox value={selectedDepartment}
by={compareDepartments}
onChange={onChange} >
<Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(department) => department.name} /> <Combobox.Options> {filteredDepartments.map((department) => ( <Combobox.Option key={department.id} value={department}> {department.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

要允许在下拉菜单中选择多个值,请使用 multiple 道具,并将数组而不是单个选项传递给 value

import { useState } from 'react' import { Combobox } 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 MyCombobox() {
const [selectedPeople, setSelectedPeople] = useState([people[0], people[1]])
return ( <Combobox value={selectedPeople} onChange={setSelectedPeople} multiple> {selectedPeople.length > 0 && ( <ul> {selectedPeople.map((person) => ( <li key={person.id}>{person.name}</li> ))} </ul> )}
<Combobox.Input />
<Combobox.Options> {people.map((person) => ( <Combobox.Option key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

displayValue 道具被省略,因为 selectedPeople 已经在输入框上方以列表形式显示。如果您希望在 Combobox.Input 中显示项目,那么 displayValue 将接收一个数组。

import { useState } from 'react' import { Combobox } 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 MyCombobox() {
const [selectedPeople, setSelectedPeople] = useState([people[0], people[1]])
return ( <Combobox value={selectedPeople} onChange={setSelectedPeople} multiple> <Combobox.Input
displayValue={(people) =>
people.map((person) => person.name).join(', ')
}
/>
<Combobox.Options> {people.map((person) => ( <Combobox.Option key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

这将使下拉菜单在您选择选项时保持打开状态,选择选项将在原位切换。

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

默认情况下,Combobox 将使用输入内容作为屏幕阅读器的标签。如果您希望更多地控制向辅助技术宣布的内容,请使用 Combobox.Label 组件。

import { useState } from 'react' import { Combobox } 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 MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return ( <Combobox value={selectedPerson} onChange={setSelectedPerson}>
<Combobox.Label>Assignee:</Combobox.Label>
<Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} /> <Combobox.Options> {filteredPeople.map((person) => ( <Combobox.Option key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

如果您在下拉菜单中添加 name 道具,则将渲染隐藏的 input 元素并与您的选择值保持同步。

import { useState } from 'react' import { Combobox } 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() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return ( <form action="/projects/1/assignee" method="post"> <Combobox value={selectedPerson} onChange={setSelectedPerson}
name="assignee"
>
<Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} /> <Combobox.Options> {filteredPeople.map((person) => ( <Combobox.Option key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> <button>Submit</button> </form> ) }

这使您可以在原生 HTML <form> 中使用下拉菜单,并进行传统的表单提交,就好像您的下拉菜单是原生 HTML 表单控件一样。

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

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

如果您向 Combobox 提供 defaultValue 道具而不是 value,Headless UI 将在内部为您跟踪其状态,使您能够将其用作 不受控组件

import { useState } from 'react' import { Combobox } 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() { const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return ( <form action="/projects/1/assignee" method="post">
<Combobox name="assignee" defaultValue={people[0]}>
<Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} /> <Combobox.Options> {filteredPeople.map((person) => ( <Combobox.Option key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> <button>Submit</button> </form> ) }

这可以在将下拉菜单与 HTML 表单 或使用通过 FormData 收集其状态而不是使用 React 状态跟踪其状态的表单 API 一起使用时简化您的代码。

您提供的任何 onChange 道具仍然会在组件的值发生更改时被调用,以防您需要运行任何副作用,但您不需要使用它来自己跟踪组件的状态。

您可以允许用户输入列表中不存在的自己的值,方法是根据 query 值包含一个动态 Combobox.Option

import { useState } from 'react' import { Combobox } 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() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return ( <Combobox value={selectedPerson} onChange={setSelectedPerson}> <Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} /> <Combobox.Options>
{query.length > 0 && (
<Combobox.Option value={{ id: null, name: query }}>
Create "{query}"
</Combobox.Option>
)}
{filteredPeople.map((person) => ( <Combobox.Option key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

根据您正在构建的内容,有时将有关活动选项的额外信息渲染在 <Combobox.Options> 之外可能很有意义。例如,在命令面板上下文中对活动选项进行预览。在这些情况下,您可以读取 activeOption 渲染道具参数来访问此信息。

import { useState } from 'react' import { Combobox } 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 MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return ( <Combobox value={selectedPerson} onChange={setSelectedPerson}>
{({ activeOption }) => (
<> <Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} /> <Combobox.Options> {filteredPeople.map((person) => ( <Combobox.Option key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options>
{activeOption && (
<div>The current active user is: {activeOption.name}</div>
)}
</> )} </Combobox> ) }

activeOption 将是当前活动 Combobox.Optionvalue

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

import { useState } from 'react' import { Combobox } 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 MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return ( <Combobox value={selectedPerson} onChange={setSelectedPerson}> <Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} /> {/* By default, the `Combobox.Options` will automatically show/hide when typing in the `Combobox.Input`, or when pressing the `Combobox.Button`. */} <Combobox.Options> {filteredPeople.map((person) => ( <Combobox.Option key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

如果您想自己处理此操作(也许是因为您出于某种原因需要添加额外的包装器元素),您可以向 Combobox.Options 实例添加 static 道具,告诉它始终渲染,并检查 Combobox 提供的 open 渲染道具,以控制要显示/隐藏的元素。

import { useState } from 'react' import { Combobox } 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 MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return ( <Combobox value={selectedPerson} onChange={setSelectedPerson}>
{({ open }) => (
<> <Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} />
{open && (
<div> {/* Using `static`, `Combobox.Options` are always rendered and the `open` state is ignored. */}
<Combobox.Options static>
{filteredPeople.map((person) => ( <Combobox.Option key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options> </div> )} </> )} </Combobox> ) }

使用 disabled 道具禁用 Combobox.Option。这将使它无法通过鼠标和键盘选择,并且在按下上/下箭头时会跳过它。

import { useState } from 'react' import { Combobox } from '@headlessui/react' 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 }, ] function MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return ( <Combobox value={selectedPerson} onChange={setSelectedPerson}> <Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} /> <Combobox.Options> {filteredPeople.map((person) => ( /* Disabled options will be skipped by keyboard navigation. */ <Combobox.Option key={person.id} value={person}
disabled={person.unavailable}
>
<span className={person.unavailable ? 'opacity-75' : ''}> {person.name} </span> </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

默认情况下,在下拉菜单中选择了一个值后,就无法将下拉菜单清除回空值 - 当您清除输入并跳过时,该值将返回到之前选择的值。

如果您想在下拉菜单中支持空值,请使用 nullable 道具。

import { useState } from 'react' import { Combobox } from '@headlessui/react' 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 }, ] function MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return (
<Combobox value={selectedPerson} onChange={setSelectedPerson} nullable>
<Combobox.Input onChange={(event) => setQuery(event.target.value)}
displayValue={(person) => person?.name}
/>
<Combobox.Options> {filteredPeople.map((person) => ( <Combobox.Option key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

使用 nullable 道具时,清除输入并从元素导航将使用 null 调用您的 onChangedisplayValue 回调。

当允许 多个值 时,此道具不会执行任何操作,因为选项会被打开和关闭,如果未选择任何选项,则会导致空数组(而不是 null)。

要为下拉框面板的打开/关闭添加动画,请使用提供的 Transition 组件。您只需将 Combobox.Options 包裹在 <Transition> 中,过渡动画就会自动应用。

import { useState } from 'react' import { Combobox, Transition } 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 MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return ( <Combobox value={selectedPerson} onChange={setSelectedPerson}> <Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} />
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Combobox.Options> {filteredPeople.map((person) => ( <Combobox.Option key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options>
</Transition>
</Combobox> ) }

默认情况下,我们内置的 Transition 组件会自动与 Combobox 组件通信以处理打开/关闭状态。但是,如果您需要对这种行为进行更多控制,您可以显式地进行控制。

import { useState } from 'react' import { Combobox, Transition } 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 MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return ( <Combobox value={selectedPerson} onChange={setSelectedPerson}>
{({ open }) => (
<>
<Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} /> {/* Use the `Transition` + `open` render prop argument to add transitions. */} <Transition
show={open}
enter="transition duration-100 ease-out" enterFrom="transform scale-95 opacity-0" enterTo="transform scale-100 opacity-100" leave="transition duration-75 ease-out" leaveFrom="transform scale-100 opacity-100" leaveTo="transform scale-95 opacity-0" >
{/* Don't forget to add `static` to your `Combobox.Options`! */}
<Combobox.Options static>
{filteredPeople.map((person) => ( <Combobox.Option key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options> </Transition>
</>
)}
</Combobox> ) }

由于它们是无渲染组件,Headless UI 组件也可以很好地与 React 生态系统中的其他动画库组合使用,例如 Framer MotionReact Spring

默认情况下,Combobox 及其子组件会渲染一个适合该组件的默认元素。

例如,Combobox.Label 默认情况下渲染一个 labelCombobox.Input 渲染一个 inputCombobox.Button 渲染一个 buttonCombobox.Options 渲染一个 ulCombobox.Option 渲染一个 li。相反,Combobox 不会渲染元素,而是直接渲染其子元素。

使用 as 属性将组件渲染为不同的元素或您自己的自定义组件,确保您的自定义组件 转发引用,以便 Headless UI 可以正确地连接它们。

import { forwardRef, useState } from 'react' import { Combobox } 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' }, ]
let MyCustomButton = forwardRef(function (props, ref) {
return <button className="..." ref={ref} {...props} />
})
function MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return (
<Combobox as="div" value={selectedPerson} onChange={setSelectedPerson}>
<Combobox.Input onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} /> <Combobox.Button
as={MyCustomButton}
>
Open
</Combobox.Button>
<Combobox.Options as="div">
{filteredPeople.map((person) => ( <Combobox.Option as="span" key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

要告诉元素直接渲染其子元素,不使用包装元素,请使用 Fragment

import { useState, Fragment } from 'react' import { Combobox } 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 MyCombobox() { const [selectedPerson, setSelectedPerson] = useState(people[0]) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter((person) => { return person.name.toLowerCase().includes(query.toLowerCase()) }) return ( <Combobox value={selectedPerson} onChange={setSelectedPerson}> {/* Render a `Fragment` instead of an `input` */} <Combobox.Input
as={Fragment}
onChange={(event) => setQuery(event.target.value)} displayValue={(person) => person.name} >
<input /> </Combobox.Input> <Combobox.Options> {filteredPeople.map((person) => ( <Combobox.Option key={person.id} value={person}> {person.name} </Combobox.Option> ))} </Combobox.Options> </Combobox> ) }

当下拉框打开时,Combobox.Input 会保持焦点。

Combobox.Button 会被默认的 Tab 流程忽略,这意味着在 Combobox.Input 中按 Tab 键会跳过 Combobox.Button

单击 Combobox.Button 会切换选项列表的打开和关闭。单击选项列表外部的任何地方都会关闭下拉框。

命令描述

向下箭头向上箭头Combobox.Input 获得焦点时

打开下拉框并使选定项目获得焦点

Enter空格向下箭头向上箭头Combobox.Button 获得焦点时

打开下拉框,使输入框获得焦点并选中选定项目

Esc 当下拉框打开时

关闭下拉框并恢复输入框中选定的项目

向下箭头向上箭头当下拉框打开时

使上一个/下一个非禁用项目获得焦点

HomePageUp 当下拉框打开时

使第一个非禁用项目获得焦点

EndPageDown 当下拉框打开时

使最后一个非禁用项目获得焦点

Enter 当下拉框打开时

选中当前项目

Enter 当下拉框关闭且在表单中时

提交表单

Tab 当下拉框打开时

选中当前激活项目并关闭下拉框

A–Za–z 当下拉框打开时

调用 onChange,允许您过滤列表

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

主下拉框组件。

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

Combobox 应渲染的元素或组件。

disabledfalse
布尔值

使用它禁用整个下拉框组件及其相关子元素。

value
T

选定的值。

defaultValue
T

使用作为不受控组件时的默认值。

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

使用它根据特定字段比较对象,或者传递您自己的比较函数来完全控制对象比较方式。

onChange
(value: T) => void

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

name
字符串

在表单中使用此组件时使用的名称。

nullable
布尔值

是否可以清除下拉框。

multiplefalse
布尔值

是否可以选中多个选项。

渲染属性描述
value

T

选定的值。

open

布尔值

下拉框是否打开。

disabled

布尔值

下拉框是否禁用。

activeIndex

数字 | null

活动选项的索引,如果没有任何活动选项则为 null。

activeOption

T | null

活动选项,如果没有任何活动选项则为 null。

下拉框的输入框。

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

Combobox.Input 应渲染的元素或组件。

displayValue
(item: T) => string

value 的字符串表示。

渲染属性描述
open

布尔值

下拉框是否打开。

disabled

布尔值

下拉框是否禁用。

下拉框的按钮。

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

Combobox.Button 应渲染的元素或组件。

渲染属性描述
value

T

选定的值。

open

布尔值

下拉框是否打开。

disabled

布尔值

下拉框是否禁用。

一个标签,可用于更精细地控制下拉框向屏幕阅读器宣布的文本。它的 id 属性将自动生成,并通过 aria-labelledby 属性与根 Combobox 组件链接。

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

Combobox.Label 应渲染的元素或组件。

渲染属性描述
open

布尔值

下拉框是否打开。

disabled

布尔值

下拉框是否禁用。

直接包装自定义下拉框中选项列表的组件。

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

Combobox.Options 应渲染的元素或组件。

staticfalse
布尔值

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

注意:staticunmount 不能同时使用。如果您尝试这样做,会收到 TypeScript 错误。

unmounttrue
布尔值

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

注意:staticunmount 不能同时使用。如果您尝试这样做,会收到 TypeScript 错误。

holdfalse
布尔值

活动选项是否应该在鼠标离开活动选项时仍然保持活动状态。

渲染属性描述
open

布尔值

下拉框是否打开。

用于包装下拉框中的每个项目。

属性默认值描述
value
T

选项值。

asli
字符串 | 组件

Combobox.Option 应渲染的元素或组件。

disabledfalse
布尔值

选项是否应该被禁用,以供键盘导航和 ARIA 使用。

渲染属性描述
active

布尔值

选项是否为活动/焦点选项。

selected

布尔值

选项是否为选中选项。

disabled

布尔值

选项是否被禁用,以供键盘导航和 ARIA 使用。

如果您对使用 Headless UI 和 Tailwind CSS 的预先设计组件示例感兴趣,请查看 **Tailwind UI** - 由我们精心打造的一系列精美设计和专业制作的组件。

这是一种支持我们开源项目工作的好方法,使我们能够改进它们并保持良好维护。