553 lines
19 KiB
Plaintext
553 lines
19 KiB
Plaintext
# Listbox
|
|
|
|
<script>
|
|
import Preview from "./_Preview.svelte";
|
|
</script>
|
|
|
|
<Preview url="examples/listbox" code="" class="h-[410px] bg-orange-300"/>
|
|
|
|
## Basic example
|
|
|
|
Listboxes are built using a combination of the `Listbox`, `ListboxButton`, `ListboxOptions`, `ListboxOption`, and `ListboxLabel` components.
|
|
|
|
The `ListboxButton` will automatically open/close the `ListboxOptions` when clicked, and when the listbox is open, the list of items receives focus and is automatically navigable via the keyboard.
|
|
|
|
```svelte
|
|
<script>
|
|
import {
|
|
Listbox,
|
|
ListboxButton,
|
|
ListboxOptions,
|
|
ListboxOption,
|
|
} from "@rgossiaux/svelte-headlessui";
|
|
|
|
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 },
|
|
];
|
|
|
|
let selectedPerson = people[0];
|
|
</script>
|
|
|
|
<Listbox bind:value={selectedPerson}>
|
|
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
|
<ListboxOptions>
|
|
{#each people as person (person.id)}
|
|
<ListboxOption value={person} disabled={person.unavailable}>
|
|
{person.name}
|
|
</ListboxOption>
|
|
{/each}
|
|
</ListboxOptions>
|
|
</Listbox>
|
|
```
|
|
|
|
## Styling
|
|
|
|
[See here](general-concepts#component-styling) for some general notes on styling the components in this library.
|
|
|
|
### Active and selected items
|
|
|
|
To style the active `ListboxOption`, you can use the `active` slot prop that it provides, which tells you whether or not that option is currently focused via the mouse or keyboard.
|
|
|
|
To style the selected `ListboxOption`, you can use the `selected` slot prop that it provides, which tells you whether or not that option is the selected option (the one passed to the `value` prop of the `<Listbox>`)
|
|
|
|
You can use these states to conditionally apply whatever active/focus styles you wish.
|
|
|
|
```svelte
|
|
<script>
|
|
import {
|
|
Listbox,
|
|
ListboxButton,
|
|
ListboxOptions,
|
|
ListboxOption,
|
|
} from "@rgossiaux/svelte-headlessui";
|
|
import { CheckIcon } from "@rgossiaux/svelte-heroicons/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" },
|
|
];
|
|
|
|
let selectedPerson = people[0];
|
|
</script>
|
|
|
|
<Listbox bind:value={selectedPerson}>
|
|
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
|
<ListboxOptions>
|
|
{#each people as person (person.id)}
|
|
<!-- Use the `active` state to conditionally style the active (focused) option -->
|
|
<!-- Use the `selected` state to conditionally style the selected option -->
|
|
<ListboxOption
|
|
value={person}
|
|
disabled={person.unavailable}
|
|
class={({ active }) => (active ? "active" : "")}
|
|
let:selected
|
|
>
|
|
{#if selected}
|
|
<CheckIcon />
|
|
{/if}
|
|
{person.name}
|
|
</ListboxOption>
|
|
{/each}
|
|
</ListboxOptions>
|
|
</Listbox>
|
|
```
|
|
|
|
## Using a custom label
|
|
|
|
By default, the `Listbox` will use the `<ListboxButton>` contents as the label for screenreaders. If you'd like more control over what is announced to assistive technologies, use the `ListboxLabel` component:
|
|
|
|
```svelte
|
|
<script>
|
|
import {
|
|
Listbox,
|
|
ListboxButton,
|
|
ListboxLabel,
|
|
ListboxOptions,
|
|
ListboxOption,
|
|
} from "@rgossiaux/svelte-headlessui";
|
|
|
|
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 selectedPerson = people[0];
|
|
</script>
|
|
|
|
<Listbox bind:value={selectedPerson}>
|
|
<ListboxLabel>Assignee:</ListboxLabel>
|
|
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
|
<ListboxOptions>
|
|
{#each people as person (person.id)}
|
|
<ListboxOption value={person}>
|
|
{person.name}
|
|
</ListboxOption>
|
|
{/each}
|
|
</ListboxOptions>
|
|
</Listbox>
|
|
```
|
|
|
|
## Showing/hiding the listbox
|
|
|
|
By default, the `ListboxOptions` instance will be shown and hidden automatically based on the internal `open` state tracked by the `Listbox` component itself.
|
|
|
|
If you'd rather handle this yourself (perhaps because you need to add an extra wrapper element for one reason or another), you can add a `static` prop to the `ListboxOptions` component to tell it to always render, and use the `open` slot prop provided by the `Listbox` to show or hide the listbox yourself.
|
|
|
|
```svelte
|
|
<script>
|
|
import {
|
|
Listbox,
|
|
ListboxButton,
|
|
ListboxOptions,
|
|
ListboxOption,
|
|
} from "@rgossiaux/svelte-headlessui";
|
|
|
|
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 selectedPerson = people[0];
|
|
</script>
|
|
|
|
<Listbox
|
|
bind:value={selectedPerson}
|
|
let:open
|
|
>
|
|
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
|
{#if open}
|
|
<div>
|
|
<!-- Using `static`, the `ListboxOptions` is always rendered
|
|
and ignores the internal `open` state -->
|
|
<ListboxOptions static>
|
|
{#each people as person (person.id)}
|
|
<ListboxOption value={person}>
|
|
{person.name}
|
|
</ListboxOption>
|
|
{/each}
|
|
</ListboxOptions>
|
|
</div>
|
|
{/if}
|
|
</Listbox>
|
|
```
|
|
|
|
You can also choose to have the `Listbox` merely hide the `ListboxOptions` when the `Listbox` is closed, instead of removing it from the DOM entirely, by using the `unmount` prop. This may be useful for performance reasons if rendering the `ListboxOptions` is expensive.
|
|
|
|
```svelte
|
|
<script>
|
|
import {
|
|
Listbox,
|
|
ListboxButton,
|
|
ListboxOptions,
|
|
ListboxOption,
|
|
} from "@rgossiaux/svelte-headlessui";
|
|
|
|
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 selectedPerson = people[0];
|
|
</script>
|
|
|
|
<Listbox bind:value={selectedPerson}>
|
|
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
|
<!-- Using `unmount={false}`, the `ListboxOptions` is kept in the DOM
|
|
when the `Listbox` is closed -->
|
|
<ListboxOptions unmount={false}>
|
|
{#each people as person (person.id)}
|
|
<ListboxOption value={person}>
|
|
{person.name}
|
|
</ListboxOption>
|
|
{/each}
|
|
</ListboxOptions>
|
|
</Listbox>
|
|
```
|
|
|
|
## Disabling an item
|
|
|
|
Use the `disabled` prop to disable a `ListboxOption`. This will make it unselectable via mouse and keyboard navigation, and it will be skipped when pressing the up/down arrows.
|
|
|
|
```svelte
|
|
<script>
|
|
import {
|
|
Listbox,
|
|
ListboxButton,
|
|
ListboxOptions,
|
|
ListboxOption,
|
|
} from "@rgossiaux/svelte-headlessui";
|
|
|
|
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 },
|
|
];
|
|
|
|
let selectedPerson = people[0];
|
|
</script>
|
|
|
|
<Listbox bind:value={selectedPerson}>
|
|
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
|
<ListboxOptions>
|
|
{#each people as person (person.id)}
|
|
<ListboxOption value={person} disabled={person.unavailable}>
|
|
<span class:unavailable={person.unavailable}>
|
|
{person.name}
|
|
</span>
|
|
</ListboxOption>
|
|
{/each}
|
|
</ListboxOptions>
|
|
</Listbox>
|
|
```
|
|
|
|
## Transitions
|
|
|
|
To animate the opening and closing of the listbox, you can use [this library's Transition component](/docs/2.0/transition) or Svelte's built-in transition engine. See that page for a comparison.
|
|
|
|
### Using the `Transition` component
|
|
|
|
To use the `Transition` component, all you need to do is wrap the `ListboxOptions` in a `<Transition>`, and the transition will be applied automatically.
|
|
|
|
```svelte
|
|
<script>
|
|
import {
|
|
Listbox,
|
|
ListboxButton,
|
|
ListboxOptions,
|
|
ListboxOption,
|
|
Transition,
|
|
} from "@rgossiaux/svelte-headlessui";
|
|
|
|
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 selectedPerson = people[0];
|
|
</script>
|
|
|
|
<Listbox bind:value={selectedPerson}>
|
|
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
|
<!-- Example using Tailwind CSS transition classes -->
|
|
<Transition
|
|
enter="transition duration-100 ease-out"
|
|
enterFrom="transform scale-95 opacity-0"
|
|
enterTo="transform scale-100 opacity-100"
|
|
leave="transition duration-75 ease-out"
|
|
leaveFrom="transform scale-100 opacity-100"
|
|
leaveTo="transform scale-95 opacity-0"
|
|
>
|
|
<ListboxOptions>
|
|
{#each people as person (person.id)}
|
|
<ListboxOption value={person}>
|
|
{person.name}
|
|
</ListboxOption>
|
|
{/each}
|
|
</ListboxOptions>
|
|
</Transition>
|
|
</Listbox>
|
|
```
|
|
|
|
The components in this library communicate with each other, so the Transition will be managed automatically when the Listbox is opened/closed. If you require more control over this behavior, you may use a more explicit version:
|
|
|
|
```svelte
|
|
<script>
|
|
import {
|
|
Listbox,
|
|
ListboxButton,
|
|
ListboxOptions,
|
|
ListboxOption,
|
|
Transition,
|
|
} from "@rgossiaux/svelte-headlessui";
|
|
|
|
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 selectedPerson = people[0];
|
|
</script>
|
|
|
|
<Listbox
|
|
bind:value={selectedPerson}
|
|
let:open
|
|
>
|
|
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
|
<!-- Example using Tailwind CSS transition classes -->
|
|
<Transition
|
|
show={open}
|
|
enter="transition duration-100 ease-out"
|
|
enterFrom="transform scale-95 opacity-0"
|
|
enterTo="transform scale-100 opacity-100"
|
|
leave="transition duration-75 ease-out"
|
|
leaveFrom="transform scale-100 opacity-100"
|
|
leaveTo="transform scale-95 opacity-0"
|
|
>
|
|
<!-- When controlling the transition manually, make sure to use `static` -->
|
|
<ListboxOptions static>
|
|
{#each people as person (person.id)}
|
|
<ListboxOption value={person}>
|
|
{person.name}
|
|
</ListboxOption>
|
|
{/each}
|
|
</ListboxOptions>
|
|
</Transition>
|
|
</Listbox>
|
|
```
|
|
|
|
### Using Svelte transitions
|
|
|
|
The last example above also provides a blueprint for using Svelte transitions:
|
|
|
|
```svelte
|
|
<script>
|
|
import {
|
|
Listbox,
|
|
ListboxButton,
|
|
ListboxOptions,
|
|
ListboxOption,
|
|
} from "@rgossiaux/svelte-headlessui";
|
|
import { fade } from "svelte/transition";
|
|
|
|
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 selectedPerson = people[0];
|
|
</script>
|
|
|
|
<Listbox
|
|
bind:value={selectedPerson}
|
|
let:open
|
|
>
|
|
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
|
{#if open}
|
|
<div transition:fade>
|
|
<!-- When controlling the transition manually, make sure to use `static` -->
|
|
<ListboxOptions static>
|
|
{#each people as person (person.id)}
|
|
<ListboxOption value={person}>
|
|
{person.name}
|
|
</ListboxOption>
|
|
{/each}
|
|
</ListboxOptions>
|
|
</div>
|
|
{/if}
|
|
</Listbox>
|
|
```
|
|
|
|
Make sure to use the `static` prop, or else the exit transitions won't work correctly.
|
|
|
|
## Horizontal options
|
|
|
|
If you've styled your `ListboxOptions` to appear horizontally, use the `horizontal` prop on the `Listbox` component to enable navigating the items with the left and right arrow keys instead of up and down, and to update the `aria-orientation` attribute for assistive technologies.
|
|
|
|
```svelte
|
|
<script>
|
|
import {
|
|
Listbox,
|
|
ListboxButton,
|
|
ListboxOptions,
|
|
ListboxOption,
|
|
} from "@rgossiaux/svelte-headlessui";
|
|
|
|
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 selectedPerson = people[0];
|
|
</script>
|
|
|
|
<Listbox
|
|
bind:value={selectedPerson}
|
|
horizontal
|
|
>
|
|
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
|
<ListboxOptions>
|
|
{#each people as person (person.id)}
|
|
<ListboxOption value={person}>
|
|
{person.name}
|
|
</ListboxOption>
|
|
{/each}
|
|
</ListboxOptions>
|
|
</Listbox>
|
|
```
|
|
|
|
## Accessibility notes
|
|
|
|
### Focus management
|
|
|
|
When a Listbox is toggled open, the `ListboxOptions` receives focus. Focus is trapped within the list of items until Escape is pressed or the user clicks outside the options. Closing the `Listbox` returns focus to the `ListboxButton`.
|
|
|
|
### Mouse interaction
|
|
|
|
Clicking a `ListboxButton` toggles the options list open and closed. Clicking anywhere outside of the options list will close the listbox.
|
|
|
|
### Keyboard interaction
|
|
|
|
When the `horizontal` prop is set, the `<ArrowUp>` and `<ArrowDown>` below become `<ArrowLeft>` and `<ArrowRight>`:
|
|
|
|
| Command | Description |
|
|
| ----------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
| `<Enter>` / `<Space>` / `<ArrowUp>` / `<ArrowDown>` when `ListboxButton` is focused | Opens listbox and focuses the selected item |
|
|
| `<Esc>` when listbox is open | Closes listbox |
|
|
| `<ArrowDown>` / `<ArrowUp>` when listbox is open | Focuses next/previous non-disabled option |
|
|
| `<Home>` / `<End>` when listbox is open | Focuses first/last non-disabled option |
|
|
| `<Enter>` / `<Space>` when listbox is open | Selects the focused option |
|
|
| `<A-Za-z>` when listbox is open | Focuses next option that matches keyboard input |
|
|
|
|
### Other
|
|
|
|
All relevant ARIA attributes are automatically managed.
|
|
|
|
For a full reference on all accessibility features implemented in `Listbox`, see <a href="https://www.w3.org/TR/wai-aria-practices-1.2/#Listbox">the ARIA spec on Listbox</a>.
|
|
|
|
## Component API
|
|
|
|
### Listbox
|
|
|
|
The main listbox component.
|
|
|
|
| Prop | Default | Type | Description |
|
|
| ------------ | ------- | --------- | ---------------------------------------------------------------------------------- |
|
|
| `as` | `div` | `string` | The element the `Listbox` should render as |
|
|
| `disabled` | `false` | `boolean` | Whether the entire `Listbox` and its children should be disabled |
|
|
| `horizontal` | `false` | `boolean` | Whether the entire `Listbox` should be oriented horizontally instead of vertically |
|
|
| `value` | -- | `T` | The selected value |
|
|
|
|
| Slot prop | Type | Description |
|
|
| ---------- | --------- | -------------------------------------- |
|
|
| `disabled` | `boolean` | Whether or not the listbox is disabled |
|
|
| `open` | `boolean` | Whether or not the listbox is open |
|
|
|
|
|
|
### ListboxButton
|
|
|
|
The listbox's button.
|
|
|
|
| Prop | Default | Type | Description |
|
|
| ---- | -------- | -------- | ------------------------------------------------ |
|
|
| `as` | `button` | `string` | The element the `ListboxButton` should render as |
|
|
|
|
| Slot prop | Type | Description |
|
|
| ---------- | --------- | -------------------------------------- |
|
|
| `disabled` | `boolean` | Whether or not the listbox is disabled |
|
|
| `open` | `boolean` | Whether or not the listbox is open |
|
|
|
|
### ListboxLabel
|
|
|
|
A label that can be used for more control over the text your listbox will announce to screenreaders. Renders an element that is linked to the root `Listbox` via the `aria-labelledby` attribute and an autogenerated id.
|
|
|
|
| Prop | Default | Type | Description |
|
|
| ---- | ------- | -------- | ----------------------------------------------- |
|
|
| `as` | `label` | `string` | The element the `ListboxLabel` should render as |
|
|
|
|
| Slot prop | Type | Description |
|
|
| ---------- | --------- | -------------------------------------- |
|
|
| `disabled` | `boolean` | Whether or not the listbox is disabled |
|
|
| `open` | `boolean` | Whether or not the listbox is open |
|
|
|
|
### ListboxOptions
|
|
|
|
The component that directly wraps the list of options in your custom listbox.
|
|
|
|
| Prop | Default | Type | Description |
|
|
| --------- | ------- | --------- | ----------------------------------------------------------------------------------------------- |
|
|
| `as` | `ul` | `string` | The element the `ListboxOptions` should render as |
|
|
| `static` | `false` | `boolean` | Whether the element should ignore the internally managed open/closed state |
|
|
| `unmount` | `true` | `boolean` | Whether the element should be unmounted, instead of just hidden, based on the open/closed state |
|
|
|
|
Note that `static` and `unmount` cannot be used together.
|
|
|
|
| Slot prop | Type | Description |
|
|
| --------- | --------- | ---------------------------------- |
|
|
| `open` | `boolean` | Whether or not the listbox is open |
|
|
|
|
### ListboxOption
|
|
|
|
Used to wrap each item within your listbox.
|
|
|
|
| Prop | Default | Type | Description |
|
|
| ---------- | ------- | --------- | ------------------------------------------------------------------------------- |
|
|
| `value` | -- | `T` | The option value |
|
|
| `as` | `li` | `string` | The element the `ListboxOption` should render as |
|
|
| `disabled` | `false` | `boolean` | Whether the option should be disabled for keyboard navigation and ARIA purposes |
|
|
|
|
| Slot prop | Type | Description |
|
|
| ---------- | --------- | -------------------------------------- |
|
|
| `active` | `boolean` | Whether the option is active (focused) |
|
|
| `selected` | `boolean` | Whether the option is selected |
|
|
| `disabled` | `boolean` | Whether the option is disabled |
|