ListBox

A control to display a list of items where one or more can be selected.

Installyarn add @diallink-corp/convergo-react-listbox
Version4.1.2
Usageimport {ListBox} from '@diallink-corp/convergo-react-listbox'

Static Collection

The ListBox component can either render static or dynamic collections of data. For simple ListBox components that need to render a list of pre-defined items a static collection should be used. Static collections cannot be changed over time.

<ListBox aria-label='What is your favorite fruit?'>
  <Item key='apples'>Apples</Item>
  <Item key='bananas'>Bananas</Item>
  <Item key='grapes'>Grapes</Item>
  <Item key='oranges'>Oranges</Item>
</ListBox>
<ListBox aria-label='What is your favorite fruit?'>
  <Item key='apples'>Apples</Item>
  <Item key='bananas'>Bananas</Item>
  <Item key='grapes'>Grapes</Item>
  <Item key='oranges'>Oranges</Item>
</ListBox>
<ListBox aria-label="What is your favorite fruit?">
  <Item key="apples">
    Apples
  </Item>
  <Item key="bananas">
    Bananas
  </Item>
  <Item key="grapes">
    Grapes
  </Item>
  <Item key="oranges">
    Oranges
  </Item>
</ListBox>

Dynamic Collection

For data that is non-static, such as data fetched from a backend, the ListBox component supports dynamic collections. Dynamic collections support adding, updating and removing of the data that is being rendered.

function Example() {
  const [fruits, setFruits] = useState([
    {id: 1, name: 'Apples'},
    {id: 2, name: 'Bananas'},
    {id: 3, name: 'Grapes'},
    {id: 4, name: 'Oranges'}
  ]);

  return (
    <ListBox aria-label='What is your favorite fruit?' items={fruits}>
      {(item) => <Item key={item.id}>{item.name}</Item>}
    </ListBox>
  );
}
function Example() {
  const [fruits, setFruits] = useState([
    { id: 1, name: 'Apples' },
    { id: 2, name: 'Bananas' },
    { id: 3, name: 'Grapes' },
    { id: 4, name: 'Oranges' }
  ]);

  return (
    <ListBox
      aria-label="What is your favorite fruit?"
      items={fruits}
    >
      {(item) => <Item key={item.id}>{item.name}</Item>}
    </ListBox>
  );
}
function Example() {
  const [
    fruits,
    setFruits
  ] = useState([
    {
      id: 1,
      name: 'Apples'
    },
    {
      id: 2,
      name: 'Bananas'
    },
    {
      id: 3,
      name: 'Grapes'
    },
    {
      id: 4,
      name: 'Oranges'
    }
  ]);

  return (
    <ListBox
      aria-label="What is your favorite fruit?"
      items={fruits}
    >
      {(item) => (
        <Item
          key={item.id}
        >
          {item.name}
        </Item>
      )}
    </ListBox>
  );
}

Asynchronous Fetching

The ListBox component supports asynchronous fetching of data via the onLoadMoreItems prop. The ListBox component uses its built-in infinite scrolling to achieve this. This means that when the user reaches the bottom of the list, the component will automatically load more data. If you provide an isLoading prop, the ListBox component will show a Spinner component, to indicate to the user, that more data is being fetched.

function Example() {
  const list = useAsyncListState({
    async loadItems({ cursor, signal }) {
      const response = await fetch(
        cursor || 'https://pokeapi.co/api/v2/pokemon',
        { signal }
      );
      const { results, next } = await response.json();
      return { items: results, cursor: next };
    }
  });

  return (
    <ListBox
      aria-label="What is your favorite fruit?"
      items={list.items}
      isLoading={list.isLoading}
      onLoadMoreItems={list.loadMoreItems}
      style={{ maxHeight: '300px' }}
    >
      {(item) => <Item key={item.name}>{item.name}</Item>}
    </ListBox>
  );
}
function Example() {
  const list = useAsyncListState({
    async loadItems({ cursor, signal }) {
      const response = await fetch(
        cursor || 'https://pokeapi.co/api/v2/pokemon',
        { signal }
      );
      const { results, next } = await response.json();
      return { items: results, cursor: next };
    }
  });

  return (
    <ListBox
      aria-label="What is your favorite fruit?"
      items={list.items}
      isLoading={list.isLoading}
      onLoadMoreItems={list.loadMoreItems}
      style={{ maxHeight: '300px' }}
    >
      {(item) => <Item key={item.name}>{item.name}</Item>}
    </ListBox>
  );
}
function Example() {
  const list =
    useAsyncListState({
      async loadItems(
        {
          cursor,
          signal
        }
      ) {
        const response =
          await fetch(
            cursor ||
              'https://pokeapi.co/api/v2/pokemon',
            { signal }
          );
        const {
          results,
          next
        } =
          await response
            .json();
        return {
          items: results,
          cursor: next
        };
      }
    });

  return (
    <ListBox
      aria-label="What is your favorite fruit?"
      items={list.items}
      isLoading={list
        .isLoading}
      onLoadMoreItems={list
        .loadMoreItems}
      style={{
        maxHeight:
          '300px'
      }}
    >
      {(item) => (
        <Item
          key={item.name}
        >
          {item.name}
        </Item>
      )}
    </ListBox>
  );
}

Uncontrolled Selection

By default, the ListBox component handles its selection uncontrolled. In the uncontrolled variant you can pass in a defaultSelectedKeys to select one or more values by default. The key that is being referenced here is the value that is passed to the Item component as a key prop, so in this case item.name.

function Example() {
  const fruits = [
    { id: 1, name: 'Apples' },
    { id: 2, name: 'Bananas' },
    { id: 3, name: 'Grapes' },
    { id: 4, name: 'Oranges' }
  ];

  return (
    <ListBox
      aria-label="Fruits"
      items={fruits}
      defaultSelectedKeys={['Grapes']}
      selectionMode="single"
    >
      {(item) => <Item key={item.name}>{item.name}</Item>}
    </ListBox>
  );
}
function Example() {
  const fruits = [
    { id: 1, name: 'Apples' },
    { id: 2, name: 'Bananas' },
    { id: 3, name: 'Grapes' },
    { id: 4, name: 'Oranges' }
  ];

  return (
    <ListBox
      aria-label="Fruits"
      items={fruits}
      defaultSelectedKeys={['Grapes']}
      selectionMode="single"
    >
      {(item) => <Item key={item.name}>{item.name}</Item>}
    </ListBox>
  );
}
function Example() {
  const fruits = [
    {
      id: 1,
      name: 'Apples'
    },
    {
      id: 2,
      name: 'Bananas'
    },
    {
      id: 3,
      name: 'Grapes'
    },
    {
      id: 4,
      name: 'Oranges'
    }
  ];

  return (
    <ListBox
      aria-label="Fruits"
      items={fruits}
      defaultSelectedKeys={[
        'Grapes'
      ]}
      selectionMode="single"
    >
      {(item) => (
        <Item
          key={item.name}
        >
          {item.name}
        </Item>
      )}
    </ListBox>
  );
}

Controlled Selection

The use the controlled selection variant, you can use the selectedKey and onSelectionChange props together.

function Example() {
  const fruits = [
    {id: 1, name: 'Apples'},
    {id: 2, name: 'Bananas'},
    {id: 3, name: 'Grapes'},
    {id: 4, name: 'Oranges'}
  ];
  const [selectedFruit, setSelectedFruit] = useState('Bananas');

  return (
    <ListBox
      aria-label='What is your favorite fruit?'
      items={fruits}
      selectionMode='multi'
      selectedKeys={[selectedFruit]}
      onSelectionChange={(value) => setSelectedFruit(value)}
    >
      {(item) => <Item key={item.name}><div>{item.name}</div></Item>}
    </ListBox>
  );
}
function Example() {
  const fruits = [
    { id: 1, name: 'Apples' },
    { id: 2, name: 'Bananas' },
    { id: 3, name: 'Grapes' },
    { id: 4, name: 'Oranges' }
  ];
  const [selectedFruit, setSelectedFruit] = useState(
    'Bananas'
  );

  return (
    <ListBox
      aria-label="What is your favorite fruit?"
      items={fruits}
      selectionMode="multi"
      selectedKeys={[selectedFruit]}
      onSelectionChange={(value) => setSelectedFruit(value)}
    >
      {(item) => (
        <Item key={item.name}>
          <div>{item.name}</div>
        </Item>
      )}
    </ListBox>
  );
}
function Example() {
  const fruits = [
    {
      id: 1,
      name: 'Apples'
    },
    {
      id: 2,
      name: 'Bananas'
    },
    {
      id: 3,
      name: 'Grapes'
    },
    {
      id: 4,
      name: 'Oranges'
    }
  ];
  const [
    selectedFruit,
    setSelectedFruit
  ] = useState(
    'Bananas'
  );

  return (
    <ListBox
      aria-label="What is your favorite fruit?"
      items={fruits}
      selectionMode="multi"
      selectedKeys={[
        selectedFruit
      ]}
      onSelectionChange={(
        value
      ) =>
        setSelectedFruit(
          value
        )}
    >
      {(item) => (
        <Item
          key={item.name}
        >
          <div>
            {item.name}
          </div>
        </Item>
      )}
    </ListBox>
  );
}

Density

The ListBox supports various densities to control the size of the items.

Compact

<ListBox density='compact'>
  <Item key='apples'>Apples</Item>
  <Item key='bananas'>Bananas</Item>
  <Item key='grapes'>Grapes</Item>
  <Item key='oranges'>Oranges</Item>
</ListBox>
<ListBox density='compact'>
  <Item key='apples'>Apples</Item>
  <Item key='bananas'>Bananas</Item>
  <Item key='grapes'>Grapes</Item>
  <Item key='oranges'>Oranges</Item>
</ListBox>
<ListBox density="compact">
  <Item key="apples">
    Apples
  </Item>
  <Item key="bananas">
    Bananas
  </Item>
  <Item key="grapes">
    Grapes
  </Item>
  <Item key="oranges">
    Oranges
  </Item>
</ListBox>

Regular

<ListBox density='regular'>
  <Item key='apples'>Apples</Item>
  <Item key='bananas'>Bananas</Item>
  <Item key='grapes'>Grapes</Item>
  <Item key='oranges'>Oranges</Item>
</ListBox>
<ListBox density='regular'>
  <Item key='apples'>Apples</Item>
  <Item key='bananas'>Bananas</Item>
  <Item key='grapes'>Grapes</Item>
  <Item key='oranges'>Oranges</Item>
</ListBox>
<ListBox density="regular">
  <Item key="apples">
    Apples
  </Item>
  <Item key="bananas">
    Bananas
  </Item>
  <Item key="grapes">
    Grapes
  </Item>
  <Item key="oranges">
    Oranges
  </Item>
</ListBox>

Spacious

<ListBox density='spacious'>
  <Item key='apples'>Apples</Item>
  <Item key='bananas'>Bananas</Item>
  <Item key='grapes'>Grapes</Item>
  <Item key='oranges'>Oranges</Item>
</ListBox>
<ListBox density='spacious'>
  <Item key='apples'>Apples</Item>
  <Item key='bananas'>Bananas</Item>
  <Item key='grapes'>Grapes</Item>
  <Item key='oranges'>Oranges</Item>
</ListBox>
<ListBox density="spacious">
  <Item key="apples">
    Apples
  </Item>
  <Item key="bananas">
    Bananas
  </Item>
  <Item key="grapes">
    Grapes
  </Item>
  <Item key="oranges">
    Oranges
  </Item>
</ListBox>

Empty State

To show the user that the list box is empty when there is no data available you can use the renderEmptyState prop.

<ListBox renderEmptyState={() => <Text>No data was found</Text>}>
  {[]}
</ListBox>
<ListBox
  renderEmptyState={() => <Text>No data was found</Text>}
>
  {[]}
</ListBox>
<ListBox
  renderEmptyState={() => (
    <Text>
      No data was found
    </Text>
  )}
>
  {[]}
</ListBox>

Slots

The ListBox comes with pre-styled slots for components like avatars, texts, descriptions and keyboard shortcuts.

function Example() {
  const list = useAsyncListState({
    async loadItems(state) {
      let { cursor, signal, filterValue } = state;

      if (cursor) {
        cursor = cursor.replace(/^http:\/\//i, 'https://');
      }

      const response = await fetch(
        cursor || `https://swapi.py4e.com/api/people/?search=${filterValue}`,
        { signal }
      );
      const { results, next } = await response.json();
      return { ...state, items: results, cursor: next };
    }
  });

  return (
    <ListBox
      aria-label="What is your favorite star wars character?"
      items={list.items}
      isLoading={list.isLoading}
      onLoadMoreItems={list.loadMoreItems}
      selectionMode="single"
      style={{ maxHeight: '300px', maxWidth: '250px' }}
    >
      {(item) => (
        <Item key={item.name}>
          <Avatar src="https://cdn-icons-png.flaticon.com/512/147/147144.png?w=360" />
          <Text>{item.name}</Text>
          <Description>Height: {item.height} Weight: {item.mass}</Description>
        </Item>
      )}
    </ListBox>
  );
}
function Example() {
  const list = useAsyncListState({
    async loadItems(state) {
      let { cursor, signal, filterValue } = state;

      if (cursor) {
        cursor = cursor.replace(/^http:\/\//i, 'https://');
      }

      const response = await fetch(
        cursor ||
          `https://swapi.py4e.com/api/people/?search=${filterValue}`,
        { signal }
      );
      const { results, next } = await response.json();
      return { ...state, items: results, cursor: next };
    }
  });

  return (
    <ListBox
      aria-label="What is your favorite star wars character?"
      items={list.items}
      isLoading={list.isLoading}
      onLoadMoreItems={list.loadMoreItems}
      selectionMode="single"
      style={{ maxHeight: '300px', maxWidth: '250px' }}
    >
      {(item) => (
        <Item key={item.name}>
          <Avatar src="https://cdn-icons-png.flaticon.com/512/147/147144.png?w=360" />
          <Text>{item.name}</Text>
          <Description>
            Height: {item.height} Weight: {item.mass}
          </Description>
        </Item>
      )}
    </ListBox>
  );
}
function Example() {
  const list =
    useAsyncListState({
      async loadItems(
        state
      ) {
        let {
          cursor,
          signal,
          filterValue
        } = state;

        if (cursor) {
          cursor = cursor
            .replace(
              /^http:\/\//i,
              'https://'
            );
        }

        const response =
          await fetch(
            cursor ||
              `https://swapi.py4e.com/api/people/?search=${filterValue}`,
            { signal }
          );
        const {
          results,
          next
        } =
          await response
            .json();
        return {
          ...state,
          items: results,
          cursor: next
        };
      }
    });

  return (
    <ListBox
      aria-label="What is your favorite star wars character?"
      items={list.items}
      isLoading={list
        .isLoading}
      onLoadMoreItems={list
        .loadMoreItems}
      selectionMode="single"
      style={{
        maxHeight:
          '300px',
        maxWidth: '250px'
      }}
    >
      {(item) => (
        <Item
          key={item.name}
        >
          <Avatar src="https://cdn-icons-png.flaticon.com/512/147/147144.png?w=360" />
          <Text>
            {item.name}
          </Text>
          <Description>
            Height:{' '}
            {item.height}
            {' '}
            Weight:{' '}
            {item.mass}
          </Description>
        </Item>
      )}
    </ListBox>
  );
}

Accessibility

In order to support internationalization, provide a localized string to the aria-label prop.

API