リストボックス
リストボックスは、キーボードナビゲーションの堅牢なサポートを備えた、カスタムでアクセス可能なアプリのセレクトメニューを構築するための優れた基盤です。
まず、npm を使って Headless UI をインストールします。
npm install @headlessui/react
リストボックスは、Listbox
、ListboxButton
、ListboxSelectedOption
、ListboxOptions
、および ListboxOption
のコンポーネントを使用して構築されています。
ListboxButton
は、クリックされたときに自動的に ListboxOptions
を開閉し、リストボックスが開いているときは、オプションのリストがフォーカスを受け取り、キーボードで自動的にナビゲートできます。
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
Headless UI は、どのリストボックスのオプションが現在選択されているか、ポップオーバーが開いているか閉じているか、メニューのどの項目がキーボードで現在フォーカスされているかなど、各コンポーネントに関する多くの状態を追跡します。
ただし、コンポーネントはヘッドレスであり、初期状態では完全にスタイル設定されていないため、各状態に必要なスタイルを自分で指定するまで、UI でこの情報を確認することはできません。
Headless UI コンポーネントのさまざまな状態をスタイル設定する最も簡単な方法は、各コンポーネントが公開する data-*
属性を使用することです。
たとえば、ListboxOption
コンポーネントは、オプションが現在マウスまたはキーボードでフォーカスされているかどうかを示す data-focus
属性と、オプションが Listbox
の現在の value
と一致するかどうかを示す data-selected
属性を公開します。
<!-- 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
状態と、オプションが Listbox
の現在の value
と一致するかどうかを示す selected
状態を公開します。
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { CheckIcon } from '@heroicons/react/20/solid'
import clsx from 'clsx'
import { Fragment, useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} as={Fragment}> {({ focus, selected }) => ( <div className={clsx('flex gap-2', focus && 'bg-blue-100')}> <CheckIcon className={clsx('size-5', !selected && 'invisible')} /> {person.name} </div> )} </ListboxOption> ))}
</ListboxOptions>
</Listbox>
)
}
利用可能なすべてのレンダープロップのリストについては、コンポーネント APIを参照してください。
Label
と Listbox
を Field
コンポーネントでラップして、生成された ID を使用して自動的に関連付けます。
import { Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Field> <Label>Assignee:</Label> <Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
</Field> )
}
Field
内で Description
コンポーネントを使用して、aria-describedby
属性を使用して Listbox
と自動的に関連付けます。
import { Description, Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Field> <Label>Assignee:</Label>
<Description>This person will have full access to this project.</Description> <Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
</Field> )
}
Listbox
と関連付けられた Label
および Description
を無効にするには、disabled
プロップを Field
コンポーネントに追加します。
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>
)
}
Listbox
自体に disabled プロップを直接追加して、Field
の外部でリストボックスを無効にすることもできます。
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 フォームで使用する場合や、React の状態を使用して追跡するのではなく、FormData を使用して状態を収集するフォーム API で使用する場合に、コードを簡素化できます。
サイドエフェクトを実行する必要がある場合に備えて、コンポーネントの値が変更されたときに、指定した onChange
プロップは引き続き呼び出されますが、コンポーネントの状態を自分で追跡するために使用する必要はありません。
ListboxOptions
ドロップダウンには、デフォルトでは幅が設定されていませんが、CSS を使用して追加できます。
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" className="w-52"> {people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
ドロップダウンの幅を ListboxButton
の幅と一致させたい場合は、ListboxOptions
要素で公開されている --button-width
CSS 変数を使用します。
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" className="w-[var(--button-width)]"> {people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
ListboxButton
を基準にしてドロップダウンを自動的に配置するには、anchor
プロップを 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 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
プロップは、JavaScript を使用して gap
、offset
、および padding
の値を制御できるオブジェクト API もサポートしています。
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>
)
}
リストボックスのドロップダウンの開閉をアニメーション化するには、ListboxOptions
コンポーネントに transition
プロパティを追加し、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 のドキュメントを参照してください。
Headless UI は、Framer Motion や React Spring のような React エコシステムの他のアニメーションライブラリともうまく連携します。これらのライブラリにいくつかの状態を公開するだけで済みます。
たとえば、Framer Motion でリストボックスをアニメーション化するには、ListboxOptions
コンポーネントに static
プロパティを追加し、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 フォームコントロールとは異なり、Headless UI は複雑なオブジェクトのバインドもサポートしています。
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [ { id: 1, name: 'Durward Reynolds' }, { id: 2, name: 'Kenton Towne' }, { id: 3, name: 'Therese Wunsch' }, { id: 4, name: 'Benedict Kessler' }, { id: 5, name: 'Katelyn Rohan' },]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name} </ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
オブジェクトを値としてバインドする場合、Listbox
の value
と対応する ListboxOption
の両方で、オブジェクトの _同じインスタンス_ を使用するようにすることが重要です。そうしないと、それらが等しくなくなり、リストボックスが正しく動作しなくなります。
同じオブジェクトの異なるインスタンスを簡単に操作できるように、by
プロパティを使用して、オブジェクトの同一性を比較する代わりに、特定のフィールドでオブジェクトを比較できます。
オブジェクトを 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
プロパティを使用します。カスタムコンポーネントが forward refs を行い、Headless UI が適切に接続できるようにしてください。
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { forwardRef, useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
let MyCustomButton = forwardRef(function (props, ref) { return <button className="..." ref={ref} {...props} />})
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton as={MyCustomButton}>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom" as="ul">
{people.map((person) => (
<ListboxOption as="li" key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
要素にラッパー要素なしで子を直接レンダリングするように指示するには、Fragment
を使用します。
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { Fragment, useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton as={Fragment}> <button>{selectedPerson.name}</button> </ListboxButton> <ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
カスタムリストボックスを構築する場合、ListboxButton
コンポーネントは必須ですが、ボタンがデフォルトで含まれており、リストボックスを使用するたびに必須にならないように構築できます。たとえば、次のような API です。
<MyListbox name="status">
<MyListboxOption value="active">Active</MyListboxOption>
<MyListboxOption value="paused">Paused</MyListboxOption>
<MyListboxOption value="delayed">Delayed</MyListboxOption>
<MyListboxOption value="canceled">Canceled</MyListboxOption>
</MyListbox>
これを実現するには、ListboxButton
内で ListboxSelectedOption
コンポーネントを使用して、現在選択されているリストボックスオプションをレンダリングします。
これを機能させるには、カスタムリストボックスの children
(すべての ListboxOption
インスタンス)を、ListboxOptions
の子として、および options
プロパティを介して ListboxSelectedOption
の両方に渡す必要があります。
次に、ListboxButton
でレンダリングされているか ListboxOptions
でレンダリングされているかに基づいて ListboxOption
をスタイル設定するには、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
プロパティもあります。
コマンド | 説明 |
Space、下矢印、または上矢印 | リストボックスを開き、選択した項目にフォーカスを合わせます |
Enter | 親フォームが存在する場合は、それを送信します |
Escリストボックスが開いている場合 | リストボックスを閉じます |
下矢印または上矢印リストボックスが開いている場合 | 前/次の無効ではない項目にフォーカスします |
左矢印または右矢印リストボックスが開いていて、 | 前/次の無効ではない項目にフォーカスします |
HomeまたはPageUpリストボックスが開いている場合 | 最初の無効ではない項目にフォーカスします |
EndまたはPageDownリストボックスが開いている場合 | 最後の無効ではない項目にフォーカスします |
EnterまたはSpaceリストボックスが開いている場合 | 現在の項目を選択します |
A–Zまたはa–zリストボックスが開いている場合 | キーボード入力に一致する最初の項目にフォーカスします |
プロパティ | デフォルト | 説明 |
as | Fragment | 文字列 | コンポーネント 要素またはコンポーネントリストボックスとしてレンダリングする必要があります。 |
invalid | false | ブール値 であるかどうかリストボックスが無効です。 |
disabled | false | ブール値 これを使用して、 |
value | — | T 選択した値。 |
defaultValue | — | T 非制御コンポーネントとして使用する場合のデフォルト値。 |
by | — | keyof T | ((a: T, z: T) => boolean) これを使用して、特定のフィールドでオブジェクトを比較するか、オブジェクトの比較方法を完全に制御するために独自の比較関数を渡します。 オブジェクトを |
onChange | — | (value: T) => void 新しいオプションが選択されたときに呼び出す関数。 |
horizontal | false | ブール値 true の場合、 |
multiple | false | ブール値 複数のオプションを選択できるかどうか。 |
name | — | 文字列 を使用する場合に使用される名前リストボックスフォーム内。 |
form | — | 文字列 のフォームの idリストボックスが属します。
|
データ属性 | レンダリングプロパティ | 説明 |
— | value |
選択した値。 |
data-open | open |
であるかどうかリストボックスが開いています。 |
data-invalid | invalid |
であるかどうかリストボックスが無効です。 |
data-disabled | disabled |
であるかどうかリストボックスが無効です。 |
プロパティ | デフォルト | 説明 |
as | ボタン | 文字列 | コンポーネント 要素またはコンポーネントリストボックスボタンとしてレンダリングする必要があります。 |
データ属性 | レンダリングプロパティ | 説明 |
— | value |
選択した値。 |
data-open | open |
であるかどうかリストボックスが開いています。 |
data-invalid | invalid |
であるかどうかリストボックスが無効です。 |
data-disabled | disabled |
であるかどうかリストボックスボタンが無効です。 |
data-focus | focus |
であるかどうかリストボックスボタンがフォーカスされています。 |
data-hover | hover |
であるかどうかリストボックスボタンがホバーされています。 |
data-active | active |
であるかどうかリストボックスボタンアクティブまたは押された状態です。 |
data-autofocus | autofocus |
|
プロパティ | デフォルト | 説明 |
as | Fragment | 文字列 | コンポーネント 要素またはコンポーネントリストボックスの選択されたオプションとしてレンダリングする必要があります。 |
placeholder | — | ReactNode オプションが選択されていない場合にレンダリングする React 要素。 |
options | — | ReactNode[]
|
プロパティ | デフォルト | 説明 |
as | div | 文字列 | コンポーネント 要素またはコンポーネントリストボックスのオプションとしてレンダリングする必要があります。 |
transition | false | ブール値 要素が |
anchor | — | オブジェクト ドロップダウンをボタンに固定する方法を設定します。 |
anchor.to | bottom | 文字列 どこに配置するかリストボックスのオプショントリガーを基準にします。
または、 |
anchor.gap | 0 | 数値 | 文字列 の間のスペースリストボックスボタンとリストボックスのオプション.
|
anchor.offset | 0 | 数値 | 文字列 の距離リストボックスのオプション元の位置からずらす必要があります。
|
anchor.padding | 0 | 数値 | 文字列 の間の最小スペースリストボックスのオプションとビューポート。
|
static | false | ブール値 要素が内部で管理されている開閉状態を無視するかどうか。 |
unmount | true | ブール値 要素を開閉状態に基づいてアンマウントするか、非表示にするか。 |
portal | false | ブール値 要素をポータルにレンダリングするかどうか。
|
modal | true | ブール値 スクロールロック、フォーカストラップなどのアクセシビリティ機能を有効にするかどうか、他の要素を |
データ属性 | レンダリングプロパティ | 説明 |
data-open | open |
であるかどうかリストボックスが開いています。 |
プロパティ | デフォルト | 説明 |
as | div | 文字列 | コンポーネント 要素またはコンポーネントリストボックスのオプションとしてレンダリングする必要があります。 |
value | — | T オプション値。 |
disabled | false | ブール値 であるかどうかリストボックスのオプション無効化されているキーボードナビゲーションとARIAの目的のため. |
データ属性 | レンダリングプロパティ | 説明 |
data-selected | selected |
であるかどうかリストボックスのオプション選択されているかどうか。 |
data-disabled | disabled |
であるかどうかリストボックスのオプションが無効です。 |
data-focus | focus |
であるかどうかリストボックスのオプションがフォーカスされています。 |
data-selectedOption | selectedOption |
リストボックスオプションが |
Headless UIを使用した、事前に設計されたTailwind CSSセレクトメニューとリストボックスの例に関心がある場合は、Tailwind UIをご覧ください。これは、当社が作成した美しくデザインされ、専門的に作られたコンポーネントのコレクションです。
これは、このようなオープンソースプロジェクトに対する私たちの活動をサポートし、それらを改善し、適切に維持できるようにするための素晴らしい方法です。