Back to React Spectrum

useListBox

packages/react-aria/docs/listbox/useListBox.mdx

2022-12-1623.6 KB
Original Source

{/* Copyright 2020 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */}

import {Layout} from '@react-spectrum/docs'; export default Layout;

import docs from 'docs:@react-aria/listbox'; import collectionsDocs from 'docs:@react-types/shared/src/collections.d.ts'; import selectionDocs from 'docs:@react-stately/selection'; import statelyDocs from 'docs:@react-stately/list'; import utilsDocs from 'docs:@react-aria/utils'; import {HeaderInfo, FunctionAPI, TypeContext, InterfaceType, TypeLink, PageDescription} from '@react-spectrum/docs'; import packageData from '@react-aria/listbox/package.json'; import Anatomy from './anatomy.svg'; import {Keyboard} from '@react-spectrum/text'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight';


category: Collections keywords: [listbox, aria]

useListBox

<PageDescription>{docs.exports.useListBox.description}</PageDescription>

<HeaderInfo packageData={packageData} componentNames={['useListBox', 'useOption', 'useListBoxSection']} sourceData={[ {type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/listbox/'} ]} />

API

<FunctionAPI function={docs.exports.useListBox} links={docs.links} /> <FunctionAPI function={docs.exports.useOption} links={docs.links} /> <FunctionAPI function={docs.exports.useListBoxSection} links={docs.links} />

Features

A listbox can be built using the <select> and <option> HTML elements, but this is not possible to style consistently cross browser. useListBox helps achieve accessible listbox components that can be styled as needed.

Note: useListBox only handles the list itself. For a dropdown similar to a <select>, see useSelect.

  • Exposed to assistive technology as a listbox using ARIA
  • Support for single, multiple, or no selection
  • Support for disabled items
  • Support for sections
  • Labeling support for accessibility
  • Support for mouse, touch, and keyboard interactions
  • Tab stop focus management
  • Keyboard navigation support including arrow keys, home/end, page up/down, select all, and clear
  • Automatic scrolling support during keyboard navigation
  • Typeahead to allow focusing options by typing text
  • Support for use with virtualized lists

Anatomy

<Anatomy />

A listbox consists of a container element, with a list of options or groups inside. useListBox, useOption, and useListBoxSection handle exposing this to assistive technology using ARIA, along with handling keyboard, mouse, and interactions to support selection and focus behavior.

useListBox returns props that you should spread onto the list container element, along with props for an optional visual label:

<TypeContext.Provider value={docs.links}> <InterfaceType properties={docs.links[docs.exports.useListBox.return.id].properties} /> </TypeContext.Provider>

useOption returns props for an individual option and its children, along with states you can use for styling:

<TypeContext.Provider value={docs.links}> <InterfaceType properties={docs.links[docs.exports.useOption.return.id].properties} /> </TypeContext.Provider>

useListBoxSection returns props for a section:

<TypeContext.Provider value={docs.links}> <InterfaceType properties={docs.links[docs.exports.useListBoxSection.return.id].properties} /> </TypeContext.Provider>

State is managed by the <TypeLink links={statelyDocs.links} type={statelyDocs.exports.useListState} /> hook from @react-stately/list. The state object should be passed as an option to each of the above hooks.

If a listbox, options, or group does not have a visible label, an aria-label or aria-labelledby prop must be passed instead to identify the element to assistive technology.

State management

useListBox requires knowledge of the options in the listbox in order to handle keyboard navigation and other interactions. It does this using the <TypeLink links={collectionsDocs.links} type={collectionsDocs.exports.Collection} /> interface, which is a generic interface to access sequential unique keyed data. You can implement this interface yourself, e.g. by using a prop to pass a list of item objects, but <TypeLink links={statelyDocs.links} type={statelyDocs.exports.useListState} /> from @react-stately/list implements a JSX based interface for building collections instead. See Collection Components for more information.

In addition, <TypeLink links={statelyDocs.links} type={statelyDocs.exports.useListState} /> manages the state necessary for multiple selection and exposes a <TypeLink links={selectionDocs.links} type={selectionDocs.exports.SelectionManager} />, which makes use of the collection to provide an interface to update the selection state. For more information, see Selection.

Example

This example uses HTML <ul> and <li> elements to represent the list, and applies props from <TypeLink links={docs.links} type={docs.exports.useListBox} /> and <TypeLink links={docs.links} type={docs.exports.useOption} />. For each item in the collection in state, either an Option or ListBoxSection (defined below) is rendered according to the item's type property.

tsx
import type {AriaListBoxProps} from '@react-aria/listbox';
import {useListBox, useOption} from '@react-aria/listbox';
import {useListState} from '@react-stately/list';
import {Item} from '@react-stately/collections';
import {useFocusRing} from '@react-aria/focus';
import {mergeProps} from '@react-aria/utils';

function ListBox<T extends object>(props: AriaListBoxProps<T>) {
  // Create state based on the incoming props
  let state = useListState(props);

  // Get props for the listbox element
  let ref = React.useRef(null);
  let {listBoxProps, labelProps} = useListBox(props, state, ref);

  return (
    <>
      <div {...labelProps}>{props.label}</div>
      <ul {...listBoxProps} ref={ref}>
        {[...state.collection].map(item => (
          item.type === 'section'
            ? <ListBoxSection key={item.key} section={item} state={state} />
            : <Option key={item.key} item={item} state={state} />
        ))}
      </ul>
    </>
  );
}

function Option({item, state}) {
  // Get props for the option element
  let ref = React.useRef(null);
  let {optionProps} = useOption({key: item.key}, state, ref);

  // Determine whether we should show a keyboard
  // focus ring for accessibility
  let {isFocusVisible, focusProps} = useFocusRing();

  return (
    <li
      {...mergeProps(optionProps, focusProps)}
      ref={ref}
      data-focus-visible={isFocusVisible}>
      {item.rendered}
    </li>
  );
}

<ListBox label="Alignment" selectionMode="single">
  <Item>Left</Item>
  <Item>Middle</Item>
  <Item>Right</Item>
</ListBox>
<details> <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show CSS</summary>
css
[role=listbox] {
  padding: 0;
  margin: 5px 0;
  list-style: none;
  border: 1px solid gray;
  max-width: 250px;
  max-height: 300px;
  overflow: auto;
}

[role=option] {
  display: block;
  padding: 2px 5px;
  outline: none;
  cursor: default;
  color: inherit;

  &[data-focus-visible=true] {
    outline: 2px solid orange;
  }

  &[aria-selected=true] {
    background: blueviolet;
    color: white;
  }

  &[aria-disabled] {
    color: #aaa;
  }
}
</details>

Dynamic collections

ListBox follows the Collection Components API, accepting both static and dynamic collections. The example above shows static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time.

As seen below, an iterable list of options is passed to the ListBox using the items prop. Each item accepts a key prop, which is passed to the onSelectionChange handler to identify the selected item. Alternatively, if the item objects contain an id property, as shown in the example below, then this is used automatically and a key prop is not required.

tsx
function Example() {
  let options = [
    { id: 1, name: 'Aardvark' },
    { id: 2, name: 'Cat' },
    { id: 3, name: 'Dog' },
    { id: 4, name: 'Kangaroo' },
    { id: 5, name: 'Koala' },
    { id: 6, name: 'Penguin' },
    { id: 7, name: 'Snake' },
    { id: 8, name: 'Turtle' },
    { id: 9, name: 'Wombat' }
  ];

  return (
    <ListBox label="Animals" items={options} selectionMode="single">
      {(item) => <Item>{item.name}</Item>}
    </ListBox>
  );
}

Selection

ListBox supports multiple selection modes. By default, selection is disabled, however this can be changed using the selectionMode prop. Use defaultSelectedKeys to provide a default set of selected items (uncontrolled) and selectedKeys to set the selected items (controlled). The value of the selected keys must match the key prop of the items. See the react-stately Selection docs for more details.

tsx
import type {Selection} from 'react-stately';

function Example() {
  let [selected, setSelected] = React.useState<Selection>(new Set(['cheese']));

  return (
    <>
      <ListBox label="Choose sandwich contents" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected}>
        <Item key="lettuce">Lettuce</Item>
        <Item key="tomato">Tomato</Item>
        <Item key="cheese">Cheese</Item>
        <Item key="tuna">Tuna Salad</Item>
        <Item key="egg">Egg Salad</Item>
        <Item key="ham">Ham</Item>
      </ListBox>
      <p>Current selection (controlled): {selected === 'all' ? 'all' : [...selected].join(', ')}</p>
    </>
  );
}

Selection behavior

By default, useListBox uses the "toggle" selection behavior, which behaves like a checkbox group: clicking, tapping, or pressing the <Keyboard>Space</Keyboard> or <Keyboard>Enter</Keyboard> keys toggles selection for the focused row. Using the arrow keys moves focus but does not change selection.

When selectionBehavior is set to "replace", clicking a row with the mouse replaces the selection with only that row. Using the arrow keys moves both focus and selection. To select multiple rows, modifier keys such as <Keyboard>Ctrl</Keyboard>, <Keyboard>Cmd</Keyboard>, and <Keyboard>Shift</Keyboard> can be used. On touch screen devices, selection always behaves as toggle since modifier keys may not be available.

These selection behaviors are defined in Aria Practices.

tsx
<ListBox label="Choose sandwich contents" selectionMode="multiple" selectionBehavior="replace">
  <Item key="lettuce">Lettuce</Item>
  <Item key="tomato">Tomato</Item>
  <Item key="cheese">Cheese</Item>
  <Item key="tuna">Tuna Salad</Item>
  <Item key="egg">Egg Salad</Item>
  <Item key="ham">Ham</Item>
</ListBox>

Sections

ListBox supports sections with separators and headings in order to group options. Sections can be used by wrapping groups of Items in a Section component. Each Section takes a title and key prop. To implement sections, implement the ListBoxSection component referenced above using the <TypeLink links={docs.links} type={docs.exports.useListBoxSection} /> hook. It will include four extra elements: an <li> between the sections to represent the separator, an <li> to contain the heading <span> element, and a <ul> to contain the child items. This structure is necessary to ensure HTML semantics are correct.

tsx
import {useListBoxSection} from '@react-aria/listbox';

function ListBoxSection({section, state}) {
  let {itemProps, headingProps, groupProps} = useListBoxSection({
    heading: section.rendered,
    'aria-label': section['aria-label']
  });

  // If the section is not the first, add a separator element to provide visual separation.
  // The heading is rendered inside an <li> element, which contains
  // a <ul> with the child items.
  return <>
    {section.key !== state.collection.getFirstKey() &&
      <li
        role="presentation"
        style={{
          borderTop: '1px solid gray',
          margin: '2px 5px'
        }} />
    }
    <li {...itemProps}>
      {section.rendered &&
        <span
          {...headingProps}
          style={{
            fontWeight: 'bold',
            fontSize: '1.1em',
            padding: '2px 5px',
          }}>
          {section.rendered}
        </span>
      }
      <ul
        {...groupProps}
        style={{
          padding: 0,
          listStyle: 'none'
        }}>
        {[...section.childNodes].map(node =>
          <Option
            key={node.key}
            item={node}
            state={state} />
        )}
      </ul>
    </li>
  </>;
}

Static items

With this in place, we can now render a static ListBox with multiple sections:

tsx
import {Section} from '@react-stately/collections';

<ListBox label="Choose sandwich contents" selectionMode="multiple">
  <Section title="Veggies">
    <Item key="lettuce">Lettuce</Item>
    <Item key="tomato">Tomato</Item>
    <Item key="onion">Onion</Item>
  </Section>
  <Section title="Protein">
    <Item key="ham">Ham</Item>
    <Item key="tuna">Tuna</Item>
    <Item key="tofu">Tofu</Item>
  </Section>
  <Section title="Condiments">
    <Item key="mayo">Mayonaise</Item>
    <Item key="mustard">Mustard</Item>
    <Item key="ranch">Ranch</Item>
  </Section>
</ListBox>

Dynamic items

The above example shows sections with static items. Sections can also be populated from a hierarchical data structure. Similarly to the props on ListBox, <Section> takes an array of data using the items prop.

tsx
import type {Selection} from 'react-stately';

function Example() {
  let options = [
    {name: 'Australian', children: [
      {id: 2, name: 'Koala'},
      {id: 3, name: 'Kangaroo'},
      {id: 4, name: 'Platypus'}
    ]},
    {name: 'American', children: [
      {id: 6, name: 'Bald Eagle'},
      {id: 7, name: 'Bison'},
      {id: 8, name: 'Skunk'}
    ]}
  ];
  let [selected, setSelected] = React.useState<Selection>(new Set());

  return (
    <ListBox
      label="Pick an animal"
      items={options}
      selectedKeys={selected}
      selectionMode="single"
      onSelectionChange={setSelected}>
      {item => (
        <Section key={item.name} items={item.children} title={item.name}>
          {item => <Item>{item.name}</Item>}
        </Section>
      )}
    </ListBox>
  );
}

Accessibility

Sections without a title must provide an aria-label for accessibility.

Complex options

By default, options that only contain text will be labeled by the contents of the option. For options that have more complex content (e.g. icons, multiple lines of text, etc.), use labelProps and descriptionProps from <TypeLink links={docs.links} type={docs.exports.useOption} /> as needed to apply to the main text element of the option and its description. This improves screen reader announcement.

NOTE: listbox options cannot contain interactive content (e.g. buttons, checkboxes, etc.). For these cases, see useGridList instead.

To implement this, we'll update the Option component to apply the ARIA properties returned by <TypeLink links={docs.links} type={docs.exports.useOption} /> to the appropriate elements. In this example, we'll pull them out of props.children and use React.cloneElement to apply the props, but you may want to use a more robust approach (e.g. context).

tsx
///- begin collapse -///
function ListBox(props) {
  let state = useListState(props);
  let ref = React.useRef(null);
  let {listBoxProps, labelProps} = useListBox(props, state, ref);

  return (
    <>
      <div {...labelProps}>{props.label}</div>
      <ul {...listBoxProps} ref={ref}>
        {[...state.collection].map(item => (
          <Option
            key={item.key}
            item={item}
            state={state} />
        ))}
      </ul>
    </>
  );
}
///- end collapse -///
function Option({item, state}) {
  let ref = React.useRef(null);
  let {optionProps, labelProps, descriptionProps} = useOption({key: item.key}, state, ref);
  let {isFocusVisible, focusProps} = useFocusRing();

  // Pull out the two expected children. We will clone them
  // and add the necessary props for accessibility.
  let [title, description] = item.rendered;

  return (
    <li
      {...mergeProps(optionProps, focusProps)}
      ref={ref}
      data-focus-visible={isFocusVisible}>
      {React.cloneElement(title, labelProps)}
      {React.cloneElement(description, descriptionProps)}
    </li>
  );
}

<ListBox label="Text alignment" selectionMode="single">
  <Item textValue="Align Left">
    <div><strong>Align Left</strong></div>
    <div>Align the selected text to the left</div>
  </Item>
  <Item textValue="Align Center">
    <div><strong>Align Center</strong></div>
    <div>Align the selected text center</div>
  </Item>
  <Item textValue="Align Right">
    <div><strong>Align Right</strong></div>
    <div>Align the selected text to the right</div>
  </Item>
</ListBox>

Asynchronous loading

This example uses the useAsyncList hook to handle asynchronous loading of data from a server. You may additionally want to display a spinner to indicate the loading state to the user, or support features like infinite scroll to load more data.

tsx
import {useAsyncList} from '@react-stately/data';

interface Pokemon {
  name: string
}

function AsyncLoadingExample() {
  let list = useAsyncList<Pokemon>({
    async load({signal}) {
      let res = await fetch(
        `https://pokeapi.co/api/v2/pokemon`,
        {signal}
      );
      let json = await res.json();

      return {
        items: json.results
      };
    }
  });

  return (
    <ListBox label="Pick a Pokemon" items={list.items} selectionMode="single">
      {(item) => <Item key={item.name}>{item.name}</Item>}
    </ListBox>
  );
}

By default, interacting with an item in a ListBox triggers onSelectionChange. Alternatively, items may be links to another page or website. This can be achieved by passing the href prop to the <Item> component.

This example shows how to update the Option component with support for rendering an <a> element if an href prop is passed to the item. Note that you'll also need to render the ListBox as a <div> instead of a <ul>, since an <a> inside a <ul> is not valid HTML.

tsx
///- begin collapse -///
function ListBox(props) {
  let state = useListState(props);
  let ref = React.useRef(null);
  let {listBoxProps, labelProps} = useListBox(props, state, ref);

  return (
    <>
      <div {...labelProps}>{props.label}</div>
      <ul {...listBoxProps} ref={ref}>
        {[...state.collection].map(item => (
          <Option
            key={item.key}
            item={item}
            state={state} />
        ))}
      </ul>
    </>
  );
}
///- end collapse -///
function Option({item, state}) {
  let ref = React.useRef(null);
  let {optionProps} = useOption({key: item.key}, state, ref);
  let {isFocusVisible, focusProps} = useFocusRing();
  /*- begin highlight -*/
  let ElementType: React.ElementType = item.props.href ? 'a' : 'div';
  /*- end highlight -*/

  return (
    <ElementType
      {...mergeProps(optionProps, focusProps)}
      ref={ref}
      data-focus-visible={isFocusVisible}>
      {item.rendered}
    </ElementType>
  );
}

<ListBox aria-label="Links">
  <Item href="https://adobe.com/" target="_blank">Adobe</Item>
  <Item href="https://apple.com/" target="_blank">Apple</Item>
  <Item href="https://google.com/" target="_blank">Google</Item>
  <Item href="https://microsoft.com/" target="_blank">Microsoft</Item>
</ListBox>
css
[role=option][href] {
  text-decoration: none;
  cursor: pointer;
  -webkit-touch-callout: none;
}

By default, link items in a ListBox are not selectable, and only perform navigation when the user interacts with them. However, with the "replace" selection behavior, items will be selected when single clicking or pressing the <Keyboard>Space</Keyboard> key, and navigate to the link when double clicking or pressing the <Keyboard>Enter</Keyboard> key.

tsx
///- begin collapse -///
function ListBox(props) {
  let state = useListState(props);
  let ref = React.useRef(null);
  let {listBoxProps, labelProps} = useListBox(props, state, ref);

  return (
    <>
      <div {...labelProps}>{props.label}</div>
      <ul {...listBoxProps} ref={ref}>
        {[...state.collection].map(item => (
          <Option
            key={item.key}
            item={item}
            state={state} />
        ))}
      </ul>
    </>
  );
}
function Option({item, state}) {
  let ref = React.useRef(null);
  let {optionProps} = useOption({key: item.key}, state, ref);
  let {isFocusVisible, focusProps} = useFocusRing();
  let ElementType: React.ElementType = item.props.href ? 'a' : 'div';

  return (
    <ElementType
      {...mergeProps(optionProps, focusProps)}
      ref={ref}
      data-focus-visible={isFocusVisible}>
      {item.rendered}
    </ElementType>
  );
}
///- end collapse -///

<ListBox aria-label="Links" selectionMode="multiple" selectionBehavior="replace">
  <Item href="https://adobe.com/" target="_blank">Adobe</Item>
  <Item href="https://apple.com/" target="_blank">Apple</Item>
  <Item href="https://google.com/" target="_blank">Google</Item>
  <Item href="https://microsoft.com/" target="_blank">Microsoft</Item>
</ListBox>

Client side routing

The <Item> component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the <TypeLink links={utilsDocs.links} type={utilsDocs.exports.RouterProvider} /> component at the root of your app. See the framework setup guide to learn how to set this up.

Disabled items

useListBox supports marking items as disabled using the disabledKeys prop. Each key in this list corresponds with the key prop passed to the Item component, or automatically derived from the values passed to the items prop. See Collections for more details.

Disabled items are not focusable, selectable, or keyboard navigable. The isDisabled property returned by useOption can be used to style the item appropriately.

tsx
<ListBox label="Choose sandwich contents" selectionMode="multiple" disabledKeys={['tuna']}>
  <Item key="lettuce">Lettuce</Item>
  <Item key="tomato">Tomato</Item>
  <Item key="cheese">Cheese</Item>
  <Item key="tuna">Tuna Salad</Item>
  <Item key="egg">Egg Salad</Item>
  <Item key="ham">Ham</Item>
</ListBox>

Internationalization

useListBox handles some aspects of internationalization automatically. For example, type to select is implemented with an Intl.Collator for internationalized string matching. You are responsible for localizing all labels and option content that is passed into the listbox.

RTL

In right-to-left languages, the listbox options should be mirrored. The text content should be aligned to the right. Ensure that your CSS accounts for this.