下拉菜单(自动完成)
下拉菜单是您应用程序中可访问的自动完成和命令面板的基础,并提供强大的键盘导航支持。
要开始使用,请通过 npm 安装 Headless UI
npm install @headlessui/react
下拉菜单使用 Combobox
、Combobox.Input
、Combobox.Button
、Combobox.Options
、Combobox.Option
和 Combobox.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
组件,当下拉菜单处于打开状态且第二个选项同时处于 selected
和 active
状态时。
<!-- 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> ) }
当绑定对象作为值时,务必确保将对象的同一实例用作 Combobox
的 value
以及相应的 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.InputdisplayValue={(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.Option
的 value
。
默认情况下,您的 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
调用您的 onChange
和 displayValue
回调。
当允许 多个值 时,此道具不会执行任何操作,因为选项会被打开和关闭,如果未选择任何选项,则会导致空数组(而不是 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} />
<Transitionenter="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. */} <Transitionshow={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 Motion 和 React Spring。
默认情况下,Combobox
及其子组件会渲染一个适合该组件的默认元素。
例如,Combobox.Label
默认情况下渲染一个 label
,Combobox.Input
渲染一个 input
,Combobox.Button
渲染一个 button
,Combobox.Options
渲染一个 ul
,Combobox.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.Buttonas={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
会切换选项列表的打开和关闭。单击选项列表外部的任何地方都会关闭下拉框。
命令 | 描述 |
向下箭头, 或 向上箭头当 | 打开下拉框并使选定项目获得焦点 |
Enter, 空格, 向下箭头, 或 向上箭头当 | 打开下拉框,使输入框获得焦点并选中选定项目 |
Esc 当下拉框打开时 | 关闭下拉框并恢复输入框中选定的项目 |
向下箭头或 向上箭头当下拉框打开时 | 使上一个/下一个非禁用项目获得焦点 |
Home或 PageUp 当下拉框打开时 | 使第一个非禁用项目获得焦点 |
End或 PageDown 当下拉框打开时 | 使最后一个非禁用项目获得焦点 |
Enter 当下拉框打开时 | 选中当前项目 |
Enter 当下拉框关闭且在表单中时 | 提交表单 |
Tab 当下拉框打开时 | 选中当前激活项目并关闭下拉框 |
A–Z或 a–z 当下拉框打开时 | 调用 |
属性 | 默认值 | 描述 |
as | Fragment | 字符串 | 组件
|
disabled | false | 布尔值 使用它禁用整个下拉框组件及其相关子元素。 |
value | — | T 选定的值。 |
defaultValue | — | T 使用作为不受控组件时的默认值。 |
by | — | keyof T | ((a: T, z: T) => boolean) 使用它根据特定字段比较对象,或者传递您自己的比较函数来完全控制对象比较方式。 |
onChange | — | (value: T) => void 选中新选项时要调用的函数。 |
name | — | 字符串 在表单中使用此组件时使用的名称。 |
nullable | — | 布尔值 是否可以清除下拉框。 |
multiple | false | 布尔值 是否可以选中多个选项。 |
渲染属性 | 描述 |
value |
选定的值。 |
open |
下拉框是否打开。 |
disabled |
下拉框是否禁用。 |
activeIndex |
活动选项的索引,如果没有任何活动选项则为 null。 |
activeOption |
活动选项,如果没有任何活动选项则为 null。 |
属性 | 默认值 | 描述 |
as | input | 字符串 | 组件
|
displayValue | — | (item: T) => string
|
渲染属性 | 描述 |
open |
下拉框是否打开。 |
disabled |
下拉框是否禁用。 |
属性 | 默认值 | 描述 |
as | button | 字符串 | 组件
|
渲染属性 | 描述 |
value |
选定的值。 |
open |
下拉框是否打开。 |
disabled |
下拉框是否禁用。 |
属性 | 默认值 | 描述 |
as | label | 字符串 | 组件
|
渲染属性 | 描述 |
open |
下拉框是否打开。 |
disabled |
下拉框是否禁用。 |
属性 | 默认值 | 描述 |
as | ul | 字符串 | 组件
|
static | false | 布尔值 元素是否应该忽略内部管理的打开/关闭状态。 注意: |
unmount | true | 布尔值 元素是否应该根据打开/关闭状态卸载或隐藏。 注意: |
hold | false | 布尔值 活动选项是否应该在鼠标离开活动选项时仍然保持活动状态。 |
渲染属性 | 描述 |
open |
下拉框是否打开。 |
属性 | 默认值 | 描述 |
value | — | T 选项值。 |
as | li | 字符串 | 组件
|
disabled | false | 布尔值 选项是否应该被禁用,以供键盘导航和 ARIA 使用。 |
渲染属性 | 描述 |
active |
选项是否为活动/焦点选项。 |
selected |
选项是否为选中选项。 |
disabled |
选项是否被禁用,以供键盘导航和 ARIA 使用。 |