清單方塊
清單方塊是建置您的應用程式客製式可存取選單的絕佳基礎,並具備健全的鍵盤導覽支援。
開始時,透過 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 會追蹤每個組件的許多狀態,例如目前選擇哪一個清單匡選項、彈出視窗是否開啟或關閉,或使用鍵盤時目前對選單中的哪一個項目設有焦點。
然而由於組件是無頭的,而且開箱即用時完全沒有樣式,因此在您提供您想要的每一個狀態的樣式之前,在您的使用者介面中看不到這些資訊。
設定 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以獲取可用渲染屬性的清單。
使用 Field
元件包覆 Label
和 Listbox
,以使用產生的 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> )
}
新增 disabled
屬性至 Field
元件,以停用 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
元件外停用下拉選單,方法是將 disabled 屬性直接新增至 Listbox
本身。
使用 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 表單 或使用表單 API,而這些 API 會透過 FormData 來收集狀態,而不是使用 React 狀態來追蹤它。
只要元件的值變更,您提供的任何 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
寬度相符,請使用 --button-width
CSS 變數,而此變數會顯示在 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" 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
屬性實作方式與 Transition
元件完全相同。請參閱 Transition 文件 以深入了解。
無頭 UI 也能順利與 React 生態系統中的其他動畫函式庫結合,例如 Framer Motion 與 React Spring。您只需要公開這些函式庫的某些狀態即可。
例如,使用 Framer Motion 為列表方塊增加動畫效果,請將 static
屬性新增至 ListboxOptions
元件,然後根據 open
渲染屬性條件式渲染列表方塊
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 表單控制項(僅允許提供字串為值)不同,無頭 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
屬性,依照特定欄位比對物件而非比對物件身分。
當傳遞物件給 value
屬性時,若 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
屬性。
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
屬性,並將陣列傳遞給 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>
)
}
建立無按鈕 API
雖然建立自訂列表方塊時需要 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
元件來產生目前選取的列表方塊選項。
作品這樣做,您必須將自訂列表方塊的孩子(所有 ListboxOption
實例)同時傳遞給 ListboxOptions
(作為它的孩子),以及傳遞給 ListboxSelectedOption
(透過 options
屬性)。
然後,若要根據 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
屬性,當選項尚未選取時,可以使用該屬性來產生 placeholder。
鍵盤互動
指令 | 說明 |
Space, ArrowDown, or ArrowUp當 | 開啟列表方塊並聚焦於選取的項目 |
Enter當 | 送出父系表單(若存在的話) |
Esc當清單方塊開啟時 | 關閉清單方塊 |
ArrowDown或ArrowUp當清單方塊開啟時 | 聚焦於上一個/下一個未停用的項目 |
ArrowLeft或ArrowRight當清單方塊開啟且設定了 | 聚焦於上一個/下一個未停用的項目 |
Home或PageUp當清單方塊開啟時 | 聚焦於第一個未停用的項目 |
End或PageDown當清單方塊開啟時 | 聚焦於最後一個未停用的項目 |
Enter或Space當清單方塊開啟時 | 選取當前項目 |
A–Z或a–z當清單方塊開啟時 | 聚焦於與鍵盤輸入相符的第一個項目 |
屬性 | 預設 | 說明 |
as | 片段 | 字串 | 元件 元件或清單方塊應該作為呈現。 |
invalid | false | 布林值 如果清單方塊無效。 |
已停用 | false | 布林值 使用此選項停用整個 |
值 | — | T 已選取的值。 |
預設值 | — | T 將元件用作不受控元件時的預設值。 |
透過 | — | keyof T | ((a: T, z: T) => boolean) 使用此選項依特定欄位比較物件,或傳入您自己的比較函式,以完全控制物件的比較方式。 當您將物件傳遞給 |
onChange | — | (value: T) => void 選取新選項時要呼叫的函式。 |
水平 | false | 布林值 如果為 true, |
多選 | false | 布林值 是否允許選取多個選項。 |
名稱 | — | 字串 在表單中使用清單方塊時使用名稱。 |
表單 | — | 字串 表單 ID,清單方塊屬於該表單。 如果提供 |
資料屬性 | 呈現 prop | 說明 |
— | 值 |
已選取的值。 |
資料-開啟 | 開啟 |
如果清單方塊已開啟。 |
資料-invalid | invalid |
如果清單方塊無效。 |
資料-已停用 | 已停用 |
如果清單方塊已停用。 |
屬性 | 預設 | 說明 |
as | 按鈕 | 字串 | 元件 元件或listbox 按鈕應該作為呈現。 |
資料屬性 | 呈現 prop | 說明 |
— | 值 |
已選取的值。 |
資料-開啟 | 開啟 |
如果清單方塊已開啟。 |
資料-invalid | invalid |
如果清單方塊無效。 |
資料-已停用 | 已停用 |
如果listbox 按鈕已停用。 |
資料-焦點 | 焦點 |
如果listbox 按鈕已聚焦。 |
資料-滑鼠游標停留在上面 | 滑鼠游標停留在上面 |
如果listbox 按鈕已滑鼠游標停留在上面。 |
資料-動作中 | 動作中 |
如果listbox 按鈕處於動作中或已按下的狀態。 |
資料-自動對焦 | 自動對焦 |
|
屬性 | 預設 | 說明 |
as | 片段 | 字串 | 元件 元件或listbox 已選取選項應該作為呈現。 |
佔位符 | — | ReactNode 在沒有選取任何選項時要呈現的 React 元素。 |
選項 | — | ReactNode[] 您的完整 |
屬性 | 預設 | 說明 |
as | div | 字串 | 元件 元件或listbox 選項應該作為呈現。 |
轉場 | false | 布林值 元素是否應呈現轉場屬性,例如 |
錨點 | — | 物件 設定下拉式選單固定在按鈕上的方式。 |
anchor.to | 底部 | 字串 將listbox 選項定位在哪裡 使用值 Кроме этого, используйте параметр |
anchor.gap | 0 | Число | Строка Пространство междуlistbox 按鈕иlistbox 選項. также можно контролировать с помощью CSS-переменной |
anchor.offset | 0 | Число | Строка Расстояние, на котороеlistbox 選項следует сдвинуть с его изначального положения. Также можно контролировать с помощью CSS-переменной |
anchor.padding | 0 | Число | Строка Минимальное расстояние междуlistbox 選項и окном обзора. Можно также контролировать с помощью CSS-переменной |
static | false | 布林值 Должен ли элемент игнорировать внутренне управляемое состояние открыто/закрыто. |
unmount | true | 布林值 Должен ли элемент быть размонтирован или скрыт в зависимости от состояния открыто/закрыто. |
portal | false | 布林值 Должен ли элемент отображаться в портале. Автоматически устанавливается в |
modal | true | 布林值 Включить ли вспомогательные функции, такие как блокировка прокрутки, фиксация фокуса и превращение других элементов |
資料屬性 | 呈現 prop | 說明 |
資料-開啟 | 開啟 |
如果清單方塊已開啟。 |
屬性 | 預設 | 說明 |
as | div | 字串 | 元件 元件或listbox option應該作為呈現。 |
值 | — | T Значение варианта. |
已停用 | false | 布林值 如果listbox optionотключенодля навигации с клавиатуры и целей ARIA. |
資料屬性 | 呈現 prop | 說明 |
資料-выбрано | выбрано |
如果listbox optionвыбрано. |
資料-已停用 | 已停用 |
如果listbox option已停用。 |
資料-焦點 | 焦點 |
如果listbox option已聚焦。 |
資料-selectedOption | selectedOption |
Является ли опция listbox дочерним элементом |
Если вас интересуют заранее разработанные примеры селектора и выпадающего списка Tailwind CSS с использованием Headless UI, ознакомьтесь с Tailwind UI — коллекцией прекрасно разработанных и профессионально созданных компонентов, созданных нами.
Это отличный способ поддержать нашу работу над такими проектами с открытым исходным кодом и позволит нам улучшать их и поддерживать их надлежащим образом.