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.
Item | Name | Description |
---|---|---|
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 row | Job Title | Email address | ||
---|---|---|---|---|
Liana Varo | Marketing | Contractor | ||
Henry Cote | Human Resources | Engineer | ||
Maria Stephens | Marketing | Chief Editor | ||
Mark Brouwer | Accounting and Finance | People Operations | ||
Tatiana Robu | R&D | Designer | ||
Ruby Nguyen | Sales | Sr. Engineer | ||
Sergio Guevara | Sales | Account Manager | ||
Rafe Walker | Accounting and Finance | Information Security Specialist | ||
Fabian Lupea | Legal | Office Operations | ||
Sienna Brown | Operations | CTO | ||
Zoe Mordin | Marketing | Recruiter | ||
Vinay Katwal | Purchasing | Account Director | ||
Charlie King | IT | Project Manager | ||
Maria Mancini | Marketing | Client Support Manager | ||
Dorothy Bishop | IT | Marketing Director | ||
Victor Dubois | Sales | Recruiter | ||
Phillip Carroll | Sales | Client Support Director | ||
Willie Morrison | Security | Level 1 Tech Suport | ||
Liam Jones | Employee | Associate Creative Director | ||
Felix Roth | Human Resources | Contractor | ||
Noah Anderson | Accounting and Finance | Engineer | ||
Sienna Smith | Operations | Sr. Designer | ||
Craig Ellis | Legal | Office Administrator | ||
Rosa Amador | Sales | Client Support Manager | ||
Alexis Muller | IT | IT Specialist | ||
Jasmine Jacob | Human Resources | IT Ops Lead | ||
Emily Lee | Legal | CEO |
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 loadconst defaultColumnsOptions = ['name','department','jobTitle','emailAddress',]// default filters and their selection on page loadconst defaultFilterNames = {Department: [],Title: [],}// Menu props for the actionsMenu and filterColumnsMenu in `sm` breakpointconst [actionsMenuProps, actionsMenuButtonProps] = useMenu()// filter and "columns" Dialog 'open' stateconst [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 columnsDialogconst [columnsCheckboxChecked, setColumnsCheckboxChecked] = React.useState<string[]>(defaultColumnsOptions)// used to make changes to the table on "Apply" of the filters dialogconst [columnsDisplayed, setColumnsDisplayed] = React.useState<string[]>(defaultColumnsOptions,)// used to keep track of the onChange of the filter checkboxesconst [filterCheckboxChecked, setFilterCheckboxChecked] = React.useState<{[key: string]: string[]}>(defaultFilterNames)// used to make changes to the table on "Apply" of the filters dialogconst [appliedFilterCheckboxes, setAppliedFilterCheckboxes] = React.useState<{[key: string]: string[]}>(defaultFilterNames)// used to determine filter count, if filters are applied, and filters dataconst filterCount =appliedFilterCheckboxes.Department.length +appliedFilterCheckboxes.Title.lengthconst isDataFiltered =appliedFilterCheckboxes.Department.length > 0 ||appliedFilterCheckboxes.Title.length > 0const 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 uncheckedconst rowSelectedCount = Object.values(rowSelected).filter(val => val).lengthconst selectAllChecked = rowSelectedCount === filteredEmployees.lengthconst selectAllIndeterminate = rowSelectedCount > 0 && !selectAllChecked// an example of keeping track of rows selectedconst toggleGridRowSelection = (id: number) => {setRowSelected(currSelected => ({...currSelected,[id]: !rowSelected[id],}))}// an example of creating the "select all" functionalityconst 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 checkedconst columnsCheckboxOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {const checkboxChecked = e.target.checkedif (checkboxChecked) {setColumnsCheckboxChecked(currChecked => [...currChecked, e.target.value])} else {setColumnsCheckboxChecked(currChecked =>currChecked.filter(value => value !== e.target.value),)}}// an example of resetting columns optionsconst resetColumnsOptions = () => {setColumnsCheckboxChecked(defaultColumnsOptions)}// an example of updating the visibility of columnsconst updateColumnVisibility = () => {setColumnsDisplayed(columnsCheckboxChecked)}// an example of keeping a state management of each filter checkbox being checkedconst filterCheckboxOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {// getting the group name from CheckboxGroupconst groupName = e.target.nameconst checkboxChecked = e.target.checkedif (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 Dialogconst applyFilters = () => {filtersDialogProps.onClose()setAppliedFilterCheckboxes(filterCheckboxChecked)}// an example of resetting filters within the filters Dialogconst resetFilters = () => {setFilterCheckboxChecked(defaultFilterNames)}return (<><ControlLayoutsearchField={<TextFieldlabel="Search"before={<Search />}placeholder="Search"visuallyHideLabel/>}selectAllCheckbox={<Checkboxtype="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={<><Buttonvariant="tertiary"size="small"before={<Filter />}{...filtersDialogButtonProps}>Filter{filterCount > 0 ? ` (${filterCount})` : null}</Button><Buttonvariant="tertiary"size="small"{...columnsDialogButtonProps}>Columns</Button><Dialogaria-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 (<Checkboxkey={`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 (<Checkboxkey={`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><Dialogaria-label="Employee information visible columns"{...columnsDialogProps}><DialogHeader>Columns</DialogHeader><DialogBody><CheckboxGroupname="columnsOptions"legend="Column options"visuallyHideLabel><Checkboxvalue="name"label="Name"checked={columnsCheckboxChecked.includes('name')}onChange={columnsCheckboxOnChange}/><Checkboxvalue="department"label="Department"checked={columnsCheckboxChecked.includes('department')}onChange={columnsCheckboxOnChange}/><Checkboxvalue="jobTitle"label="Job title"checked={columnsCheckboxChecked.includes('jobTitle')}onChange={columnsCheckboxOnChange}/><Checkboxvalue="emailAddress"label="Email address"checked={columnsCheckboxChecked.includes('emailAddress')}onChange={columnsCheckboxOnChange}/></CheckboxGroup></DialogBody><DialogFooter><DialogActions><Buttonvariant="primary"onClick={() => {columnsDialogProps.onClose()updateColumnVisibility()}}>Apply</Button><Button onClick={resetColumnsOptions}>Reset</Button></DialogActions></DialogFooter></Dialog></>}/><DataGridaria-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 (<DataGridRowkey={`emp-${employee.Digit}`}rowSelected={rowSelected[employee.Digit]}><DataGridSelectRowDataCelllabel={`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><TextFieldlabel="Email address"visuallyHideLabelvalue={employee.Email}readOnly/></DataGridDataCell>)}</DataGridRow>)})}</DataGridBody></DataGrid><Paginationposition="sticky"aria-label="Employee information table"variant="joined"><PaginationPerPageSelectField /><PaginationNavigation><PaginationNavigationButton variant="first" /><PaginationNavigationButton variant="previous" /><PaginationNavigationButton variant="next" /><PaginationNavigationButton variant="last" /></PaginationNavigation></Pagination></>)}
Name | Type | Default | Description |
---|---|---|---|
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. |
Name | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | The content of the DataGrid `tbody`. |
Name | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | The content of the DataGrid `td` data cell. |
Name | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | The content of the DataGrid `thead`. |
Name | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | The content of the DataGrid `th` cell. |
scope | row rowgroup col colgroup | col | The `scope` attribute of the DataGrid `th` cell. |
Name | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | The content of the DataGrid `thead`. |
rowSelected | row rowgroup col colgroup | false | Use to show that this row is selected. |
Name | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | The content of the DataGrid `td` cell of the "select row" column. |
label Required | ReactNode | false | Label text for the associated `<label>` of the checkbox within the cell. |
Name | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | The content of the DataGrid `th` cell of the "select row" column. |
label | string | Select row | The visually hidden content for the "select row" column. |
All keyboard navigation guidance is based off the W3C APG guidelines for data grid patterns.
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. |
Tab | First 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 Down | Moves 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 Up | Moves 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. |
Home | Moves 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. |
End | Moves 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 + Home | Moves 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 + End | Moves 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. |
Keys | Action |
---|---|
Enter | If interactive elements are present in the cell, focus will be moved to the first interactive element. |
Escape | Move 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 Library with occasional help from Jest.
Heads up!
Testing DataGrid (and table) can be resource intensive and could potentially cause timeouts and flakes due to the number of DOM nodes generated. Consider breaking tests into smaller chunks to make testing more reliable.
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><DataGridSelectRowCelllabel="Select row for Hannah Ardent"onChange={jest.fn()}/><DataGridHeaderCell scope="row">Hannah Ardent</DataGridDataCell><DataGridDataCell textAlign="end">40</DataGridDataCell></DataGridRow><DataGridRow><DataGridSelectRowCelllabel="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 readingconst dataGrid = screen.getByRole('grid', { name: 'Employee Grid' });expect(dataGrid).toBeInTheDocument();/** DataGridRow */// Ensure each row existsconst [headerRow, hannahRow, isaiahRow] = within(dataGrid).getAllByRole('row');expect(headerRow).toBeInTheDocument();expect(hannahRow).toBeInTheDocument();expect(isaiahRow).toBeInTheDocument();/** DataGridHeaderCell */// Ensure each header cell existsconst [selectRowHeaderCell,fullNameHeaderCell,hoursHeaderCell] = within(headerRow).getAllByRole('columnheader');expect(selectRowHeaderCell).toBeInTheDocument();expect(fullNameHeaderCell).toBeInTheDocument();expect(hoursHeaderCell).toBeInTheDocument();// Assert on sort clickconst hoursHeaderCellSortButton = getByRole(hoursHeaderCell, 'button');// Click the sort buttonawait userEvent.click(hoursHeaderCellSortButton);// Assert sort expectations/** DataGridSelectRowDataCell */// Assert on row selectionconst hannahSelectRowCellCheckbox = within(hannaRow).getByRole('checkbox', {name: 'Select row for Hannah Ardent',})// Click the row selection checkboxawait userEvent.click(hannahSelectRowCellCheckbox);// Assert row selection expectationsexpect(hannahSelectRowCellCheckbox).toBeChecked();});