List View

Basics

List composition is based on using CSS grid to power the alignment and responsivness of rows. The main components used for composing lists are:

ListView

The main wrapper for your list. This component will wrap each row you are rendering. Accepted props:

  • children: Mainly your row components. Use ListRow for this type of list.
  • columns: Array to describe each column: { heading, width, loader }.
  • isLoadingRows: Boolean to idicate row loader should be shown.
  • onScrollToBottom: Callback function that will be called when list is scrolled to bottom. Used to load more items.
  • listStyle: Additional style props to pass to the list wrapper.

SelectableListView

Same as ListView, used to create selectable lists. Additional props:

  • selectionMap: A {[rowKey]: boolean} mapping that describes which rows are selected.
  • onSelectionChange: Callback function to be called with an updated map as selection changes so you can update your state.
  • disabled: Are the selection checkboxes disabled. This will not disable individual row clicks.
  • selectionAlwaysVisible: Checkboxes are always visible, this is great for mobile screens where you can’t hover to make the checkboxes appear.

Use SelectableListBoundary to enable drag to select: This component defines the area over which you can drag to select. Note that this component should have a selectable list some where down the component tree.

ListRow

Each row in the list should be rendered as a ListRow. Depending on your use case, you should use this component to wrap 1 or more ListRowCell as described below.

Props:

  • onClick: Callback to call when row is clicked.
  • disabled: Disables row clicks.
  • testId

SelectableListRow

Same as ListRow, used as a row in SelectableListView. You must provide a rowKey prop. This key represents the id of the row and is used when selected items change to update the selection map.

ListRowCell

Every row should contain at least one cell. A cell is used to group components, although the grouping largely depends on the use case. Avatars and Text are a good example of components that can be grouped together. The number of cells in each row should be the same, and the number of columns provided to colWidths should match the number of cells in each row.

ListItemAction is a special cell that contains row actions, and should be considered as any other cell.

Simple Example

Basic usage of the <ListView />. Each item is rendered as a ListRow with a single ListRowCell wrapping the content.

const renderListItem = (item) => (
  <ListRow>
    <ListRowCell>
      <Avatar mr={3} size={36} username={item.owner.username} />
      <ListText kind="bold">{item.title}</ListText>
    </ListRowCell>
  </ListRow>
);

return <ListView>{projectsSetA.map(renderListItem)}</ListView>;
SnapCat
Mono
Pro Finder
Marvel Fashion
Cat agency

Selectable Lists, actions, and drag boundaries

To create a selectable list, you will need to use a SelectableListView. Use a SelectableListRow to render selectable rows and provide rowKey as a unique id for the row. To enable drag to select, you can use SelectableListBoundary - this will define the area over which you can drag to trigger selection in a selectable list view somewhere further down the tree. In this example we also use ListRowActions to render row actions & buttons for each row.

// selectionMap is a {[rowKey]: boolean} mapping that represents
// the current state of selected items (true when selected, false or undefined otherwise)
const [selectionMap, setSelectionMap] = useState({});

// onSelectionChange will be called with an updated map when selection changes
const onSelectionChange = (val) => setSelectionMap(val);

const renderSelectableItem = (item) => (
  <SelectableListRow rowKey={item.pk}>
    <ListRowCell>
      <Avatar mr={3} size={36} username={item.owner.username} />
      <ListText kind="bold">{item.title}</ListText>
    </ListRowCell>
    <ListRowActions>
      <Button kind="ghost">Some Action</Button>
      <Button kind="danger">Delete</Button>
    </ListRowActions>
  </SelectableListRow>
);

// SelectableListBoundary wraps a Box with some padding and has SelectableListView further down the tree.
// This means the entire area defined by the Box, including the padding, is enabled as a drag area for that list.
return (
  <SelectableListBoundary boundaryName="Example Boundary">
    <Box padding={6}>
      <SelectableListView
        onSelectionChange={onSelectionChange}
        selectionMap={selectionMap}
      >
        {projectsSetA.map(renderSelectableItem)}
      </SelectableListView>
    </Box>
  </SelectableListBoundary>
);
SnapCat
Mono
Pro Finder
Marvel Fashion
Cat agency

Table-like lists & headings

To create a table layout, you need to pass a columns array to ListView or SelectableListView. Each column in the array should have the following properties:

  • heading - The text shown as the column’s heading (a string)
  • width - The width of the column. Any valid grid unit will work - it is recommended to use fr.
  • loader - A LoaderCellType, to describe the loading state placeholder (see infinite scroll example).

Each row will inherit the cell widths specified as colWidths.

As with previous examples, you can group cell content together by wrapping your components with ListRowCell.

const onSelectionChange = (val) => setSelectionMap(val);

const renderItem = (item) => (
  <SelectableListRow rowKey={item.pk} key={item.pk}>
    <ListRowCell>
      <Avatar mr={3} size={36} username={item.owner.username} />
      <ListText kind="bold">{item.title}</ListText>
    </ListRowCell>
    <ListRowCell>
      <ListText>Hello there</ListText>
    </ListRowCell>
    <ListRowActions>
      <Button kind="ghost">Some Action</Button>
      <Button kind="danger">Delete</Button>
    </ListRowActions>
  </SelectableListRow>
);

const columns = [
  { heading: 'User', width: '1fr' },
  { heading: 'Greeting', width: '1fr' },
  { heading: '', width: '1fr' },
];

return (
  <SelectableListView
    columns={columns}
    onSelectionChange={onSelectionChange}
    selectionMap={selectionMap}
  >
    {projectsSetA.map(renderItem)}
  </SelectableListView>
);
User
Greeting
SnapCat
Hello there
Mono
Hello there
Pro Finder
Hello there
Marvel Fashion
Hello there
Cat agency
Hello there

Example with filtering

The list itself has no knowledge of any filtering or sorting methods we use (or might use) in the app. It is simply a set of building blocks for rendering lists. This means that, Any filtering and sorting functionality should be done on the data you use to render your lists. You can combine other UI components to help you achieve this when you are building a more “specialized” list.

const [visibleItems, setVisibleItems] = useState(projectsSetA);
const [searchTerm, setSearchTerm] = useState('');

const onSearchTermChange = term => {
  setSearchTerm(term);

  if (term) {
    // Filter by title matching search term
    const filteredItems = visibleItems.filter(item => item.title.toLowerCase().indexOf(term.toLowerCase()) > -1);
    setVisibleItems(filteredItems);
  } else {
    setVisibleItems(projectsSetA);
  }
}

// Render your list items as usual
const renderItem = item => ...

return (
  <Box>
    <SearchInput onChange={onSearchTermChange} value={searchTerm}/>
    <ListView>{visibleItems.map(renderListItem)}</ListView>
  </Box>
);
SnapCat
Mono
Pro Finder
Marvel Fashion
Cat agency

Infinite scroll & loading additional items

To enable infinite scroll on either ListView or SelectableListView, pass the onScrollToBottom prop that will be called when the list is scrolled to the bottom. Pass the isLoadingRows prop when you are fetching more data for the list. Each column can specify its loader type. This will render the correct preloader graphic when the list is loading. Available loader types are: LoaderCellType.Text, LoaderCellType.Avatar and LoaderCellType.AvatarWithText.

Optionally, use the listStyle prop to pass additional styles for the list, such as maxHeight and overflow: scroll if you need to set the list in a fixed size scrollable container. If the callback passed to onScrollToBottom returns a Promise, the list view will not trigger the callback again as long as the ongoing async callback did not complete.

To display loading states for rows, use the ListRowLoader component. It takes the following props:

const [items, setItems] = useState(projectsSetA);
const [isLoading, setLoading] = useState(false);

// This function returns a promise and will be called when scrolling to bottom.
// In a real use case, you will be fetching real data here.
const loadMoreItems = () => {
  return new Promise((resolve) => {
    setLoading(true);

    setTimeout(() => {
      const newItems = someNewItems;
      setItems([...items, ...newItems]);
      setLoading(false);
      resolve();
    }, 3000);
  });
};

const renderListItem = (item) => (
  <ListRow>
    <ListRowCell>
      <Avatar mr={3} size={36} username={item.owner.username} />
      <ListText kind="bold">{item.title}</ListText>
    </ListRowCell>
  </ListRow>
);

const columns = [{ loader: LoaderCellType.AvatarWithText }];

return (
  <Box borderRadius={2} boxShadow="popover.default" marginTop={4}>
    <ListView
      columns={columns}
      isLoadingRows={isLoading}
      listStyle={{ padding: 6, height: '400px', overflow: 'scroll' }}
      onScrollToBottom={loadMoreItems}
    >
      {items.map(renderListItem)}
    </ListView>
  </Box>
);
SnapCat
Mono
Pro Finder
Marvel Fashion
Cat agency