import React from 'react' import { Grid } from '.' import type { Column, GridActionsMenuFullProps, GridActionsMenuProps, GridRowData, GridRowMeta, } from '../../types' import { render, screen, snapshot, waitFor, within, } from '../../utils/test-utils' import userEvent from '@testing-library/user-event' import { GridCellBase, GridCellDefault, GridCellDropdownMenu, } from '../../components' import { Chip, EmptyState, ListItem, ListItemInfo, Menu, } from '@planview/pv-uikit' import { GridEditorInput, GridEditorSwitch, GridEditorCombobox, } from '../../editors' import { TestingProvider } from '@planview/pv-utilities' /* Just ignore setPointerCapture calls, JSDOM doesn't support it */ Object.defineProperty(global.HTMLElement.prototype, 'setPointerCapture', { value: () => {}, }) type User = { id: number firstName: string lastName: string status: 'Active' | 'Pending' } const columns: Column[] = [ { id: 'firstName', label: 'First Name', }, { id: 'lastName', label: 'Last Name', }, { id: 'status', label: 'Status', cell: { Renderer({ tabIndex, value }) { return ( ) }, }, }, ] const treeColumns = columns.map((c) => { if (c.id === 'firstName') { return { ...c, tree: true, } } return c }) const columnsWithEditors: Column[] = [ { id: 'firstName', label: 'First Name', cell: { editable: true, }, }, { id: 'lastName', label: 'Last Name', cell: { editable: true, Editor(props) { return ( ) }, }, }, { id: 'status', label: 'Status', cell: { editable: true, value({ row }) { return row.status === 'Active' }, hybridEditor: true, Editor: GridEditorSwitch, }, }, { id: 'statusDisplay', label: 'Status Detail', cell: { value({ row }) { return row.status }, }, }, ] const rows: User[] = [ { id: 1, firstName: 'Joe', lastName: 'Doe', status: 'Active' }, { id: 2, firstName: 'Fran', lastName: 'Bran', status: 'Pending' }, ] const nestedRows = new Map([ [3, { id: 3, firstName: 'Mr', lastName: 'Manager', status: 'Active' }], [1, { id: 1, firstName: 'Joe', lastName: 'Doe', status: 'Active' }], [2, { id: 2, firstName: 'Fran', lastName: 'Bran', status: 'Pending' }], ]) const treeMeta = new Map>([ [3, { type: 'tree', children: [1, 2] }], ]) const groupMeta = new Map>([ [3, { type: 'group', children: [1, 2] }], ]) describe('Grid', () => { describe('snapshots', () => { snapshot( 'should render a simple grid', ) snapshot( 'should render a grid with empty content', } /> ) snapshot( 'should render an editable grid', ) snapshot( 'should render a grid without columns or rows', ) snapshot( 'should render a grid in loading mode without columns or rows', ) snapshot( 'should render a grid without columns', ) }) it('should support an accessible label', () => { const { queryByRole } = render( ) expect(queryByRole('grid', { name: 'Users grid' })).toBeInTheDocument() }) it('should not render empty state when changing from loading to rows provided', () => { let emptyRendered = false const EmptyComponent = () => { emptyRendered = true return
No data
} const { queryAllByRole, rerender } = render( } /> ) rerender( } /> ) expect(emptyRendered).toBe(false) expect(queryAllByRole('row')).toHaveLength(3) }) it('should render a grid with 25 rows (1 header row) and 14 columns by default', () => { const { queryAllByRole } = render( ({ id: `column-${i}`, label: `${i}`, width: 100, }))} rows={Array.from({ length: 100 }, (_, i) => ({ id: i, title: `${i}`, }))} /> ) expect(queryAllByRole('row')).toHaveLength(25) expect(queryAllByRole('row')[1].querySelectorAll('td')).toHaveLength(14) }) it('should render a grid with more rows and columns when used with TestingProvider', () => { const { queryAllByRole } = render( ({ id: `column-${i}`, label: `${i}`, width: 100, }))} rows={Array.from({ length: 100 }, (_, i) => ({ id: i, title: `${i}`, }))} /> ) expect(queryAllByRole('row')).toHaveLength(29) expect(queryAllByRole('row')[1].querySelectorAll('td')).toHaveLength(16) }) it('should render a grid without a selection column with selectionMode=multi-hidden', () => { const { queryAllByRole } = render( ) expect(queryAllByRole('checkbox')).toHaveLength(0) }) it('should render a grid in loading with filteredIds without crashing', () => { expect(() => render( ) ).not.toThrow() }) it('should render a tree', async () => { const { queryByText, getByRole } = render( ) const treegrid = getByRole('treegrid') expect(treegrid).toBeInTheDocument() expect(treegrid).toHaveAttribute('aria-rowcount', '4') expect(queryByText('Joe')).not.toBeInTheDocument() await userEvent.click(getByRole('button', { name: 'Expand row' })) expect(queryByText('Joe')).toBeInTheDocument() }) it('should set correct setsize and posinset values on tree', async () => { const { getAllByRole } = render( ) const [, firstRow, secondRow, thirdRow] = getAllByRole('row') expect(firstRow).toHaveAttribute('aria-setsize', '1') expect(firstRow).toHaveAttribute('aria-posinset', '1') expect(secondRow).toHaveAttribute('aria-setsize', '2') expect(secondRow).toHaveAttribute('aria-posinset', '1') expect(thirdRow).toHaveAttribute('aria-setsize', '2') expect(thirdRow).toHaveAttribute('aria-posinset', '2') }) it('should not render setsize and posinset values on flat grid', async () => { const { getAllByRole } = render() const [, firstRow] = getAllByRole('row') expect(firstRow).not.toHaveAttribute('aria-setsize') expect(firstRow).not.toHaveAttribute('aria-posinset') }) it('should render a tree with correct role on expandable rows', async () => { const { getByRole, queryByRole, queryAllByRole } = render( ) expect(getByRole('treegrid')).toBeInTheDocument() expect(queryByRole('row', { expanded: false })).toBeInTheDocument() await userEvent.click(getByRole('button', { name: 'Expand row' })) expect(queryByRole('row', { expanded: true })).toBeInTheDocument() expect(queryByRole('row', { expanded: false })).not.toBeInTheDocument() expect( queryAllByRole('row').map((el) => el.getAttribute('aria-level')) ).toEqual([null, '1', '2', '2']) }) it('should render footer when column has aggregation defined', () => { const { getByRole } = render( `Number of rows: ${v?.toString()}`, }, }, ]} rows={rows} /> ) expect( getByRole('gridcell', { name: 'Number of rows: 2' }) ).toBeInTheDocument() }) it('should render footer when column has label defined', () => { const { getByRole } = render( `Custom label`, }, }, ]} rows={rows} /> ) expect( getByRole('gridcell', { name: 'Custom label' }) ).toBeInTheDocument() }) it('should support selecting a tree parent independently from a child', async () => { const { getAllByRole } = render( ) await userEvent.click( getAllByRole('checkbox', { name: 'Toggle row selection' })[0] ) expect(getAllByRole('row', { selected: true })).toHaveLength(1) }) it('should support selecting a group parent which selects all children', async () => { const { getAllByRole } = render( ) await userEvent.click( getAllByRole('checkbox', { name: 'Toggle row selection' })[0] ) expect(getAllByRole('row', { selected: true })).toHaveLength(3) }) it('should set aria-readonly when no cell is possibly editable', () => { const { queryByRole } = render() expect(queryByRole('grid')).toHaveAttribute('aria-readonly', 'true') }) it('should have both rows', () => { const { getByRole } = render() expect(getByRole('gridcell', { name: /Joe/ })).toBeInTheDocument() expect(getByRole('gridcell', { name: /Fran/ })).toBeInTheDocument() }) it('should render indeterminate state for select all checkbox if some items selected', () => { const { getByRole } = render( ) expect( getByRole('checkbox', { name: 'Select all rows', }) // eslint-disable-next-line jest-dom/prefer-checked ).toHaveAttribute('aria-checked', 'mixed') }) it('should not coerce falsy values in user data', () => { const rows = [ { id: '1', enabled: true, count: 5, related: 'Joe' }, { id: '2', enabled: false, count: 0, related: undefined }, ] const columns: Column<(typeof rows)[0]>[] = [ { id: 'enabled', label: 'Enabled', }, { id: 'count', label: 'Count', }, { id: 'related', label: 'Related', cell: { label({ value }) { return typeof value }, }, }, ] const { getAllByRole } = render( ) const row1Content = within(getAllByRole('row')[1]) .getAllByRole('gridcell') .map((d) => d.textContent) const row2Content = within(getAllByRole('row')[2]) .getAllByRole('gridcell') .map((d) => d.textContent) expect(row1Content).toEqual(['true', '5', 'string']) expect(row2Content).toEqual(['false', '0', 'undefined']) }) it('should make metadata available to every column', () => { const data: Record = { 1: { id: '1' }, 2: { id: '2' }, } const meta: Record = { 1: { type: 'tree' }, 2: { type: 'group' }, } const columns: Column[] = [ { id: 'id', label: 'Id', tree: true, cell: { value({ rowMeta }) { return `Column 1 ${rowMeta.type}` }, }, }, { id: 'alt', label: 'Alt', cell: { value({ rowMeta }) { return `Column 2 ${rowMeta.type}` }, }, }, ] const { getByRole } = render( ) expect( getByRole('gridcell', { name: 'Column 1 tree' }) ).toBeInTheDocument() expect( getByRole('gridcell', { name: 'Column 2 tree' }) ).toBeInTheDocument() expect( getByRole('gridcell', { name: 'Column 1 group' }) ).toBeInTheDocument() expect( getByRole('gridcell', { name: 'Column 2 group' }) ).toBeInTheDocument() }) describe('Header interaction', () => { it('should focus on first column header checkbox', async () => { const { getByRole } = render() await userEvent.keyboard('{Tab}') expect( getByRole('checkbox', { name: 'Select all rows', }) ).toHaveFocus() }) it('header checkbox should toggle rows selected using space key', async () => { const { queryAllByRole } = render( ) await userEvent.keyboard('{Tab}') expect(queryAllByRole('row', { selected: true })).toHaveLength(0) await userEvent.keyboard(' ') expect(queryAllByRole('row', { selected: true })).toHaveLength(2) }) it('header checkbox should toggle rows selected using enter key', async () => { const { queryAllByRole } = render( ) await userEvent.keyboard('{Tab}') expect(queryAllByRole('row', { selected: true })).toHaveLength(0) await userEvent.keyboard('{Enter}') expect(queryAllByRole('row', { selected: true })).toHaveLength(2) }) it('should move focus to second column and update sorting using space key', async () => { const { getByRole } = render() await userEvent.keyboard('{Tab}{ArrowRight}') expect( getByRole('columnheader', { name: 'First Name', }) ).toHaveFocus() expect( getByRole('columnheader', { name: 'First Name', }) ).toHaveAttribute('aria-sort', 'none') await userEvent.keyboard(' ') expect( getByRole('columnheader', { name: 'First Name', }) ).toHaveAttribute('aria-sort', 'ascending') }) it('should move focus to second column and update sorting using enter key', async () => { const { getByRole } = render() await userEvent.keyboard('{Tab}{ArrowRight}') expect( getByRole('columnheader', { name: 'First Name', }) ).toHaveFocus() expect( getByRole('columnheader', { name: 'First Name', }) ).toHaveAttribute('aria-sort', 'none') await userEvent.keyboard('{Enter}') expect( getByRole('columnheader', { name: 'First Name', }) ).toHaveAttribute('aria-sort', 'ascending') }) it('should update sorting using click', async () => { const { getByRole } = render() expect( getByRole('columnheader', { name: 'First Name', }) ).toHaveAttribute('aria-sort', 'none') await userEvent.click( getByRole('columnheader', { name: 'First Name', }) ) expect( getByRole('columnheader', { name: 'First Name', }) ).toHaveAttribute('aria-sort', 'ascending') }) it('should update focus state on click on header cell', async () => { const { getByRole } = render() const headerCell = getByRole('columnheader', { name: 'First Name', }) await userEvent.click(headerCell) expect(headerCell).toHaveAttribute('tabindex', '0') }) }) describe('Body interaction', () => { it('should update focus state on click on cell', async () => { const { getByLabelText, getByText } = render( ) expect(getByLabelText('Select all rows')).toHaveAttribute( 'tabindex', '0' ) const label = getByText(/Fran/) await userEvent.click(label) expect(label).toHaveAttribute('tabindex', '0') }) it('should toggle row selected using space key', async () => { const { queryAllByRole } = render( ) expect(queryAllByRole('row', { selected: true })).toHaveLength(0) await userEvent.keyboard('{Tab}{ArrowDown}') await userEvent.keyboard(' ') expect(queryAllByRole('row', { selected: true })).toHaveLength(1) }) it('should toggle row selected using enter key', async () => { const { queryAllByRole } = render( ) expect(queryAllByRole('row', { selected: true })).toHaveLength(0) await userEvent.keyboard('{Tab}{ArrowDown}') await userEvent.keyboard('{Enter}') expect(queryAllByRole('row', { selected: true })).toHaveLength(1) }) }) describe('Focus handling', () => { it('should update focus state on click on header cell and navigate down to next row with arrow down', async () => { const { getByRole, getByText } = render( ) const headerCell = getByRole('columnheader', { name: 'First Name', }) await userEvent.click(headerCell) expect(headerCell).toHaveAttribute('tabindex', '0') await userEvent.keyboard('{ArrowDown}') expect(getByText(/Fran/)).toHaveFocus() }) it('should treat click on select all as focus event as well and navigate to next column with arrow right', async () => { const { getByRole } = render() await userEvent.click( getByRole('columnheader', { name: 'First Name', }) ) await userEvent.click( getByRole('checkbox', { name: 'Select all rows', }) ) await userEvent.keyboard('{ArrowRight}') expect( getByRole('columnheader', { name: 'First Name', }) ).toHaveFocus() }) it('should treat click on row select as focus event as well and navigate to next column with arrow right', async () => { const { getAllByRole, getByText } = render( ) await userEvent.click( getAllByRole('checkbox', { name: 'Toggle row selection', })[0] ) await userEvent.keyboard('{ArrowRight}') expect(getByText(/Joe/)).toHaveFocus() }) it('should treat click on cell as focus event and navigate to the next column with arrow right', async () => { const { getByText, getByRole } = render( ) const cell = getByRole('gridcell', { name: /Joe/ }) await userEvent.click(cell) await userEvent.keyboard('{ArrowRight}') expect(getByText(/Doe/)).toHaveFocus() }) it('should not get stuck on custom content with tabIndex=0', async () => { const { getByText } = render( ) }, }, }, ]} /> ) await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowDown}') expect(getByText('Three')).toHaveFocus() }) it('should navigate through a merged cell without changing the index', async () => { const { getByRole, getByText } = render( ) await userEvent.click(getByRole('gridcell', { name: '1.2' })) await userEvent.keyboard('{ArrowDown}') expect(getByText('2.1')).toHaveFocus() await userEvent.keyboard('{ArrowDown}') expect(getByText('3.2')).toHaveFocus() }) }) describe('onRowClick', () => { it('should trigger onRowClick when clicking on a cell', async () => { const rowClickSpy = jest.fn() const { getByRole } = render( ) await userEvent.click(getByRole('gridcell', { name: /Joe/ })) expect(rowClickSpy).toHaveBeenCalledTimes(1) expect(rowClickSpy).toHaveBeenCalledWith(1) }) it('should trigger onRowClick when clicking selection checkbox', async () => { const rowClickSpy = jest.fn() const { getAllByRole } = render( ) await userEvent.click(getAllByRole('checkbox')[1]) expect(rowClickSpy).toHaveBeenCalledTimes(1) expect(rowClickSpy).toHaveBeenCalledWith(1) }) it('should trigger onRowClick when hitting shift+space', async () => { const rowClickSpy = jest.fn() render( ) await userEvent.keyboard( '{Tab}{ArrowDown}{ArrowRight}{Shift>} {/Shift}' ) expect(rowClickSpy).toHaveBeenCalledTimes(1) expect(rowClickSpy).toHaveBeenCalledWith(1) }) it('should not trigger onRowClick when clicking on a dropdown menu', async () => { const rowClickSpy = jest.fn() const { getAllByRole } = render( ) await userEvent.click( getAllByRole('button', { expanded: false })[0] ) expect(rowClickSpy).not.toHaveBeenCalled() }) it('should not trigger onRowClick when clicking to edit a cell', async () => { const rowClickSpy = jest.fn() const { getByRole } = render( ) await userEvent.click(getByRole('gridcell', { name: /Joe/ })) expect(rowClickSpy).not.toHaveBeenCalled() }) describe('range selection', () => { it('should select range of rows', async () => { const spy = jest.fn() const { getAllByRole } = render( ) const user = userEvent.setup() await user.click(getAllByRole('row')[1]) expect(spy).toHaveBeenCalledWith(new Set([1])) await user.keyboard('[ShiftLeft>]') await user.click(getAllByRole('row')[2]) expect(spy).toHaveBeenCalledWith(new Set([1, 2])) }) }) }) describe('ActionsMenu', () => { const ActionsMenuItems = ({ row }: GridActionsMenuProps) => ( <> {row.firstName} {row.lastName} ) const ActionsMenu = ({ row, menuProps, }: GridActionsMenuFullProps) => ( {row.firstName} {row.lastName} ) ;[ { label: 'MenuItems', description: 'with a MenuItems component', actionsMenu: ActionsMenuItems, }, { label: 'Menu', description: 'with a Menu component', actionsMenu: { Menu: ActionsMenu }, }, ].forEach(({ label, description, actionsMenu }) => { describe(`${description}`, () => { it('should open the menu if a user right clicks on a row', async () => { const { getAllByRole, getByRole } = render( ) const renderedRows = getAllByRole('row') await userEvent.pointer({ target: renderedRows[1], keys: '[MouseRight]', }) expect( getByRole('menuitemradio', { name: 'Action one' }) ).toBeInTheDocument() expect( within(getByRole('menu')).getByText('Joe Doe') ).toBeInTheDocument() }) it('should open the menu if a user clicks on the actions button', async () => { const { getAllByRole, getByRole } = render( ) const buttons = getAllByRole('button', { name: /More actions/, expanded: false, }) await userEvent.click(buttons[0]) expect( getByRole('menuitemradio', { name: 'Action one' }) ).toBeInTheDocument() expect( within(getByRole('menu')).getByText('Joe Doe') ).toBeInTheDocument() }) it('should not open a menu if a user right clicks on a link', async () => { const { getByRole, queryByRole } = render( ) }, }, }, ]} rows={rows} actionsMenu={actionsMenu} /> ) const link = getByRole('link', { name: /Joe/, }) await userEvent.pointer({ target: link, keys: '[MouseRight]', }) expect( queryByRole('menuitemradio', { name: 'Action one' }) ).not.toBeInTheDocument() }) it('should not open an extra menu if a user right clicks in a dropdown menu', async () => { const { getAllByRole, getByRole } = render( ) const buttons = getAllByRole('button', { name: /More actions/, expanded: false, }) await userEvent.click(buttons[0]) await userEvent.pointer({ target: getByRole('menuitemradio', { name: 'Action one', }), keys: '[MouseRight]', }) expect( getAllByRole('menuitemradio', { name: 'Action one' }) ).toHaveLength(1) }) if (label === 'MenuItems') { it('should support conditional rendering (deprecated support)', async () => { const { getAllByRole, queryByRole } = render( ) const renderedRows = getAllByRole('row') // Button rendered on first row expect( within(renderedRows[1]).queryByRole('button', { name: /More actions/, expanded: false, }) ).toBeInTheDocument() // Button not rendered on second row expect( within(renderedRows[2]).queryByRole('button', { name: /More actions/, expanded: false, }) ).not.toBeInTheDocument() // Right click on second row, no menu should be shown await userEvent.pointer({ target: renderedRows[2], keys: '[MouseRight]', }) expect( queryByRole('menuitemradio', { name: 'Action one' }) ).not.toBeInTheDocument() // Right click on first row, menu should be shown await userEvent.pointer({ target: renderedRows[1], keys: '[MouseRight]', }) expect( queryByRole('menuitemradio', { name: 'Action one' }) ).toBeInTheDocument() }) } it('should support conditional rendering', async () => { const { getAllByRole, queryByRole } = render( ) const renderedRows = getAllByRole('row') // Button rendered on first row expect( within(renderedRows[1]).queryByRole('button', { name: /More actions/, expanded: false, }) ).toBeInTheDocument() // Button not rendered on second row expect( within(renderedRows[2]).queryByRole('button', { name: /More actions/, expanded: false, }) ).not.toBeInTheDocument() // Right click on second row, no menu should be shown await userEvent.pointer({ target: renderedRows[2], keys: '[MouseRight]', }) expect( queryByRole('menuitemradio', { name: 'Action one' }) ).not.toBeInTheDocument() // Right click on first row, menu should be shown await userEvent.pointer({ target: renderedRows[1], keys: '[MouseRight]', }) expect( queryByRole('menuitemradio', { name: 'Action one' }) ).toBeInTheDocument() }) }) }) }) describe('Navigation', () => { it('if last row is focused when removed,it should move focus up to the header', async () => { const { getByRole, rerender } = render( ) await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowRight}') rerender() expect( getByRole('columnheader', { name: /First Name/, }) ).toHaveFocus() }) it('if all rows are removed, it should move focus up to the header', async () => { const { getByRole, rerender } = render( ) await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowRight}') rerender() expect( getByRole('columnheader', { name: /First Name/, }) ).toHaveFocus() }) it('if last column is focused when removed, it should move focus to first column', async () => { const { getAllByRole, rerender } = render( ) await userEvent.keyboard( '{Tab}{ArrowDown}{ArrowDown}{ArrowRight}{ArrowRight}{ArrowRight}' ) rerender() expect( getAllByRole('checkbox', { name: 'Toggle row selection', })[1] ).toHaveFocus() }) it('should navigate to second role, second row', async () => { const { getByText } = render() await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowRight}') expect(getByText(/Fran/)).toHaveFocus() }) it('should navigate to last column on keyboard End', async () => { const { getByRole } = render() await userEvent.keyboard('{Tab}{End}') await waitFor(() => expect( getByRole('columnheader', { name: /Status/, }) ).toHaveFocus() ) }) it('should navigate to last column in last row on keyboard Control + End', async () => { const { getByRole } = render() await userEvent.keyboard('{Tab}{Control>}{End}{/Control}') await waitFor(() => expect( getByRole('button', { name: /Pending/, expanded: false, }) ).toHaveFocus() ) }) it('should navigate to first column on keyboard Home', async () => { const { getByText, getAllByRole } = render( ) await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowRight}') expect(getByText(/Fran/)).toHaveFocus() await userEvent.keyboard('{Home}') await waitFor(() => expect( getAllByRole('checkbox', { name: 'Toggle row selection', })[1] ).toHaveFocus() ) }) it('should navigate to first column in header row on keyboard Control + Home', async () => { const { getByText, getByRole } = render( ) await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowRight}') expect(getByText(/Fran/)).toHaveFocus() await userEvent.keyboard('{Control>}{Home}{/Control}') await waitFor(() => expect( getByRole('checkbox', { name: 'Select all rows', }) ).toHaveFocus() ) }) it('should navigate to last item on page down', async () => { const { getAllByRole } = render( ) await userEvent.keyboard('{Tab}{PageDown}') await waitFor(() => expect( getAllByRole('checkbox', { name: 'Toggle row selection', })[1] ).toHaveFocus() ) }) it('should navigate to header on page up', async () => { const { getByText, getByRole } = render( ) await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowRight}') expect(getByText(/Fran/)).toHaveFocus() await userEvent.keyboard('{PageUp}') await waitFor(() => expect( getByRole('columnheader', { name: /First Name/, }) ).toHaveFocus() ) }) it('should freeze navigation on opening dropdown', async () => { const { getByRole, getByText } = render( ) await userEvent.keyboard( '{Tab}{ArrowDown}{ArrowRight}{ArrowRight}{ArrowRight}' ) expect( getByRole('button', { name: /Active/, expanded: false, }) ).toHaveFocus() await userEvent.keyboard(' ') await userEvent.keyboard('{ArrowLeft}{ArrowDown}{ArrowLeft}') expect(getByRole('menu')).toHaveFocus() await userEvent.keyboard('{Escape}') await userEvent.keyboard('{ArrowLeft}') expect(getByText(/Doe/)).toHaveFocus() }) it('should not select the row when clicking on a dropdown', async () => { const spy = jest.fn() const { getAllByRole, getByRole } = render( ) const buttons = getAllByRole('button', { name: /Active/, expanded: false, }) await userEvent.click(buttons[0]) expect(getByRole('menu')).toHaveFocus() expect(spy).not.toHaveBeenCalled() }) it('should focus back on the cell when starting by interacting with a click on dropdown menu', async () => { const spy = jest.fn() const { getAllByRole, getByRole, getByText } = render( ) const buttons = getAllByRole('button', { name: /Active/, expanded: false, }) await userEvent.click(buttons[0]) expect(getByRole('menu')).toHaveFocus() await userEvent.click( getByRole('menuitemradio', { name: /Active/ }) ) await userEvent.keyboard('{ArrowLeft}') expect(getByText(/Doe/)).toHaveFocus() }) }) describe('Editing', () => { describe('aria-readonly', () => { beforeEach(() => { render() }) it('should not set aria-readonly on grid when a cell is possibly editable', () => { expect(screen.queryByRole('grid')).not.toHaveAttribute( 'aria-readonly' ) }) it('should not mark editable cells as read only', () => { expect( screen.queryByRole('gridcell', { name: /Joe/ }) ).not.toHaveAttribute('aria-readonly') }) it('should mark non-editable cells as read only', () => { expect( screen.queryByRole('gridcell', { name: /Active/ }) ).toHaveAttribute('aria-readonly', 'true') }) }) it('should not set aria-readonly on grid when a cell is possibly editable', () => { const { queryByRole } = render( ) expect(queryByRole('grid')).not.toHaveAttribute('aria-readonly') }) describe('mouse interaction', () => { it('should support editing a single cell', async () => { const spy = jest.fn() const { getByRole, getByText } = render( ) await userEvent.click(getByRole('gridcell', { name: /Joe/ })) await userEvent.type(getByRole('textbox'), 'Jane', { initialSelectionStart: 0, initialSelectionEnd: 3, }) await userEvent.keyboard('{Enter}') expect(getByText(/Joe/)).toHaveFocus() expect(spy).toHaveBeenCalledWith({ previousValue: 'Joe', nextValue: 'Jane', columnId: 'firstName', rowId: 1, }) }) it('should support editing multiple cells', async () => { const spy = jest.fn() const { getByRole, getAllByRole } = render( ) await userEvent.click(getByRole('gridcell', { name: /Joe/ })) await userEvent.type(getByRole('textbox'), 'Jane', { initialSelectionStart: 0, initialSelectionEnd: 3, }) await userEvent.click(getByRole('gridcell', { name: /Doe/ })) await userEvent.type(getByRole('textbox'), 'Green', { initialSelectionStart: 0, initialSelectionEnd: 3, }) await userEvent.click( getAllByRole('checkbox', { name: 'Status' })[0] ) expect(spy).toHaveBeenCalledTimes(3) expect(spy).toHaveBeenCalledWith({ previousValue: 'Joe', nextValue: 'Jane', columnId: 'firstName', rowId: 1, }) expect(spy).toHaveBeenCalledWith({ previousValue: 'Doe', nextValue: 'Green', columnId: 'lastName', rowId: 1, }) expect(spy).toHaveBeenCalledWith({ previousValue: true, nextValue: false, columnId: 'status', rowId: 1, }) }) }) describe('GridEditorCombobox', () => { it('should call onCellChange on click', async () => { const spy = jest.fn() const options = [ { label: 'option active', value: 'active', }, { label: 'option inactive', value: 'inactive', }, ] const { getByRole } = render( o.value === props.value )} options={options} /> ) }, }, }, ]} rows={[{ id: '1', status: 'active' }]} onCellChange={spy} /> ) await userEvent.click(getByRole('gridcell', { name: /active/ })) expect(spy).toHaveBeenCalledTimes(0) await userEvent.click( getByRole('option', { name: /option inactive/ }) ) expect(spy).toHaveBeenCalledTimes(1) }) it('should call onCellChange on Enter', async () => { const spy = jest.fn() const options = [ { label: 'option active', value: 'active', }, { label: 'option inactive', value: 'inactive', }, ] const { getAllByRole, getByRole } = render( o.value === props.value )} options={options} /> ) }, }, }, { id: 'firstName', label: 'First Name', cell: { editable: true, }, }, ]} rows={[ { id: '1', status: 'active', firstName: 'test' }, ]} onCellChange={spy} /> ) await userEvent.keyboard('{Tab}{ArrowDown}{ArrowRight}{Enter}') expect(getAllByRole('option')).toHaveLength(2) await waitFor(() => expect(getByRole('combobox')).toHaveFocus()) await userEvent.keyboard('{ArrowDown}{Enter}') expect(spy).toHaveBeenCalledTimes(1) }) it('should call onCellChange on tab', async () => { const spy = jest.fn() const options = [ { label: 'option active', value: 'active', }, { label: 'option inactive', value: 'inactive', }, ] const { getByRole } = render( o.value === props.value )} options={options} /> ) }, }, }, { id: 'firstName', label: 'First Name', cell: { editable: true, }, }, ]} rows={[ { id: '1', status: 'active', firstName: 'test' }, ]} onCellChange={spy} /> ) await userEvent.keyboard('{Tab}{ArrowDown}{ArrowRight}{Enter}') await waitFor(() => expect(getByRole('combobox')).toHaveFocus()) await userEvent.keyboard('{ArrowDown}{ArrowDown}{Tab}') expect(spy).toHaveBeenCalledTimes(1) }) it('should call not onCellChange on Escape', async () => { const spy = jest.fn() const options = [ { label: 'option active', value: 'active', }, { label: 'option inactive', value: 'inactive', }, ] const { getAllByRole } = render( o.value === props.value )} options={options} /> ) }, }, }, { id: 'firstName', label: 'First Name', cell: { editable: true, }, }, ]} rows={[ { id: '1', status: 'active', firstName: 'test' }, ]} onCellChange={spy} /> ) await userEvent.keyboard('{Tab}{ArrowDown}{ArrowRight}{Enter}') expect(getAllByRole('option')).toHaveLength(2) await userEvent.keyboard('{Escape}') expect(spy).toHaveBeenCalledTimes(0) }) }) }) })