Skip to content

DataCard

DataCard is a container comprised of data displayed in a card format. This component can be used in place of both Table and DataGrid at breakpoints 600px and below. DataCard is one of multiple variants that can be used within DataView.
A pointing to the entire card; B pointing to the checkbox what will select the card; C pointing to one of the many heading/content records
DataCard and its sub-components
Anatomy of the DataCard component
ItemNameDescription
A<DataCard />The <DataCard /> contains the data for each individual card
B<DataCardCheckbox />The <Checkbox /> allows users the choice to select a <DataCard /> for performing bulk actions. <Checkbox /> is optional and can be turned off when a<DataCard /> is not meant to be selectable.
C<DataCardRecord />The <DataCardRecord /> is comprised of a heading prop that displays the row’s title and a row prop that displays the row’s data
D<Menu /><Menu /> with <IconButton /> is used when multiple actions are associated with the <DataCard />. If there's only a single action, use <Button /> instead and relocate the action from the top right-hand corner to the bottom of the <DataCard />.

import React from 'react'
import {
Box,
Button,
Checkbox,
CheckboxGroup,
ControlLayout,
DataCard,
DataCardCheckbox,
DataCardList,
DataCardRecord,
Dialog,
DialogActions,
DialogBody,
DialogFooter,
DialogHeader,
IconButton,
Input,
Label,
Link,
Menu,
MenuItem,
Pagination,
PaginationNavigation,
PaginationNavigationButton,
PaginationPerPageSelectField,
TextField,
useDialog,
useId,
useMenu,
} from '@gusto/workbench'
import { CaretDown, More, Search } from '@gusto/workbench-icons'
import { employees } from './_employees'
export const Basic = () => {
// default "columns" checkbox set on page load
const defaultColumnsOptions = [
'name',
'department',
'jobTitle',
'emailAddress',
]
const emailId = useId()
// default filters and their selection on page load
const defaultFilterNames = {
Department: [],
Title: [],
}
// Menu props for the actionsMenu and filterColumnsMenu in `sm` breakpoint
const [actionsMenuProps, actionsMenuButtonProps] = useMenu()
const [filterColumnsMenuProps, filterColumnsMenuButtonProps] = useMenu()
// filter and "columns" Dialog 'open' state
const [filtersDialogProps, filtersDialogButtonProps] = useDialog()
const [columnsDialogProps, columnsDialogButtonProps] = useDialog()
const [cardSelected, setCardSelected] = React.useState<{
[key: number]: boolean
}>({})
// used to keep track of the onChange of the "columns" checkboxes in columnsDialog
const [columnsCheckboxChecked, setColumnsCheckboxChecked] = React.useState<
string[]
>(defaultColumnsOptions)
// used to make changes to the table on "Apply" of the filters dialog
const [columnsDisplayed, setColumnsDisplayed] = React.useState<string[]>(
defaultColumnsOptions,
)
// used to keep track of the onChange of the filter checkboxes
const [filterCheckboxChecked, setFilterCheckboxChecked] = React.useState<{
[key: string]: string[]
}>(defaultFilterNames)
// used to make changes to the table on "Apply" of the filters dialog
const [appliedFilterCheckboxes, setAppliedFilterCheckboxes] = React.useState<{
[key: string]: string[]
}>(defaultFilterNames)
// used to determine filter count, if filters are applied, and filters data
const filterCount =
appliedFilterCheckboxes.Department.length +
appliedFilterCheckboxes.Title.length
const isDataFiltered =
appliedFilterCheckboxes.Department.length > 0 ||
appliedFilterCheckboxes.Title.length > 0
const filteredEmployees = isDataFiltered
? employees.filter(employee => {
return (
appliedFilterCheckboxes.Department.includes(employee.Department) ||
appliedFilterCheckboxes.Title.includes(employee.Title)
)
})
: employees
const [perPage, setPerPage] = React.useState(10)
const [currentPage, setCurrentPage] = React.useState(1)
// used to determine if the pagination buttons are disabled
const lastPage = Math.ceil(filteredEmployees.length / perPage)
const isFirstPage = currentPage === 1
const isLastPage = currentPage === lastPage
const paginatedEmployees = filteredEmployees.slice(
(currentPage - 1) * perPage,
currentPage * perPage,
)
// used to determine if the "select all" checkbox is checked, indeterminate, or unchecked
const cardSelectedCount = Object.values(cardSelected).filter(
val => val,
).length
const selectAllChecked = cardSelectedCount === paginatedEmployees.length
const selectAllIndeterminate = cardSelectedCount > 0 && !selectAllChecked
// an example of keeping track of rows selected
const toggleSelected = (id: number) => {
setCardSelected(currSelected => ({
...currSelected,
[id]: !cardSelected[id],
}))
}
// an example of creating the "select all" functionality
const toggleAllRows = () => {
paginatedEmployees
.map(employee => employee.Digit)
.forEach(digit => {
setCardSelected(currRowsSelected => ({
...currRowsSelected,
[digit]: !selectAllChecked,
}))
})
}
// an example of keeping a state management of each columns checkbox being checked
const columnsCheckboxOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const checkboxChecked = e.target.checked
if (checkboxChecked) {
setColumnsCheckboxChecked(currChecked => [...currChecked, e.target.value])
} else {
setColumnsCheckboxChecked(currChecked =>
currChecked.filter(value => value !== e.target.value),
)
}
}
// an example of resetting columns options
const resetColumnsOptions = () => {
setColumnsCheckboxChecked(defaultColumnsOptions)
}
// an example of updating the visibility of columns
const updateColumnVisibility = () => {
setColumnsDisplayed(columnsCheckboxChecked)
}
// an example of keeping a state management of each filter checkbox being checked
const filterCheckboxOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// getting the group name from CheckboxGroup
const groupName = e.target.name
const checkboxChecked = e.target.checked
if (checkboxChecked) {
setFilterCheckboxChecked(currChecked => ({
...currChecked,
[groupName]: [...currChecked[groupName], e.target.value],
}))
} else {
setFilterCheckboxChecked(currChecked => ({
...currChecked,
[groupName]: currChecked[groupName].filter(
value => value !== e.target.value,
),
}))
}
}
// an example of applying filters on "apply" button submit within filters Dialog
const applyFilters = () => {
filtersDialogProps.onClose()
setAppliedFilterCheckboxes(filterCheckboxChecked)
}
// an example of resetting filters within the filters Dialog
const resetFilters = () => {
setFilterCheckboxChecked(defaultFilterNames)
}
return (
<Box maxHeight={840} overflow="auto">
<Box maxWidth={490} margin="auto" padding={6}>
<ControlLayout
searchField={
<TextField
label="Search"
before={<Search />}
placeholder="Search"
visuallyHideLabel
/>
}
selectAllCheckbox={
<Checkbox
type="checkbox"
label={
cardSelectedCount > 0
? `${cardSelectedCount} Selected`
: `Select all (${paginatedEmployees.length})`
}
checked={selectAllChecked}
indeterminate={selectAllIndeterminate}
onChange={toggleAllRows}
/>
}
selectAllActionsMenu={
selectAllChecked || selectAllIndeterminate ? (
<>
<Button
{...actionsMenuButtonProps}
after={<CaretDown />}
size="small"
variant="primary"
>
Actions
</Button>
<Menu
{...actionsMenuProps}
aria-label="Employee information actions menu"
>
<MenuItem>Lorem ipsum</MenuItem>
<MenuItem>Lorem ipsum</MenuItem>
</Menu>
</>
) : null
}
actions={
<>
<IconButton
{...filterColumnsMenuButtonProps}
title="More actions"
>
<More />
</IconButton>
<Menu
{...filterColumnsMenuProps}
aria-label="Employee information filters menu"
>
<MenuItem {...filtersDialogButtonProps}>
Filter{filterCount > 0 ? ` (${filterCount})` : null}
</MenuItem>
<MenuItem {...columnsDialogButtonProps}>Columns</MenuItem>
</Menu>
<Dialog
aria-label="Employee information filters"
{...filtersDialogProps}
>
<DialogHeader>Filters</DialogHeader>
<DialogBody>
<Box marginBottom={6}>
<CheckboxGroup name="Department" legend="Departments">
{Array.from(
new Set(employees.map(employee => employee.Department)),
).map(department => {
return (
<Checkbox
key={`emp-type-checkbox-${department}`}
value={department}
label={department}
checked={filterCheckboxChecked.Department.includes(
department,
)}
onChange={filterCheckboxOnChange}
/>
)
})}
</CheckboxGroup>
</Box>
<Box marginBottom={6}>
<CheckboxGroup name="Title" legend="Job title">
{Array.from(
new Set(employees.map(employee => employee.Title)),
).map(title => {
return (
<Checkbox
key={`emp-type-checkbox-${title}`}
value={title}
label={title}
checked={filterCheckboxChecked.Title.includes(
title,
)}
onChange={filterCheckboxOnChange}
/>
)
})}
</CheckboxGroup>
</Box>
</DialogBody>
<DialogFooter>
<DialogActions>
<Button variant="primary" onClick={applyFilters}>
Apply
</Button>
<Button onClick={resetFilters}>Clear all filters</Button>
</DialogActions>
</DialogFooter>
</Dialog>
<Dialog
aria-label="Employee information visible columns"
{...columnsDialogProps}
>
<DialogHeader>Columns</DialogHeader>
<DialogBody>
<CheckboxGroup
name="columnsOptions"
legend="Column options"
visuallyHideLabel
>
<Checkbox
value="name"
label="Name"
checked={columnsCheckboxChecked.includes('name')}
onChange={columnsCheckboxOnChange}
/>
<Checkbox
value="department"
label="Department"
checked={columnsCheckboxChecked.includes('department')}
onChange={columnsCheckboxOnChange}
/>
<Checkbox
value="jobTitle"
label="Job title"
checked={columnsCheckboxChecked.includes('jobTitle')}
onChange={columnsCheckboxOnChange}
/>
<Checkbox
value="emailAddress"
label="Email address"
checked={columnsCheckboxChecked.includes('emailAddress')}
onChange={columnsCheckboxOnChange}
/>
</CheckboxGroup>
</DialogBody>
<DialogFooter>
<DialogActions>
<Button
variant="primary"
onClick={() => {
columnsDialogProps.onClose()
updateColumnVisibility()
}}
>
Apply
</Button>
<Button onClick={resetColumnsOptions}>Reset</Button>
</DialogActions>
</DialogFooter>
</Dialog>
</>
}
/>
<DataCardList aria-label="Employee information">
{paginatedEmployees.map(employee => {
return (
<DataCard
aria-label={employee['Name-FL']}
key={`emp-${employee.Digit}`}
selected={cardSelected[employee.Digit]}
>
<DataCardCheckbox
label={`Select ${employee['Name-FL']}`}
checked={cardSelected[employee.Digit] || false}
onChange={() => toggleSelected(employee.Digit)}
/>
{columnsDisplayed.includes('name') ? (
<DataCardRecord heading="Name">
<Link href="https://gusto.com/">{employee['Name-FL']}</Link>
</DataCardRecord>
) : null}
{columnsDisplayed.includes('department') ? (
<DataCardRecord heading="Department">
<Link href="https://gusto.com/">{employee.Department}</Link>
</DataCardRecord>
) : null}
{columnsDisplayed.includes('jobTitle') ? (
<DataCardRecord heading="Job title">
<Link href="https://gusto.com/">{employee.Title}</Link>
</DataCardRecord>
) : null}
{columnsDisplayed.includes('emailAddress') ? (
<DataCardRecord
heading={
<Label htmlFor={emailId + String(employee.Digit)}>
Email Address
</Label>
}
>
<Input
autoComplete="email"
id={emailId + String(employee.Digit)}
value={employee.Email}
readOnly
/>
</DataCardRecord>
) : null}
</DataCard>
)
})}
</DataCardList>
<Box marginTop={4}>
<Pagination aria-label="Employee information list" variant="floating">
<PaginationPerPageSelectField
value={perPage}
onChange={e => setPerPage(parseInt(e.target.value, 10))}
/>
<PaginationNavigation
hasPreviousPage={!isFirstPage}
hasNextPage={!isLastPage}
>
<PaginationNavigationButton
variant="first"
onClick={() => setCurrentPage(1)}
/>
<PaginationNavigationButton
variant="previous"
onClick={() => setCurrentPage(currentPage - 1)}
/>
<PaginationNavigationButton
variant="next"
onClick={() => setCurrentPage(currentPage + 1)}
/>
<PaginationNavigationButton
variant="last"
onClick={() => setCurrentPage(lastPage)}
/>
</PaginationNavigation>
</Pagination>
</Box>
</Box>
</Box>
)
}

import React from 'react'
import {
DataCard,
DataCardCheckbox,
DataCardList,
DataCardRecord,
Flex,
Input,
Label,
Link,
useId,
} from '@gusto/workbench'
export const DataCardNoActions = ({ ...props }) => {
const [cardSelected, setCardSelected] = React.useState<{
[key: number]: boolean
}>({})
const emailId = useId()
// an example of keeping track of rows selected
const toggleSelected = (id: number) => {
setCardSelected(currSelected => ({
...currSelected,
[id]: !cardSelected[id],
}))
}
return (
<Flex justifyContent="center">
<DataCardList {...props} aria-label="Employee information">
<DataCard aria-label="Hannah Aredent" selected={cardSelected[0]}>
<DataCardCheckbox
label="Select Hannah Aredent"
checked={cardSelected[0] || false}
onChange={() => toggleSelected(0)}
/>
<DataCardRecord heading="Name">
<Link href="https://gusto.com/">Hannah Arendt</Link>
</DataCardRecord>
<DataCardRecord heading="Department">
<Link href="https://gusto.com/">Marketing</Link>
</DataCardRecord>
<DataCardRecord heading="Job title">
<Link href="https://gusto.com/">Account Manager</Link>
</DataCardRecord>
<DataCardRecord
heading={<Label htmlFor={emailId}>Email Address</Label>}
>
<Input
autoComplete="email"
id={emailId}
readOnly
/>
</DataCardRecord>
</DataCard>
</DataCardList>
</Flex>
)
}

import React from 'react'
import {
Button,
DataCard,
DataCardAction,
DataCardCheckbox,
DataCardList,
DataCardRecord,
Flex,
Input,
Label,
Link,
useId,
} from '@gusto/workbench'
export const DataCardOneAction = ({ ...props }) => {
const [cardSelected, setCardSelected] = React.useState<{
[key: number]: boolean
}>({})
const emailId = useId()
// an example of keeping track of rows selected
const toggleSelected = (id: number) => {
setCardSelected(currSelected => ({
...currSelected,
[id]: !cardSelected[id],
}))
}
return (
<Flex justifyContent="center">
<DataCardList {...props} aria-label="Employee information">
<DataCard aria-label="Hannah Aredent" selected={cardSelected[0]}>
<DataCardCheckbox
label="Select Hannah Aredent"
checked={cardSelected[0] || false}
onChange={() => toggleSelected(0)}
/>
<DataCardRecord heading="Name">
<Link href="https://gusto.com/">Hannah Arendt</Link>
</DataCardRecord>
<DataCardRecord heading="Department">
<Link href="https://gusto.com/">Marketing</Link>
</DataCardRecord>
<DataCardRecord heading="Job title">
<Link href="https://gusto.com/">Account Manager</Link>
</DataCardRecord>
<DataCardRecord
heading={<Label htmlFor={emailId}>Email Address</Label>}
>
<Input
autoComplete="email"
id={emailId}
readOnly
/>
</DataCardRecord>
<DataCardAction>
<Button variant="secondary" size="small">
Skip
</Button>
</DataCardAction>
</DataCard>
</DataCardList>
</Flex>
)
}

import React from 'react'
import {
DataCard,
DataCardActionsMenu,
DataCardCheckbox,
DataCardList,
DataCardRecord,
Flex,
IconButton,
Input,
Label,
Link,
Menu,
MenuItem,
useId,
useMenu,
} from '@gusto/workbench'
import { More } from '@gusto/workbench-icons'
export const DataCardWithMultipleActions = ({ ...props }) => {
const [cardSelected, setCardSelected] = React.useState<{
[key: number]: boolean
}>({})
const [menuProps, menuButtonProps] = useMenu()
const emailId = useId()
// an example of keeping track of rows selected
const toggleSelected = (id: number) => {
setCardSelected(currSelected => ({
...currSelected,
[id]: !cardSelected[id],
}))
}
return (
<Flex justifyContent="center">
<DataCardList {...props} aria-label="Employee information">
<DataCard aria-label="Hannah Aredent" selected={cardSelected[0]}>
<DataCardCheckbox
label="Select Hannah Aredent"
checked={cardSelected[0] || false}
onChange={() => toggleSelected(0)}
/>
<DataCardRecord heading="Name">
<Link href="https://gusto.com/">Hannah Arendt</Link>
</DataCardRecord>
<DataCardRecord heading="Department">
<Link href="https://gusto.com/">Marketing</Link>
</DataCardRecord>
<DataCardRecord heading="Job title">
<Link href="https://gusto.com/">Account Manager</Link>
</DataCardRecord>
<DataCardRecord
heading={<Label htmlFor={emailId}>Email Address</Label>}
>
<Input
autoComplete="email"
id={emailId}
readOnly
/>
</DataCardRecord>
<DataCardActionsMenu>
<IconButton {...menuButtonProps} title="More actions">
<More />
</IconButton>
<Menu {...menuProps} aria-label="All menu item variants">
<MenuItem>Action one</MenuItem>
<MenuItem>Action two</MenuItem>
<MenuItem>Action three</MenuItem>
</Menu>
</DataCardActionsMenu>
</DataCard>
</DataCardList>
</Flex>
)
}
React props
NameTypeDefaultDescription
aria-label  
string
-Set the `aria-label` attribute to the `<ul>` element.
aria-labelledby  
string
-Set the `aria-labelledby` attribute to the `<ul>` element.
children  
ReactNode
-The content of the component.
selected  
boolean
falseDetermine if the card is currently selected.
React props
NameTypeDefaultDescription
children  
ReactNode
-The content of the component.
React props
NameTypeDefaultDescription
children  
ReactNode
-The content of the component.
React props
NameTypeDefaultDescription
children  
ReactNode
-The content of the component.
label  Required
ReactNode
-Label text for the associated `<label>` of the checkbox.
React props
NameTypeDefaultDescription
aria-label  
string
-Set the `aria-label` attribute to the `<ul>` element.
aria-labelledby  
string
-Set the `aria-labelledby` attribute to the `<ul>` element.
children  
ReactNode
-The content of the component.
React props
NameTypeDefaultDescription
children  
ReactNode
-The content of the component.
heading  Required
ReactNode
-The heading for the record entry.

The following testing snippet(s) offer suggestions for testing the component using React Testing LibraryExternal link with occasional help from JestExternal link.

import {
DataCard,
DataCardCheckbox,
DataCardList,
DataCardRecord,
} from '@gusto/workbench';
it('tests for basic data card render and functionality', async () => {
render(
<DataCardList aria-label="employees">
<DataCard aria-label="Hannah Ardent">
<DataCardCheckbox
label="Select Hannah Aredent"
onChange={jest.fn()}
/>
<DataCardRecord heading="Full name">
Hannah Ardent
</DataCardRecord>
<DataCardRecord heading="Hours">
40
</DataCardRecord>
</DataCard>
<DataCard aria-label="Isaiah Berlin">
<DataCardCheckbox
label="Select Isaiah Berlin"
onChange={jest.fn()}
/>
<DataCardRecord heading="Full name">
Isaiah Berlin
</DataCardRecord>
<DataCardRecord heading="Hours">
32
</DataCardRecord>
</DataCard>
</DataCardList>
);
/** DataCardList */
// Ensure the data card list has the expected accessible name and reading
const dataCardList = screen.getByRole('list', { name: 'employees' });
expect(dataCardList).toBeInTheDocument();
expect(dataCardList).toHaveAccessibleName('employees');
/** DataCard */
// Ensure each data card has the expected accessible name and reading
const hannahCard = screen.getByRole('list', { name: 'Hannah Ardent' });
expect(hannahCard).toBeInTheDocument();
expect(hannahCard).toHaveAccessibleName('Hannah Ardent');
const isaiahCard = screen.getByRole('list', { name: 'Isaiah Berlin' });
expect(isaiahCard).toBeInTheDocument();
expect(isaiahCard).toHaveAccessibleName('Isaiah Berlin');
/** DataCardCheckbox */
// Assert data card selection
const hannahCheckbox = within(hannahCard).getByRole('checkbox', { name: 'Select entry for Hannah Ardent' });
// Click the row selection checkbox
userEvent.click(hannahCheckbox);
// Assert checkbox click expectations
})