Back to React Spectrum

UseGridList

packages/react-aria/docs/gridlist/useGridList.mdx

2022-12-1623.4 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/gridlist'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; 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 focusDocs from 'docs:@react-aria/focus'; import checkboxDocs from 'docs:@react-aria/checkbox'; import utilsDocs from 'docs:@react-aria/utils'; import {HeaderInfo, FunctionAPI, TypeContext, InterfaceType, TypeLink, PageDescription} from '@react-spectrum/docs'; import {Keyboard} from '@react-spectrum/text'; import packageData from '@react-aria/gridlist/package.json'; import Anatomy from './anatomy.svg';


category: Collections keywords: [list, aria, grid]

useGridList

<PageDescription>Provides the behavior and accessibility implementation for a list component with interactive children. A grid list displays data in a single column and enables a user to navigate its contents via directional navigation keys.</PageDescription>

<HeaderInfo packageData={packageData} componentNames={['useGridList', 'useGridListItem', 'useGridListSelectionCheckbox']} sourceData={[ {type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/grid/'} ]} />

API

<FunctionAPI function={docs.exports.useGridList} links={docs.links} /> <FunctionAPI function={docs.exports.useGridListItem} links={docs.links} /> <FunctionAPI function={docs.exports.useGridListSelectionCheckbox} links={docs.links} />

Features

A list can be built using <ul> or <ol> HTML elements, but does not support any user interactions. HTML lists are meant for static content, rather than lists with rich interactions like focusable elements within rows, keyboard navigation, row selection, etc. useGridList helps achieve accessible and interactive list components that can be styled as needed.

  • Item selection – Single or multiple selection, with optional checkboxes, disabled rows, and both toggle and replace selection behaviors.
  • Interactive children – List items may include interactive elements such as buttons, checkboxes, menus, etc.
  • Actions – Items support optional row actions such as navigation via click, tap, double click, or <Keyboard>Enter</Keyboard> key.
  • Async loading – Support for loading items asynchronously, with infinite and virtualized scrolling.
  • Keyboard navigation – List items and focusable children can be navigated using the arrow keys, along with page up/down, home/end, etc. Typeahead, auto scrolling, and selection modifier keys are supported as well.
  • Touch friendly – Selection and actions adapt their behavior depending on the device. For example, selection is activated via long press on touch when item actions are present.
  • Accessible – Follows the ARIA grid pattern, with additional selection announcements via an ARIA live region. Extensively tested across many devices and assistive technologies to ensure announcements and behaviors are consistent.

Note: Use useGridList when your list items may contain interactive elements such as buttons, checkboxes, menus, etc. within them. If your list items contain only static content such as text and images, then consider using useListBox instead for a slightly better screen reader experience (especially on mobile).

Anatomy

<Anatomy role="img" aria-label="Anatomy diagram of a list container, consisting of multiple list items. Each list item contains a selection checkbox, a list item row, and a list item cell." />

A grid list consists of a container element, with rows of data inside. The rows within a list may contain focusable elements or plain text content. If the list supports row selection, each row can optionally include a selection checkbox.

The <TypeLink links={docs.links} type={docs.exports.useGridList} /> and <TypeLink links={docs.links} type={docs.exports.useGridListItem} /> hooks handle keyboard, mouse, and other interactions to support row selection, in list navigation, and overall focus behavior. Those hooks handle exposing the list and its contents to assistive technology using ARIA. <TypeLink links={docs.links} type={docs.exports.useGridListSelectionCheckbox} /> handles row selection and associating each checkbox with its respective rows for assistive technology.

useGridList returns props that you should spread onto the list container element:

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

useGridListItem 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.useGridListItem.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 where applicable.

Note that an aria-label or aria-labelledby must be passed to the list to identify the element to assistive technology.

State management

useGridList requires knowledge of the rows in the list 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

Lists are collection components that include rows as child elements. In this example, we'll use the standard HTML unordered list elements along with hooks from React Aria for each child. You may also use other elements like <div> to render these components as appropriate. We'll walk through creating the list container and list item, then add some additional behavior such as selection.

The <TypeLink links={docs.links} type={docs.exports.useGridList} /> hook will be used to render the outer most list element. It uses the <TypeLink links={statelyDocs.links} type={statelyDocs.exports.useListState} /> hook to construct the list's collection of rows, and manage state such as the focused row and row selection. We'll use the collection to iterate through the rows of the List and render the relevant components, which we'll define below.

You may notice the extra <div> with gridCellProps in our example. This is needed because we are following the ARIA grid pattern, which does not allow rows without any child gridcell elements.

tsx
import {useListState} from '@react-stately/list';
import {mergeProps} from '@react-aria/utils';
import {useRef} from 'react';
import {useFocusRing} from '@react-aria/focus';
import {useGridList, useGridListItem} from '@react-aria/gridlist';

function List(props) {
  let state = useListState(props);
  let ref = useRef<HTMLUListElement | null>(null);
  let { gridProps } = useGridList(props, state, ref);

  return (
    <ul {...gridProps} ref={ref} className="list">
      {[...state.collection].map((item) => (
        <ListItem key={item.key} item={item} state={state} />
      ))}
    </ul>
  );
}

function ListItem({ item, state }) {
  let ref = React.useRef(null);
  let {rowProps, gridCellProps, isPressed} = useGridListItem(
    {node: item},
    state,
    ref
  );

  let {isFocusVisible, focusProps} = useFocusRing();
  let showCheckbox = state.selectionManager.selectionMode !== 'none' && state.selectionManager.selectionBehavior === 'toggle';

  return (
    <li
      {...mergeProps(rowProps, focusProps)}
      ref={ref}
      className={`${isPressed ? 'pressed' : ''} ${isFocusVisible ? 'focus-visible' : ''}`}>
      <div {...gridCellProps}>
        {showCheckbox && <ListCheckbox item={item} state={state} />}
        {item.rendered}
      </div>
    </li>
  );
}

Now we can render a basic example list, with multiple selection and interactive children in each item.

tsx
import {Item} from "@react-stately/collections";

// Reuse the Button from your component library. See below.
import {Button} from 'your-component-library';

<List aria-label="Example List" selectionMode="multiple" selectionBehavior="replace">
  <Item textValue="Charizard">
    Charizard
    <Button onPress={() => alert(`Info for Charizard...`)}>Info</Button>
  </Item>
  <Item textValue="Blastoise">
    Blastoise
    <Button onPress={() => alert(`Info for Blastoise...`)}>Info</Button>
  </Item>
  <Item textValue="Venusaur">
    Venusaur
    <Button onPress={() => alert(`Info for Venusaur...`)}>Info</Button>
  </Item>
  <Item textValue="Pikachu">
    Pikachu
    <Button onPress={() => alert(`Info for Pikachu...`)}>Info</Button>
  </Item>
</List>
<details> <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show CSS</summary>
css
.list {
  padding: 0;
  list-style: none;
  background: var(--page-background);
  border: 1px solid var(--spectrum-global-color-gray-400);
  max-width: 400px;
  min-width: 200px;
  max-height: 250px;
  overflow: auto;
}

.list li {
  padding: 8px;
  outline: none;
  cursor: default;
}

.list li:nth-child(2n) {
  background: var(--spectrum-alias-highlight-hover);
}

.list li.pressed {
  background: var(--spectrum-global-color-gray-200);
}

.list li[aria-selected=true] {
  background: slateblue;
  color: white;
}

.list li.focus-visible {
  outline: 2px solid slateblue;
  outline-offset: -3px;
}

.list li.focus-visible[aria-selected=true] {
  outline-color: white;
}

.list li[aria-disabled] {
  opacity: 0.4;
}

.list [role=gridcell] {
  display: flex;
  align-items: center;
  gap: 4px;
}

.list li button {
  margin-left: auto;
}

/* iOS Safari has a bug that prevents accent-color: white from working. */
@supports not (-webkit-touch-callout: none) {
  .list li input[type=checkbox] {
    accent-color: white;
  }
}
</details>

Adding selection checkboxes

Next, let's add support for selection checkboxes to allow the user to select items explicitly. This is done using the <TypeLink links={docs.links} type={docs.exports.useGridListSelectionCheckbox} /> hook. It is passed the key of the item it is contained within. When the user checks or unchecks the checkbox, the row will be added or removed from the List's selection.

The Checkbox component used in this example is independent and can be used separately from useGridList. The code is available below.

tsx
import {useGridListSelectionCheckbox} from '@react-aria/gridlist';

// Reuse the Checkbox from your component library. See below for details.
import {Checkbox} from 'your-component-library';

function ListCheckbox({ item, state }) {
  let { checkboxProps } = useGridListSelectionCheckbox({ key: item.key }, state);
  return <Checkbox {...checkboxProps} />;
}

The following example shows an example list with multiple selection using checkboxes and the default toggle selection behavior.

tsx
<List aria-label="List with selection" selectionMode="multiple">
  <Item textValue="Charizard">
    Charizard
    <Button onPress={() => alert(`Info for Charizard...`)}>Info</Button>
  </Item>
  <Item textValue="Blastoise">
    Blastoise
    <Button onPress={() => alert(`Info for Blastoise...`)}>Info</Button>
  </Item>
  <Item textValue="Venusaur">
    Venusaur
    <Button onPress={() => alert(`Info for Venusaur...`)}>Info</Button>
  </Item>
  <Item textValue="Pikachu">
    Pikachu
    <Button onPress={() => alert(`Info for Pikachu...`)}>Info</Button>
  </Item>
</List>

And that's it! We now have a fully interactive List component that can support keyboard navigation, single or multiple selection. In addition, it is fully accessible for screen readers and other assistive technology. See below for more examples of how to use the List component that we've built.

Checkbox

The Checkbox component is used in the above example for row selection. It is built using the useCheckbox hook, and can be shared with many other components.

<details> <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show code</summary>
tsx
import {useCheckbox} from '@react-aria/checkbox';
import {useToggleState} from '@react-stately/toggle';

function Checkbox(props) {
  let inputRef = useRef(null);
  let { inputProps } = useCheckbox(
    props,
    useToggleState(props),
    inputRef
  );
  return <input {...inputProps} ref={inputRef} />;
}
</details>

Button

The Button component is used in the above example to show how rows can contain interactive elements. It is built using the useButton hook, and can be shared with many other components.

<details> <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show code</summary>
tsx
import {useButton} from '@react-aria/button';

function Button(props) {
  let ref = React.useRef(null);
  let {buttonProps} = useButton(props, ref);
  return <button {...buttonProps} ref={ref}>{props.children}</button>;
}
</details>

Usage

Dynamic collections

So far, our examples have shown static collections, where the data is hard coded. Dynamic collections, as shown below, can be used when the data comes from an external data source such as an API, or updates over time. In the example below, the rows are provided to the List via a render function.

tsx
function ExampleList(props) {
  let rows = [
    {id: 1, name: 'Games'},
    {id: 2, name: 'Program Files'},
    {id: 3, name: 'bootmgr'},
    {id: 4, name: 'log.txt'}
  ];

  return (
    <List aria-label="Example dynamic collection List" selectionMode="multiple" items={rows} {...props}>
      {item => (
        <Item textValue={item.name}>
          {item.name}
          <Button onPress={() => alert(`Info for ${item.name}...`)}>
            Info
          </Button>
        </Item>
      )}
    </List>
  );
}

Single selection

By default, useListState doesn't allow row selection but this can be enabled using the selectionMode prop. Use defaultSelectedKeys to provide a default set of selected rows. Note that the value of the selected keys must match the key prop of the row.

The example below enables single selection mode, and uses defaultSelectedKeys to select the row with key equal to "2". A user can click on a different row to change the selection, or click on the same row again to deselect it entirely.

tsx
// Using the example above
<ExampleList aria-label="List with single selection" selectionMode="single" selectionBehavior="replace" defaultSelectedKeys={[2]} />

Multiple selection

Multiple selection can be enabled by setting selectionMode to multiple.

tsx
<ExampleList aria-label="List with multiple selection" selectionMode="multiple" defaultSelectedKeys={[2, 4]} />

Disallow empty selection

useGridList also supports a disallowEmptySelection prop which forces the user to have at least one row in the List selected at all times. In this mode, if a single row is selected and the user presses it, it will not be deselected.

tsx
<ExampleList aria-label="List with disallowed empty selection" selectionMode="multiple" defaultSelectedKeys={[2]} disallowEmptySelection />

Controlled selection

To programmatically control row selection, use the selectedKeys prop paired with the onSelectionChange callback. The key prop from the selected rows will be passed into the callback when the row is pressed, allowing you to update state accordingly.

tsx
function PokemonList(props) {
  let rows = [
    {id: 1, name: 'Charizard'},
    {id: 2, name: 'Blastoise'},
    {id: 3, name: 'Venusaur'},
    {id: 4, name: 'Pikachu'}
  ];

  let [selectedKeys, setSelectedKeys] = React.useState(new Set([2]));

  return (
    <List aria-label="List with controlled selection" items={rows} selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} {...props}>
      {item => <Item>{item.name}</Item>}
    </List>
  );
}

Disabled rows

You can disable specific rows by providing an array of keys to useListState via the disabledKeys prop. This will disable all interactions on disabled rows, unless the disabledBehavior prop is used to change this behavior. Note that you are responsible for the styling of disabled rows, however, the selection checkbox will be automatically disabled.

tsx
// Using the example above
<PokemonList aria-label="List with disabled rows" selectionMode="multiple" disabledKeys={[3]} />

When disabledBehavior is set to selection, interactions such as focus, dragging, or actions can still be performed on disabled rows.

tsx
<PokemonList aria-label="List with selection disabled for disabled rows" selectionMode="multiple" disabledKeys={[3]} disabledBehavior="selection" />

Selection behavior

By default, useGridList 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. The "toggle" selection mode is often paired with a checkbox in each row as an explicit affordance for 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 styles implement the behaviors defined in Aria Practices.

tsx
<PokemonList aria-label="List with replace selection behavior" selectionMode="multiple" selectionBehavior="replace" />

Row actions

useGridList supports row actions via the onAction prop, which is useful for functionality such as navigation. When nothing is selected, the list performs actions by default when clicking or tapping a row. Items may be selected using the checkbox, or by long pressing on touch devices. When at least one item is selected, the list is in selection mode, and clicking or tapping a row toggles the selection. Actions may also be triggered via the <Keyboard>Enter</Keyboard> key, and selection using the <Keyboard>Space</Keyboard> key.

This behavior is slightly different when selectionBehavior="replace", where single clicking selects the row and actions are performed via double click. Touch and keyboard behaviors are unaffected.

tsx
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 24 }}>
  <ExampleList aria-label="Checkbox selection list with row actions" selectionMode="multiple" selectionBehavior="toggle" onAction={key => alert(`Opening item ${key}...`)} />
  <ExampleList aria-label="Highlight selection list with row actions" selectionMode="multiple" selectionBehavior="replace" onAction={key => alert(`Opening item ${key}...`)} />
</div>

Items in a GridList may also be links to another page or website. This can be achieved by passing the href prop to the <Item> component. Links behave the same way as described above for row actions depending on the selectionMode and selectionBehavior.

tsx
<List aria-label="Links" selectionMode="multiple">
  <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>
</List>
css
.list li[data-href] {
  cursor: pointer;
}

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.

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';

function AsyncList() {

  let list = useAsyncList({
    async load({signal, cursor}) {
      if (cursor) {
        cursor = cursor.replace(/^http:\/\//i, 'https://');
      }

      let res = await fetch(cursor || `https://swapi.py4e.com/api/people/?search=`, {signal});
      let json = await res.json();

      return {
        items: json.results,
        cursor: json.next
      };
    }
  });

  return (
    <List
      selectionMode="multiple"
      aria-label="Async loading ListView example"
      items={list.items}>
      {(item) => (
        <Item key={item.name}>{item.name}</Item>
      )}
    </List>
  );
}

Internationalization

useGridList handles some aspects of internationalization automatically. For example, type to select is implemented with an Intl.Collator for internationalized string matching, and keyboard navigation is mirrored in right-to-left languages. You are responsible for localizing all text content within the List.

RTL

In right-to-left languages, the list layout should be mirrored. The row contents should be ordered from right to left and the row's text alignment should be inverted. Ensure that your CSS accounts for this.