Skip to content

DataGrid

DataGrid is used to organize and display data, particularly in scenarios that require customers to enter or edit data within individual cells. This component is one of multiple variants that can be used within DataView.
A pointing to a section for searching and various data interactions; B pointing to the header row of the DataGrid; C pointing to the data rows of the DataGrid; D pointing to the pagination of the DataGrid; E pointing to the entire grid of the DataGrid
DataGrid and its sub-components
Anatomy of the DataGrid component
ItemNameDescription
A<DataGrid />The <DataGrid /> contains the data of the DataGrid
B<DataGridHeader />The <DataGridHeader /> displays the columns title and allows them to sort. <DataGridHeader /> may also contain <DataGridSelectRowHeaderCell /> and <DataGridHeaderCell /> components
C<DataGridRow />The <DataGridRow /> displays particular data from column to column. <DataGridRow /> may also contain <DataGridSelectRowCell />, <DataGridHeaderCell span="row" />, and <DataGridDataCell /> components.
Select rowJob TitleEmail address
Liana VaroMarketingContractor
Henry CoteHuman ResourcesEngineer
Maria StephensMarketingChief Editor
Mark BrouwerAccounting and FinancePeople Operations
Tatiana RobuR&DDesigner
Ruby NguyenSalesSr. Engineer
Sergio GuevaraSalesAccount Manager
Rafe WalkerAccounting and FinanceInformation Security Specialist
Fabian LupeaLegalOffice Operations
Sienna BrownOperationsCTO
Zoe MordinMarketingRecruiter
Vinay KatwalPurchasingAccount Director
Charlie KingITProject Manager
Maria ManciniMarketingClient Support Manager
Dorothy BishopITMarketing Director
Victor DuboisSalesRecruiter
Phillip CarrollSalesClient Support Director
Willie MorrisonSecurityLevel 1 Tech Suport
Liam JonesEmployeeAssociate Creative Director
Felix RothHuman ResourcesContractor
Noah AndersonAccounting and FinanceEngineer
Sienna SmithOperationsSr. Designer
Craig EllisLegalOffice Administrator
Rosa AmadorSalesClient Support Manager
Alexis MullerITIT Specialist
Jasmine JacobHuman ResourcesIT Ops Lead
Emily LeeLegalCEO

import React from 'react'
import {
Box,
Button,
Checkbox,
CheckboxGroup,
ControlLayout,
DataGrid,
DataGridBody,
DataGridDataCell,
DataGridHeader,
DataGridHeaderCell,
DataGridRow,
DataGridSelectRowDataCell,
DataGridSelectRowHeaderCell,
Dialog,
DialogActions,
DialogBody,
DialogFooter,
DialogHeader,
Link,
Menu,
MenuItem,
Pagination,
PaginationNavigation,
PaginationNavigationButton,
PaginationPerPageSelectField,
TextField,
useDialog,
useMenu,
} from '@gusto/workbench'
import { CaretDown, Filter, 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',
]
// 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()
// filter and "columns" Dialog 'open' state
const [filtersDialogProps, filtersDialogButtonProps] = useDialog()
const [columnsDialogProps, columnsDialogButtonProps] = useDialog()
const [rowSelected, setRowSelected] = 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
// used to determine if the "select all" checkbox is checked, indeterminate, or unchecked
const rowSelectedCount = Object.values(rowSelected).filter(val => val).length
const selectAllChecked = rowSelectedCount === filteredEmployees.length
const selectAllIndeterminate = rowSelectedCount > 0 && !selectAllChecked
// an example of keeping track of rows selected
const toggleGridRowSelection = (id: number) => {
setRowSelected(currSelected => ({
...currSelected,
[id]: !rowSelected[id],
}))
}
// an example of creating the "select all" functionality
const toggleAllRows = () => {
employees
.map(employee => employee.Digit)
.forEach(digit => {
setRowSelected(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 (
<>
<ControlLayout
searchField={
<TextField
label="Search"
before={<Search />}
placeholder="Search"
visuallyHideLabel
/>
}
selectAllCheckbox={
<Checkbox
type="checkbox"
label={
rowSelectedCount > 0
? `${rowSelectedCount} Selected`
: `Select all (${filteredEmployees.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={
<>
<Button
variant="tertiary"
size="small"
before={<Filter />}
{...filtersDialogButtonProps}
>
Filter{filterCount > 0 ? ` (${filterCount})` : null}
</Button>
<Button
variant="tertiary"
size="small"
{...columnsDialogButtonProps}
>
Columns
</Button>
<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>
</>
}
/>
<DataGrid
aria-label="Employee information"
gridSectionStyles={{ maxHeight: '50rem' }}
>
<DataGridHeader>
<DataGridRow>
<DataGridSelectRowHeaderCell />
{columnsDisplayed.includes('name') && (
<DataGridHeaderCell sortable>Name</DataGridHeaderCell>
)}
{columnsDisplayed.includes('department') && (
<DataGridHeaderCell sortable>Department</DataGridHeaderCell>
)}
{columnsDisplayed.includes('jobTitle') && (
<DataGridHeaderCell>Job Title</DataGridHeaderCell>
)}
{columnsDisplayed.includes('emailAddress') && (
<DataGridHeaderCell>Email address</DataGridHeaderCell>
)}
</DataGridRow>
</DataGridHeader>
<DataGridBody>
{filteredEmployees.map(employee => {
return (
<DataGridRow
key={`emp-${employee.Digit}`}
rowSelected={rowSelected[employee.Digit]}
>
<DataGridSelectRowDataCell
label={`Select row ${employee['Name-FL']}`}
checked={rowSelected[employee.Digit] || false}
onChange={() => toggleGridRowSelection(employee.Digit)}
/>
{columnsDisplayed.includes('name') && (
<DataGridHeaderCell scope="row">
<Link href="https://gusto.com">{employee['Name-FL']}</Link>
</DataGridHeaderCell>
)}
{columnsDisplayed.includes('department') && (
<DataGridDataCell>{employee.Department}</DataGridDataCell>
)}
{columnsDisplayed.includes('jobTitle') && (
<DataGridDataCell>{employee.Title}</DataGridDataCell>
)}
{columnsDisplayed.includes('emailAddress') && (
<DataGridDataCell>
<TextField
label="Email address"
visuallyHideLabel
value={employee.Email}
readOnly
/>
</DataGridDataCell>
)}
</DataGridRow>
)
})}
</DataGridBody>
</DataGrid>
<Pagination
position="sticky"
aria-label="Employee information table"
variant="joined"
>
<PaginationPerPageSelectField />
<PaginationNavigation>
<PaginationNavigationButton variant="first" />
<PaginationNavigationButton variant="previous" />
<PaginationNavigationButton variant="next" />
<PaginationNavigationButton variant="last" />
</PaginationNavigation>
</Pagination>
</>
)
}
React props
NameTypeDefaultDescription
aria-label  
string
-Set the `aria-label` attribute to the `<table>` element.
aria-labelledby  
string
-Set the `aria-labelledby` attribute to the `<table>` element.
children  
ReactNode
-The content of the component.
gridSectionStyles  
CSSProperties
-The styles for the containing section to adjust height and width of the grid.
React props
NameTypeDefaultDescription
children  
ReactNode
-The content of the DataGrid `tbody`.
React props
NameTypeDefaultDescription
children  
ReactNode
-The content of the DataGrid `td` data cell.
React props
NameTypeDefaultDescription
children  
ReactNode
-The content of the DataGrid `thead`.
React props
NameTypeDefaultDescription
children  
ReactNode
-The content of the DataGrid `th` cell.
scope  
rowrowgroupcolcolgroup
colThe `scope` attribute of the DataGrid `th` cell.
React props
NameTypeDefaultDescription
children  
ReactNode
-The content of the DataGrid `thead`.
rowSelected  
rowrowgroupcolcolgroup
falseUse to show that this row is selected.
React props
NameTypeDefaultDescription
children  
ReactNode
-The content of the DataGrid `td` cell of the "select row" column.
label  Required
ReactNode
falseLabel text for the associated `<label>` of the checkbox within the cell.
React props
NameTypeDefaultDescription
children  
ReactNode
-The content of the DataGrid `th` cell of the "select row" column.
label  
string
Select rowThe visually hidden content for the "select row" column.

All keyboard navigation guidance is based off the W3C APG guidelines for data grid patternsExternal link.

Navigation moving across cells in DataGrid
Keys
Action
Right Arrow, Left Arrow, Up Arrow,Down Arrow
Cycle through the cells of the grid. If the cell contains interactive element(s) and the first one does not require arrow navigation, that element will be focused instead of the cell.
TabFirst Tab will focus into the last focused cell/interactive element. Any subsequent will either focus to the next interactive element within that cell or focus on the next interactive element outside the grid.
Page DownMoves focus to the current position in the bottom row in the currently visible. If focus is in the last row of the grid, focus does not move. If the cell contains interactive element(s) and the first one does not require arrow navigation, that element will be focused instead of the cell.
Page UpMoves focus to the top row in the currently visible set of rows becomes one of the last visible rows. If focus is in the first row of the grid, focus does not move. If the cell contains interactive element(s) and the first one does not require arrow navigation, that element will be focused instead of the cell.
HomeMoves focus to the first cell in the row that contains focus. If the cell contains interactive element(s) and the first one does not require arrow navigation, that element will be focused instead of the cell.
EndMoves focus to the last cell in the row that contains focus. If the cell contains interactive element(s) and the first one does not require arrow navigation, that element will be focused instead of the cell.
Control + HomeMoves focus to the first cell in the first row. If the cell contains interactive element(s) and the first one does not require arrow navigation, that element will be focused instead of the cell.
Control + EndMoves focus to the last cell in the last row. If the cell contains interactive element(s) and the first one does not require arrow navigation, that element will be focused instead of the cell.
Navigation within a cell of DataGrid with interactive elements
Keys
Action
EnterIf interactive elements are present in the cell, focus will be moved to the first interactive element.
EscapeMove focus to the cell for grid navigation.
Tab If mutiple interactive elements are present in the cell, focus will be moved to the next interactive element. If there are no more interactive elements within the cell, focus will go to the next interactive element outside the grid.
DataGrid captions are required and should provide context about the data in the HTML table. Reading through tabular data is time consuming, so a caption helps users decide if the content is relevant to them.

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

import {
DataGrid,
DataGridBody,
DataGridDataCell,
DataGridHeader,
DataGridHeaderCell,
DataGridRow,
DataGridSelectRowDataCell,
DataGridSelectRowHeaderCell,
} from '@gusto/workbench';
it('tests for basic data grid render and functionality', async () => {
render(
<DataGrid aria-label="Employee Grid">
<DataGridHeader>
<DataGridRow>
<DataGridSelectRowHeaderCell />
<DataGridHeaderCell>Full Name</DataGridHeaderCell>
<DataGridHeaderCell sortable sort="descending">Hours</DataGridHeaderCell>
</DataGridRow>
</DataGridHeader>
<DataGridBody>
<DataGridRow>
<DataGridSelectRowCell
label="Select row for Hannah Ardent"
onChange={jest.fn()}
/>
<DataGridHeaderCell scope="row">
Hannah Ardent
</DataGridDataCell>
<DataGridDataCell textAlign="end">40</DataGridDataCell>
</DataGridRow>
<DataGridRow>
<DataGridSelectRowCell
label="Select row for Isaiah Berlin"
onChange={jest.fn()}
/>
<DataGridHeaderCell scope="row">
Isaiah Berlin
</DataGridDataCell>
<DataGridDataCell textAlign="end">32</DataGridDataCell>
</DataGridRow>
</DataGridBody>
</DataGrid>
);
/** DataGrid */
// Ensure the data grid has the expected accessible name and reading
const dataGrid = screen.getByRole('grid', { name: 'Employee Grid' });
expect(dataGrid).toBeInTheDocument();
/** DataGridRow */
// Ensure each row exists
const [headerRow, hannahRow, isaiahRow] = within(dataGrid).getAllByRole('row');
expect(headerRow).toBeInTheDocument();
expect(hannahRow).toBeInTheDocument();
expect(isaiahRow).toBeInTheDocument();
/** DataGridHeaderCell */
// Ensure each header cell exists
const [
selectRowHeaderCell,
fullNameHeaderCell,
hoursHeaderCell
] = within(headerRow).getAllByRole('columnheader');
expect(selectRowHeaderCell).toBeInTheDocument();
expect(fullNameHeaderCell).toBeInTheDocument();
expect(hoursHeaderCell).toBeInTheDocument();
// Assert on sort click
const hoursHeaderCellSortButton = getByRole(hoursHeaderCell, 'button');
// Click the sort button
await userEvent.click(hoursHeaderCellSortButton);
// Assert sort expectations
/** DataGridSelectRowDataCell */
// Assert on row selection
const hannahSelectRowCellCheckbox = within(hannaRow).getByRole('checkbox', {
name: 'Select row for Hannah Ardent',
})
// Click the row selection checkbox
await userEvent.click(hannahSelectRowCellCheckbox);
// Assert row selection expectations
expect(hannahSelectRowCellCheckbox).toBeChecked();
});