列表方塊(選取)
列表方塊是為您的應用程式建立自訂無障礙選單的絕佳基礎,並提供健全的鍵盤導覽支援。
首先,請透過 npm 安裝 Headless UI。
請注意,此程式庫僅支援 Vue 3。
npm install @headlessui/vue
列表方塊是由 Listbox
、ListboxButton
、ListboxOptions
、ListboxOption
和 ListboxLabel
元件組成。
當按一下 ListboxButton
時,它會自動開啟/關閉 ListboxOptions
,而且當選單開啟時,項目清單會接收焦點,並可自動透過鍵盤導覽。
<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person" :disabled="person.unavailable" > {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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 }, ] const selectedPerson = ref(people[0]) </script>
Headless UI 會追蹤每個元件的許多狀態,例如目前選取的列表方塊選項是什麼、快顯視窗是開啟還是關閉,或是目前透過鍵盤在列表方塊中的哪個項目是作用中的。
但由於元件是無頭的且出廠時完全沒有樣式,因此在您為每個狀態提供所需的樣式之前,您無法在您的使用者介面上看到此資訊。
每個元件會透過 插槽屬性公開其目前狀態的資訊,您可以使用這些屬性有條件地套用不同樣式或呈現不同內容。
例如,ListboxOption
元件會公開一個 active
狀態,它會告訴您目前是否透過滑鼠或鍵盤讓項目獲得焦點。
<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <!-- Use the `active` state to conditionally style the active option. --> <!-- Use the `selected` state to conditionally style the selected option. --> <ListboxOption v-for="person in people" :key="person.id" :value="person" as="template"
v-slot="{ active, selected }"> <li :class="{'bg-blue-500 text-white': active,'bg-white text-black': !active,}" ><CheckIcon v-show="selected" />{{ person.name }} </li> </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' import { CheckIcon } from '@heroicons/vue/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' }, ] const selectedPerson = ref(people[0]) </script>
要取得全部可用的插槽屬性的完整清單,請參閱 元件 API 文件。
每個元件透過 `data-headlessui-state` 屬性公開目前狀態的資訊,你可以使用該屬性以有條件地套用不同的樣式。
當 插槽屬性 API 中任何狀態為 `true` 時,這些狀態會以空白分隔字串的形式列在此一屬性中,因此你可以使用 `[attr~=value]` 形式的 CSS 屬性選取器 來鎖定它們。
例如,以下是在清單方塊開啟以及第二個項目為 `active` 時,`ListboxOptions` 元件與一些子 `ListboxOption` 元件的呈現
<!-- Rendered `ListboxOptions` --> <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:*` 來鎖定這個屬性
<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"
class="ui-active:bg-blue-500 ui-active:text-white ui-not-active:bg-white ui-not-active:text-black"><CheckIcon class="hidden ui-selected:block" />{{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' import { CheckIcon } from '@heroicons/vue/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' }, ] const selectedPerson = ref(people[0]) </script>
與僅允許你提供字串做為值的原生 HTML 表單控制項不同的是,Headless UI 也支援繫結複雜物件。
<template>
<Listbox v-model="selectedPerson"><ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id":value="person":disabled="person.unavailable" > {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue'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 },]const selectedPerson = ref(people[1]) </script>
在將物件繫結為值時,務必確保使用同一物件的同一個實例做為 `Listbox` 的 `value`,以及對應的 `ListboxOption`,否則它們會無法相等並且會導致清單方塊行為不正確。
為了簡化使用同一物件的不同實例,你可以使用 `by` 屬性,根據特定欄位來比較物件,而不是比較物件身分
<template> <Listbox :modelValue="modelValue" @update:modelValue="value => emit('update:modelValue', value)"
by="id"> <ListboxButton>{{ modelValue.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="department in departments" :key="department.id" :value="department" > {{ department.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const props = defineProps({ modelValue: Object }) const emit = defineEmits(['update:modelValue']) 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' }, ] </script>
你也可以傳遞你自己的比較函式給 `by` 屬性,如果你想要完全控制物件的比較方式
<template> <Listbox :modelValue="modelValue" @update:modelValue="value => emit('update:modelValue', value)"
:by="compareDepartments"> <ListboxButton>{{ modelValue.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="department in departments" :key="department.id" :value="department" > {{ department.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const props = defineProps({ modelValue: Object }) const emit = defineEmits(['update:modelValue'])function compareDepartments(a, b) {return a.name.toLowerCase() === b.name.toLowerCase()}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' }, ] </script>
要在清單方塊中允許多重值選取,請使用 `multiple` 屬性並傳遞一個陣列給 `v-model`,而不是單一選項。
<template>
<Listbox v-model="selectedPeople" multiple><ListboxButton> {{ selectedPeople.map((person) => person.name).join(', ') }} </ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ]const selectedPeople = ref([people[0], people[1]])</script>
這將在選取選項時讓清單方塊保持開啟,而選取選項會就地切換。
只要新增或移除選項,`v-model` 繫結就會更新,會包含所有已選選項的陣列。
預設情況下,Listbox
會將按鈕內容作為螢幕閱讀器的標籤。如果您想進一步控制公告給輔助技術,請使用 ListboxLabel
元件。
<template> <Listbox v-model="selectedPerson">
<ListboxLabel>Assignee:</ListboxLabel><ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] const selectedPerson = ref(people[0]) </script>
如果您將 name
道具加入您的下拉選單,系統將會產生隱藏的 input
元素,並與您選擇的數值同步。
<template> <form action="/projects/1/assignee" method="post">
<Listbox v-model="selectedPerson" name="assignee"><ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> <button>Submit</button> </form> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] const selectedPerson = ref(people[0]) </script>
這讓您可以使用 HTML <form>
內部的下拉選單進行傳統的表單提交,就好像您的下拉選單是本機 HTML 表單控制一樣。
像字串這類的基本值將會產生一個單一的隱藏輸入,包含該數值,但像物件這類的複雜值會使用中括弧表示法編碼到多個輸入中,作為名稱。
<input type="hidden" name="assignee[id]" value="1" /> <input type="hidden" name="assignee[name]" value="Durward Reynolds" />
如果您對 Listbox
提供的 defaultValue
道具而不是對 value
提供,Headless UI 會為您在內部追蹤其狀態,讓您能將其作為非受控元件使用。
您可以透過 Listbox
和 ListboxButton
元件的 value
區段道具存取目前選取的選項。
<template> <form action="/projects/1/assignee" method="post">
<Listbox name="assignee" :defaultValue="people[0]"><ListboxButton v-slot="{ value }">{{ value.name }}</ListboxButton><ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> <button>Submit</button> </form> </template> <script setup> import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] </script>
當您將下拉選單與HTML 表單 搭配使用,或是與使用FormData 而非使用 React 狀態追蹤其狀態的表單 API 搭配使用時,這可以簡化您的程式碼。
您提供的任何 @update:modelValue
道具,仍會在元件的數值改變時被呼叫,以防您需要執行任何副作用,但您不需要用它自己追蹤元件的狀態。
預設情況下,您的 ListboxOptions
執行個體會根據 Listbox
元件本身內追蹤的內部 open
狀態來自行顯示/隱藏。
<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <!-- By default, the `ListboxOptions` will automatically show/hide when the `ListboxButton` is pressed. --> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { name: 'Durward Reynolds' }, { name: 'Kenton Towne' }, { name: 'Therese Wunsch' }, { name: 'Benedict Kessler' }, { name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>
如果您希望自己處理這件事(可能因為您需要額外包覆元件),您可以將 static
道具加入到 ListboxOptions
執行個體,告訴其始終要進行顯示,並檢查 Listbox
提供的 open
區段道具自己控制哪個元素顯示/隱藏。
<template>
<Listbox v-model="selectedPerson" v-slot="{ open }"><ListboxButton>{{ selectedPerson.name }}</ListboxButton><div v-show="open"><!-- Using the `static` prop, the `ListboxOptions` are always rendered and the `open` state is ignored. --><ListboxOptions static><ListboxOption v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </div> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { name: 'Durward Reynolds' }, { name: 'Kenton Towne' }, { name: 'Therese Wunsch' }, { name: 'Benedict Kessler' }, { name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>
使用 disabled
prop 以停用 ListboxOption
。這將使它無法透過滑鼠和鍵盤選取,並且在按下上下箭頭時將會略過它。
<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <!-- Disabled options will be skipped by keyboard navigation. --> <ListboxOption v-for="person in people" :key="person.name" :value="person"
:disabled="person.unavailable"> <span :class='{ "opacity-75": person.unavailable }'> {{ person.name }} </span> </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { name: 'Durward Reynolds', unavailable: true }, { name: 'Kenton Towne', unavailable: false }, { name: 'Therese Wunsch', unavailable: false }, { name: 'Benedict Kessler', unavailable: true }, { name: 'Katelyn Rohan', unavailable: false }, ] const selectedPerson = ref(people[0]) </script>
若要為列表方塊的開啟/關閉加上動畫效果,請使用 Vue 內建的 <transition>
組件。你所需要做的,就是將你的 ListboxOptions
實體包裝在 <transition>
中,這樣轉場就會自動套用。
<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <!-- Use Vue's built-in `transition` component to add transitions. -->
<transitionenter-active-class="transition duration-100 ease-out"enter-from-class="transform scale-95 opacity-0"enter-to-class="transform scale-100 opacity-100"leave-active-class="transition duration-75 ease-out"leave-from-class="transform scale-100 opacity-100"leave-to-class="transform scale-95 opacity-0"><ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </transition> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] const selectedPerson = ref(people[0]) </script>
若要為列表方塊中不同子項協調多個轉場,請查看 Headless UI 中包含的轉場組件。
預設情況下,Listbox
及其子組件各自呈現一個對於該組件具有意義的預設元素。
例如,ListboxLabel
預設呈現一個 label
,ListboxButton
呈現一個 button
,ListboxOptions
呈現一個 ul
,而 ListboxOption
呈現一個 li
。相比之下,Listbox
不會呈現元素,而是直接呈現它的子項。
這很容易使用存在於每個組件上的 as
prop 來改變。
<template> <!-- Render a `div` instead of nothing -->
<Listbox as="div" v-model="selectedPerson"><ListboxButton>{{ selectedPerson.name }}</ListboxButton> <!-- Render a `div` instead of a `ul` --><ListboxOptions as="div"><!-- Render a `span` instead of a `li` --> <ListboxOptionas="span"v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] const selectedPerson = ref(people[0]) </script>
要指示某個元素直接呈現其子項,而不使用包覆元素,請使用 as="template"
。
<template> <Listbox v-model="selectedPerson"> <!-- Render children directly instead of a `ListboxButton` -->
<ListboxButton as="template"><button>{{ selectedPerson.name }}</button> </ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] const selectedPerson = ref(people[0]) </script>
如果你已經將 ListboxOptions
的樣式設定為水平顯示,請在 Listbox
組件上使用 horizontal
prop,以啟用使用左右箭頭鍵來瀏覽項目(取代上下鍵),並更新輔助技術的 aria-orientation
屬性。
<template>
<Listbox v-model="selectedPerson" horizontal><ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions class="flex flex-row"> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] const selectedPerson = ref(people[0]) </script>
當開啟列表方塊時,ListboxOptions
將接收焦點。焦點將停留在項目清單中,直到按下 Escape 或使用者按一下選項以外的位置。關閉列表方塊會將焦點返回到 ListboxButton
。
按一下 ListboxButton
會切換選項清單開啟和關閉狀態。在選項清單以外的任何位置按一下都會關閉框選按鈕。
指令 | 說明 |
Enter、空白鍵、向下鍵、或向上鍵在 | 開啟框選按鈕並將焦點放在已選取項目 |
Esc 在框選按鈕開啟時 | 關閉框選按鈕 |
向下鍵或向上鍵在框選按鈕開啟時 | 將焦點放在上一個/下一個未停用的項目 |
左箭頭或右箭頭在框選按鈕開啟且設定 | 將焦點放在上一個/下一個未停用的項目 |
Home或PageUp 在框選按鈕開啟時 | 將焦點放在第一個未停用的項目 |
結束或向下翻頁,清單方塊開啟時可用 | 聚焦最後一個未停用的項目 |
Enter或空白鍵,清單方塊開啟時可用 | 選取目前的項目 |
A–Z或a–z,清單方塊開啟時可用 | 聚焦與鍵盤輸入相符的第一個項目 |
屬性 | 預設值 | 說明 |
as | 範本 | 字串 | 元件
|
v-model | — | T 選取的值。 |
defaultValue | — | T 作為不受控制元件使用時預設值。 |
by | — | keyof T | ((a: T, z: T) => boolean) 使用此方法可透過特定欄位來比較物件,或傳遞自己的比較函式,以完全控制物件的比較方式。 |
disabled | false | 布林值 使用此方法可停用整個 Listbox 元件與相關的子項目。 |
horizontal | false | 布林值 若為 true, |
name | — | 字串 在此元件於表單中使用時使用的名稱。 |
multiple | false | 布林值 是否容許選取多個選項。 |
插槽屬性 | 說明 |
value |
選取的值。 |
open |
Listbox 是否開啟。 |
disabled |
Listbox 是否停用。 |
屬性 | 預設值 | 說明 |
as | button | 字串 | 元件
|
插槽屬性 | 說明 |
value |
選取的值。 |
open |
Listbox 是否開啟。 |
disabled |
Listbox 是否停用。 |
屬性 | 預設值 | 說明 |
as | 標籤 | 字串 | 元件
|
插槽屬性 | 說明 |
open |
Listbox 是否開啟。 |
disabled |
Listbox 是否停用。 |
屬性 | 預設值 | 說明 |
as | ul | 字串 | 元件
|
靜態 | false | 布林值 元素是否應忽略內部管理的開啟/關閉狀態。 |
卸載 | 正確 | 布林值 元素應根據開啟/關閉狀態卸載或隱藏。 |
插槽屬性 | 說明 |
open |
Listbox 是否開啟。 |
屬性 | 預設值 | 說明 |
value | — | T 選項值。 |
as | li | 字串 | 元件
|
disabled | false | 布林值 選項是否應針對鍵盤導覽和 ARIA 目的停用。 |
插槽屬性 | 說明 |
主動 |
選項是否為主動/焦點選項。 |
已選取 |
選項是否為已選取選項。 |
disabled |
選項是否應針對鍵盤導覽和 ARIA 目的停用。 |