/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import React, { useEffect, useState } from 'react'; import { mount, ReactWrapper, render } from 'enzyme'; import { OuiDataGrid, OuiDataGridProps } from './'; import { findTestSubject, requiredProps, takeMountedSnapshot, } from '../../test'; import { OuiDataGridColumnResizer } from './data_grid_column_resizer'; import { OuiDataGridRowHeightOption } from './data_grid_types'; import { keys } from '../../services'; import { act } from 'react-dom/test-utils'; jest.mock('./row_height_utils', () => { return { RowHeightUtils: jest.fn().mockImplementation(() => { return { computeStylesForGridCell: () => {}, getCalculatedHeight: ( heightOption: OuiDataGridRowHeightOption, defaultHeight: number ) => { if (typeof heightOption === 'object') { if (heightOption.lineCount) { return heightOption.lineCount; } if (heightOption.height) { return heightOption.height; } } if (heightOption) { return heightOption; } return defaultHeight; }, }; }), getStylesForCell: () => ({}), }; }); function getFocusableCell(component: ReactWrapper) { return findTestSubject(component, 'dataGridRowCell').find('[tabIndex=0]'); } function extractGridData(datagrid: ReactWrapper) { const rows: string[][] = []; const headerCells = findTestSubject(datagrid, 'dataGridHeaderCell', '|='); const headerRow: string[] = []; headerCells.forEach((cell: any) => headerRow.push( cell.find('[className="ouiDataGridHeaderCell__content"]').text() ) ); rows.push(headerRow); // reduce the virtualized grid of cells into rows const columnCount = datagrid.prop('columnVisibility').visibleColumns.length; const gridCells = findTestSubject(datagrid, 'dataGridRowCell'); const visibleRowsCount = gridCells.length / columnCount; for (let i = 0; i < visibleRowsCount; i++) { const rowContent: string[] = []; for (let j = i * columnCount; j < (i + 1) * columnCount; j++) { const cell = gridCells.at(j); rowContent.push(cell.find('[data-test-subj="cell-content"]').text()); } rows.push(rowContent); } return rows; } function extractRowHeights(datagrid: ReactWrapper) { return (findTestSubject(datagrid, 'dataGridRowCell') as ReactWrapper< any >).reduce((heights: { [key: string]: number }, cell) => { const cellProps = cell.props(); const cellContentProps = cell .find('[data-test-subj="cell-content"]') .props() as any; heights[cellContentProps.rowIndex] = parseFloat(cellProps.style.height); return heights; }, {}); } function extractColumnWidths(datagrid: ReactWrapper) { return (findTestSubject(datagrid, 'dataGridHeaderCell', '|=') as ReactWrapper< any >).reduce((widths: { [key: string]: number }, cell) => { const [, columnId] = cell .props() ['data-test-subj'].match(/dataGridHeaderCell-(.*)/); widths[columnId] = parseFloat(cell.props().style.width); return widths; }, {}); } function resizeColumn( datagrid: ReactWrapper, columnId: string, columnWidth: number ) { const widths = extractColumnWidths(datagrid); const originalWidth = widths[columnId]; const firstResizer = datagrid .find(`OuiDataGridColumnResizer[columnId="${columnId}"]`) .instance() as OuiDataGridColumnResizer; firstResizer.onMouseDown({ pageX: originalWidth, stopPropagation: () => {}, preventDefault: () => {}, } as React.MouseEvent); firstResizer.onMouseMove({ pageX: columnWidth }); act(() => firstResizer.onMouseUp()); datagrid.update(); } function openColumnSorterSelection(datagrid: ReactWrapper) { let columnSelectionPopover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSortingPopoverColumnSelection"]' ); expect(columnSelectionPopover).not.ouiPopoverToBeOpen(); const popoverButton = columnSelectionPopover .find('div[className="ouiPopover__anchor"]') .find('[onClick]') .first(); // @ts-ignore onClick is known to exist, and does not require an argument in this usage act(() => popoverButton.props().onClick()); datagrid.update(); columnSelectionPopover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSortingPopoverColumnSelection"]' ); expect(columnSelectionPopover).ouiPopoverToBeOpen(); return columnSelectionPopover; } function closeColumnSorterSelection(datagrid: ReactWrapper) { let columnSelectionPopover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSortingPopoverColumnSelection"]' ); // popover will go away if all of the columns are selected if (columnSelectionPopover.length > 0) { expect(columnSelectionPopover).ouiPopoverToBeOpen(); const popoverButton = columnSelectionPopover .find('div[className="ouiPopover__anchor"]') .find('[onClick]') .first(); // @ts-ignore onClick is known to exist, and does not require an argument in this usage act(() => popoverButton.props().onClick()); datagrid.update(); columnSelectionPopover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSortingPopoverColumnSelection"]' ); expect(columnSelectionPopover).not.ouiPopoverToBeOpen(); } return columnSelectionPopover; } function getColumnSortDirection( datagrid: ReactWrapper, columnId: string ): [ReactWrapper, string] { // get the button that sorts by this column let columnSorter = datagrid.find( `div[data-test-subj="ouiDataGridColumnSorting-sortColumn-${columnId}"]` ); if (columnSorter.length === 0) { // need to enable this column openColumnSorterSelection(datagrid); // find button to enable this column and click it const selectColumnButton = datagrid.find( `[data-test-subj="dataGridColumnSortingPopoverColumnSelection-${columnId}"]` ); expect(selectColumnButton.length).toBe(1); // @ts-ignore onClick is known to exist, and does not require an argument in this usage act(() => selectColumnButton.props().onClick()); // close column selection popover closeColumnSorterSelection(datagrid); // find the column sorter const columnSelectionPopover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSortingPopover"]' ); columnSorter = columnSelectionPopover.find( `div[data-test-subj="ouiDataGridColumnSorting-sortColumn-${columnId}"]` ); } expect(columnSorter.length).toBe(1); const activeSort = columnSorter.find( 'label[className*="ouiButtonGroupButton-isSelected"]' ); const sortDirection = (activeSort.props() as { 'data-test-subj': string; })['data-test-subj'].match(/(?[^-]+)$/)!.groups!.direction; return [columnSorter, sortDirection]; } function openColumnSorter(datagrid: ReactWrapper) { let popover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSortingPopover"]' ); expect(popover).not.ouiPopoverToBeOpen(); const popoverButton = popover .find('div[className="ouiPopover__anchor"]') .find('[onClick]') .first(); // @ts-ignore onClick is known to exist, and does not require an argument in this usage act(() => popoverButton.props().onClick()); datagrid.update(); popover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSortingPopover"]' ); expect(popover).ouiPopoverToBeOpen(); return popover; } function closeColumnSorter(datagrid: ReactWrapper) { let popover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSortingPopover"]' ); expect(popover).ouiPopoverToBeOpen(); const popoverButton = popover .find('div[className="ouiPopover__anchor"]') .find('[onClick]') .first(); // @ts-ignore onClick is known to exist, and does not require an argument in this usage act(() => popoverButton.props().onClick()); datagrid.update(); popover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSortingPopover"]' ); expect(popover).not.ouiPopoverToBeOpen(); return popover; } function sortByColumn( datagrid: ReactWrapper, columnId: string, direction: 'asc' | 'desc' | 'off' ) { openColumnSorter(datagrid); let [columnSorter, currentSortDirection] = getColumnSortDirection( datagrid, columnId ); // if this column isn't being sorted, enable it if (currentSortDirection === 'off') { act(() => { // @ts-ignore does not require an argument in this usage columnSorter.find('OuiSwitch').props().onChange!(); }); datagrid.update(); // inspect the column's new sort details [columnSorter, currentSortDirection] = getColumnSortDirection( datagrid, columnId ); } if (currentSortDirection !== direction) { const sortButton = columnSorter.find( `label[data-test-subj="ouiDataGridColumnSorting-sortColumn-${columnId}-${direction}"]` ); expect(sortButton.length).toBe(1); sortButton.find('input').simulate('change', [undefined, direction]); } closeColumnSorter(datagrid); } expect.extend({ toBeOuiPopover(received: ReactWrapper) { const pass = received.name() === 'OuiPopover'; if (pass) { return { pass: true, message: () => `expected component "${received.name}" to not be OuiPopover`, }; } else { return { pass: false, message: () => `expected component "${received.name}" to be OuiPopover`, }; } }, ouiPopoverToBeOpen(received) { expect(received).toBeOuiPopover(); const { isOpen } = received.props(); const pass = isOpen === true; if (pass) { return { pass: true, message: () => 'expected OuiPopover to be closed', }; } else { return { pass: false, message: () => 'expected OuiPopover to be open', }; } }, }); declare global { /* eslint-disable-next-line @typescript-eslint/no-namespace,no-redeclare */ namespace jest { interface Matchers { toBeOuiPopover(): R; ouiPopoverToBeOpen(): R; } } } function openColumnSelector(datagrid: ReactWrapper) { let popover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSelectorPopover"]' ); expect(popover).not.ouiPopoverToBeOpen(); const popoverButton = popover .find('div[className="ouiPopover__anchor"]') .find('[onClick]') .first(); // @ts-ignore onClick is known to exist, and does not require an argument in this usage act(() => popoverButton.props().onClick()); datagrid.update(); popover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSelectorPopover"]' ); expect(popover).ouiPopoverToBeOpen(); return popover; } function closeColumnSelector(datagrid: ReactWrapper) { let popover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSelectorPopover"]' ); expect(popover).ouiPopoverToBeOpen(); const popoverButton = popover .find('div[className="ouiPopover__anchor"]') .find('[onClick]') .first(); // @ts-ignore onClick is known to exist, and does not require an argument in this usage act(() => popoverButton.props().onClick()); datagrid.update(); popover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSelectorPopover"]' ); expect(popover).not.ouiPopoverToBeOpen(); return popover; } function setColumnVisibility( datagrid: ReactWrapper, columnId: string, isVisible: boolean ) { const popover = openColumnSelector(datagrid); // toggle column's visibility switch const portal = popover.find('OuiPortal'); const columnSwitch = portal.find(`OuiSwitch[name="${columnId}"]`); const switchInput = columnSwitch.find('button'); switchInput.getDOMNode().setAttribute('aria-checked', `${isVisible}`); switchInput.simulate('click'); closeColumnSelector(datagrid); } function moveColumnToIndex( datagrid: ReactWrapper, columnId: string, nextIndex: number ) { // open datagrid column options let popover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSelectorPopover"]' ); expect(popover).not.ouiPopoverToBeOpen(); let popoverButton = popover .find('div[className="ouiPopover__anchor"]') .find('[onClick]') .first(); // @ts-ignore onClick is known to exist, and does not require an argument in this usage act(() => popoverButton.props().onClick()); datagrid.update(); popover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSelectorPopover"]' ); expect(popover).ouiPopoverToBeOpen(); const [initialColumnOrder] = extractGridData(datagrid); const initialColumnIndex = initialColumnOrder.indexOf(columnId); // "drag" column into new location const portal = popover.find('OuiPortal'); act(() => portal.find('OuiDragDropContext').props().onDragEnd!({ // @ts-ignore - only `index` is used from `source`, don't need to mock rest of the event source: { index: initialColumnIndex }, destination: { index: nextIndex }, }) ); datagrid.update(); // close popover popover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSelectorPopover"]' ); expect(popover).ouiPopoverToBeOpen(); popoverButton = popover .find('div[className="ouiPopover__anchor"]') .find('[onClick]') .first(); // @ts-ignore onClick is known to exist, and does not require an argument in this usage act(() => popoverButton.props().onClick()); datagrid.update(); popover = datagrid.find( 'OuiPopover[data-test-subj="dataGridColumnSelectorPopover"]' ); expect(popover).not.ouiPopoverToBeOpen(); } describe('OuiDataGrid', () => { describe('rendering', () => { const getBoundingClientRect = window.Element.prototype.getBoundingClientRect; beforeAll(() => { window.Element.prototype.getBoundingClientRect = () => ({ width: 100, height: 100 } as DOMRect); }); afterAll(() => { window.Element.prototype.getBoundingClientRect = getBoundingClientRect; }); it('renders with common and div attributes', () => { const component = render( {}, }} rowCount={3} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}, ${columnId}` } /> ); expect(component).toMatchSnapshot(); }); it('renders custom column headers', () => { const component = render( More Elements }, ]} columnVisibility={{ visibleColumns: ['A', 'B'], setVisibleColumns: () => {}, }} rowCount={3} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}, ${columnId}` } /> ); expect(component).toMatchSnapshot(); }); it('renders and applies custom props', () => { const component = mount( {}, }} rowCount={2} renderCellValue={({ rowIndex, columnId, setCellProps }) => { useEffect(() => { setCellProps({ className: 'customClass', 'data-test-subj': `cell-${rowIndex}-${columnId}`, style: { color: columnId === 'A' ? 'red' : 'blue' }, }); }, [columnId, rowIndex, setCellProps]); return `${rowIndex}, ${columnId}`; }} /> ); expect( component.find('.ouiDataGridRowCell').map((cell) => { const props = cell.props(); delete props.children; return props; }) ).toMatchInlineSnapshot(` Array [ Object { "className": "ouiDataGridRowCell ouiDataGridRowCell--firstColumn customClass", "data-test-subj": "dataGridRowCell", "onBlur": [Function], "onFocus": [Function], "onKeyDown": [Function], "onMouseEnter": [Function], "role": "gridcell", "style": Object { "color": "red", "height": 34, "left": 0, "position": "absolute", "right": undefined, "top": "100px", "width": 100, }, "tabIndex": -1, }, Object { "className": "ouiDataGridRowCell ouiDataGridRowCell--lastColumn customClass", "data-test-subj": "dataGridRowCell", "onBlur": [Function], "onFocus": [Function], "onKeyDown": [Function], "onMouseEnter": [Function], "role": "gridcell", "style": Object { "color": "blue", "height": 34, "left": 100, "position": "absolute", "right": undefined, "top": "100px", "width": 100, }, "tabIndex": -1, }, Object { "className": "ouiDataGridRowCell ouiDataGridRowCell--stripe ouiDataGridRowCell--firstColumn customClass", "data-test-subj": "dataGridRowCell", "onBlur": [Function], "onFocus": [Function], "onKeyDown": [Function], "onMouseEnter": [Function], "role": "gridcell", "style": Object { "color": "red", "height": 34, "left": 0, "position": "absolute", "right": undefined, "top": "134px", "width": 100, }, "tabIndex": -1, }, Object { "className": "ouiDataGridRowCell ouiDataGridRowCell--stripe ouiDataGridRowCell--lastColumn customClass", "data-test-subj": "dataGridRowCell", "onBlur": [Function], "onFocus": [Function], "onKeyDown": [Function], "onMouseEnter": [Function], "role": "gridcell", "style": Object { "color": "blue", "height": 34, "left": 100, "position": "absolute", "right": undefined, "top": "134px", "width": 100, }, "tabIndex": -1, }, ] `); }); it('renders correct aria attributes on column headers', () => { const component = mount( {}, }} rowCount={1} renderCellValue={() => 'value'} /> ); // no columns are sorted, expect no aria-sort or aria-describedby attributes expect(component.find('[role="columnheader"][aria-sort]').length).toBe(0); expect( component.find('[role="columnheader"][aria-describedby]').length ).toBe(0); // sort on one column component.setProps({ sorting: { columns: [{ id: 'A', direction: 'asc' }], onSort: () => {} }, }); // expect A column to have aria-sort, expect no aria-describedby expect(component.find('[role="columnheader"][aria-sort]').length).toBe(1); expect( component.find( '[role="columnheader"][aria-sort="ascending"][data-test-subj="dataGridHeaderCell-A"]' ).length ).toBe(1); expect( component.find('[role="columnheader"][aria-describedby]').length ).toBe(0); // sort on both columns component.setProps({ sorting: { columns: [ { id: 'A', direction: 'asc' }, { id: 'B', direction: 'desc' }, ], onSort: () => {}, }, }); // expect no aria-sort, both columns have aria-describedby expect(component.find('[role="columnheader"][aria-sort]').length).toBe(0); expect( component.find('[role="columnheader"][aria-describedby]').length ).toBe(2); expect( component.find('[role="columnheader"][aria-describedby="generated-id"]') .length ).toBe(2); }); it('renders additional toolbar controls', () => { const component = render( {}, }} rowCount={3} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}, ${columnId}` } toolbarVisibility={{ additionalControls: }} /> ); expect(component).toMatchSnapshot(); }); it('renders control columns', () => { const component = render( {}, }} leadingControlColumns={[ { id: 'leading', width: 50, headerCellRender: () => leading heading, rowCellRender: ({ rowIndex }) => rowIndex, }, ]} trailingControlColumns={[ { id: 'trailing', width: 50, headerCellRender: () => trailing heading, rowCellRender: ({ rowIndex }) => rowIndex, }, ]} rowCount={3} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}, ${columnId}` } toolbarVisibility={{ additionalControls: }} /> ); expect(component).toMatchSnapshot(); }); it('can hide the toolbar', () => { const component = mount( {}, }} toolbarVisibility={false} rowCount={1} renderCellValue={() => 'value'} /> ); // The toolbar should not show expect(findTestSubject(component, 'dataGridControls').length).toBe(0); // Check for false / true and unset values component.setProps({ toolbarVisibility: { showFullScreenSelector: false, showSortSelector: false, showStyleSelector: true, }, }); // fullscreen selector expect(findTestSubject(component, 'dataGridFullScrenButton').length).toBe( 0 ); // sort selector expect( findTestSubject(component, 'dataGridColumnSortingButton').length ).toBe(0); // style selector component.debug(); expect( findTestSubject(component, 'dataGridStyleSelectorButton').length ).toBe(1); // column selector expect( findTestSubject(component, 'dataGridColumnSelectorButton').length ).toBe(1); }); describe('schema classnames', () => { it('applies classnames from explicit schemas', () => { const component = mount( {}, }} rowCount={3} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}, ${columnId}` } /> ); const gridCellClassNames = component .find('[className*="ouiDataGridRowCell--"]') .map((x) => x.props().className); expect(gridCellClassNames).toMatchInlineSnapshot(` Array [ "ouiDataGridRowCell--firstColumn", "ouiDataGridRowCell ouiDataGridRowCell--numeric ouiDataGridRowCell--firstColumn", "ouiDataGridRowCell--lastColumn", "ouiDataGridRowCell ouiDataGridRowCell--customFormatName ouiDataGridRowCell--lastColumn", "ouiDataGridRowCell--stripe ouiDataGridRowCell--firstColumn", "ouiDataGridRowCell ouiDataGridRowCell--numeric ouiDataGridRowCell--stripe ouiDataGridRowCell--firstColumn", "ouiDataGridRowCell--stripe ouiDataGridRowCell--lastColumn", "ouiDataGridRowCell ouiDataGridRowCell--customFormatName ouiDataGridRowCell--stripe ouiDataGridRowCell--lastColumn", "ouiDataGridRowCell--firstColumn", "ouiDataGridRowCell ouiDataGridRowCell--numeric ouiDataGridRowCell--firstColumn", "ouiDataGridRowCell--lastColumn", "ouiDataGridRowCell ouiDataGridRowCell--customFormatName ouiDataGridRowCell--lastColumn", ] `); }); it('automatically detects column types and applies classnames', () => { const component = mount( {}, }} inMemory={{ level: 'pagination' }} rowCount={2} renderCellValue={({ columnId }) => { if (columnId === 'A') { return 5.5; } else if (columnId === 'B') { return 'true'; } else { return 'asdf'; } }} /> ); const gridCellClassNames = component .find('[className~="ouiDataGridRowCell"]') .map((x) => x.props().className); expect(gridCellClassNames).toMatchInlineSnapshot(` Array [ "ouiDataGridRowCell ouiDataGridRowCell--numeric ouiDataGridRowCell--firstColumn", "ouiDataGridRowCell ouiDataGridRowCell--boolean", "ouiDataGridRowCell ouiDataGridRowCell--lastColumn", "ouiDataGridRowCell ouiDataGridRowCell--numeric ouiDataGridRowCell--stripe ouiDataGridRowCell--firstColumn", "ouiDataGridRowCell ouiDataGridRowCell--boolean ouiDataGridRowCell--stripe", "ouiDataGridRowCell ouiDataGridRowCell--stripe ouiDataGridRowCell--lastColumn", ] `); }); it('overrides automatically detected column types with supplied schema', () => { const component = mount( {}, }} inMemory={{ level: 'pagination' }} rowCount={2} renderCellValue={({ columnId }) => columnId === 'A' ? 5.5 : 'true' } /> ); const gridCellClassNames = component .find('[className~="ouiDataGridRowCell"]') .map((x) => x.props().className); expect(gridCellClassNames).toMatchInlineSnapshot(` Array [ "ouiDataGridRowCell ouiDataGridRowCell--numeric ouiDataGridRowCell--firstColumn", "ouiDataGridRowCell ouiDataGridRowCell--alphanumeric ouiDataGridRowCell--lastColumn", "ouiDataGridRowCell ouiDataGridRowCell--numeric ouiDataGridRowCell--stripe ouiDataGridRowCell--firstColumn", "ouiDataGridRowCell ouiDataGridRowCell--alphanumeric ouiDataGridRowCell--stripe ouiDataGridRowCell--lastColumn", ] `); }); it('detects all of the supported types', () => { const values: { [key: string]: string } = { A: '-5.80', B: 'false', C: '$-5.80', E: '2019-09-18T12:31:28', F: '2019-09-18T12:31:28Z', G: '2019-09-18T12:31:28.234', H: '2019-09-18T12:31:28.234+0300', }; const component = mount( ({ id }))} columnVisibility={{ visibleColumns: Object.keys(values), setVisibleColumns: () => {}, }} inMemory={{ level: 'pagination' }} rowCount={1} renderCellValue={({ columnId }) => values[columnId]} /> ); const gridCellClassNames = component .find('[className~="ouiDataGridRowCell"]') .map((x) => x.props().className); expect(gridCellClassNames).toMatchInlineSnapshot(` Array [ "ouiDataGridRowCell ouiDataGridRowCell--numeric ouiDataGridRowCell--firstColumn", "ouiDataGridRowCell ouiDataGridRowCell--boolean", "ouiDataGridRowCell ouiDataGridRowCell--currency", "ouiDataGridRowCell ouiDataGridRowCell--datetime", "ouiDataGridRowCell ouiDataGridRowCell--datetime", "ouiDataGridRowCell ouiDataGridRowCell--datetime", "ouiDataGridRowCell ouiDataGridRowCell--datetime ouiDataGridRowCell--lastColumn", ] `); }); it('accepts extra detectors', () => { const values: { [key: string]: string } = { A: '-5.80', B: '127.0.0.1', }; const component = mount( ({ id }))} columnVisibility={{ visibleColumns: Object.keys(values), setVisibleColumns: () => {}, }} schemaDetectors={[ { type: 'ipaddress', detector(value: string) { return value.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/) ? 1 : 0; }, icon: 'alert', color: 'primary', sortTextAsc: 'a-z', sortTextDesc: 'z-a', }, ]} inMemory={{ level: 'pagination' }} rowCount={1} renderCellValue={({ columnId }) => values[columnId]} /> ); const gridCellClassNames = component .find('[className~="ouiDataGridRowCell"]') .map((x) => x.props().className); expect(gridCellClassNames).toMatchInlineSnapshot(` Array [ "ouiDataGridRowCell ouiDataGridRowCell--numeric ouiDataGridRowCell--firstColumn", "ouiDataGridRowCell ouiDataGridRowCell--ipaddress ouiDataGridRowCell--lastColumn", ] `); }); }); }); describe('cell rendering', () => { it('supports hooks', () => { const component = mount( {}, }} rowCount={2} renderCellValue={({ rowIndex, columnId }) => { const [value] = useState(`Hello, Row ${rowIndex}-${columnId}!`); return {value}; }} /> ); expect(extractGridData(component)).toMatchInlineSnapshot(` Array [ Array [ "Column 1", "Column 2", ], Array [ "Hello, Row 0-Column 1!", "Hello, Row 0-Column 2!", ], Array [ "Hello, Row 1-Column 1!", "Hello, Row 1-Column 2!", ], ] `); }); }); describe('pagination', () => { it('renders', () => { const component = mount( {}, }} rowCount={10} renderCellValue={({ rowIndex }) => rowIndex} pagination={{ pageIndex: 1, pageSize: 6, pageSizeOptions: [3, 6, 10], onChangePage: () => {}, onChangeItemsPerPage: () => {}, }} /> ); expect( takeMountedSnapshot(component.find('OuiTablePagination')) ).toMatchSnapshot(); }); describe('page navigation', () => { it('next button pages through content', () => { const component = mount( {}, }} rowCount={8} renderCellValue={({ rowIndex }) => rowIndex} pagination={{ pageIndex: 0, pageSize: 3, pageSizeOptions: [3, 6, 10], onChangePage: jest.fn((pageIndex) => { const pagination = component.props().pagination; component.setProps({ pagination: { ...pagination, pageIndex }, }); }), onChangeItemsPerPage: jest.fn(), }} /> ); expect(extractGridData(component)).toEqual([ ['Column'], ['0'], ['1'], ['2'], ]); findTestSubject(component, 'pagination-button-next').simulate('click'); expect(component.props().pagination.onChangePage).toHaveBeenCalledTimes( 1 ); const firstCallPageIndex = component.props().pagination.onChangePage .mock.calls[0][0]; expect(firstCallPageIndex).toBe(1); expect(extractGridData(component)).toEqual([ ['Column'], ['3'], ['4'], ['5'], ]); findTestSubject(component, 'pagination-button-next').simulate('click'); expect(component.props().pagination.onChangePage).toHaveBeenCalledTimes( 2 ); const secondCallPageIndex = component.props().pagination.onChangePage .mock.calls[1][0]; expect(secondCallPageIndex).toBe(2); expect(extractGridData(component)).toEqual([['Column'], ['6'], ['7']]); }); it('pages are navigable through page links', () => { const component = mount( {}, }} rowCount={8} renderCellValue={({ rowIndex }) => rowIndex} pagination={{ pageIndex: 0, pageSize: 3, pageSizeOptions: [3, 6, 10], onChangePage: jest.fn((pageIndex) => { const pagination = component.props().pagination; component.setProps({ pagination: { ...pagination, pageIndex }, }); }), onChangeItemsPerPage: jest.fn(), }} /> ); expect(extractGridData(component)).toEqual([ ['Column'], ['0'], ['1'], ['2'], ]); // goto page 3 findTestSubject(component, 'pagination-button-2').simulate('click'); expect(component.props().pagination.onChangePage).toHaveBeenCalledTimes( 1 ); const firstCallPageIndex = component.props().pagination.onChangePage .mock.calls[0][0]; expect(firstCallPageIndex).toBe(2); expect(extractGridData(component)).toEqual([['Column'], ['6'], ['7']]); // goto page 2 findTestSubject(component, 'pagination-button-1').simulate('click'); expect(component.props().pagination.onChangePage).toHaveBeenCalledTimes( 2 ); const secondCallPageIndex = component.props().pagination.onChangePage .mock.calls[1][0]; expect(secondCallPageIndex).toBe(1); expect(extractGridData(component)).toEqual([ ['Column'], ['3'], ['4'], ['5'], ]); }); }); it('changes the page size', () => { const component = mount( {}, }} rowCount={8} renderCellValue={({ rowIndex }) => rowIndex} pagination={{ pageIndex: 0, pageSize: 3, pageSizeOptions: [3, 6, 10], onChangePage: jest.fn(), onChangeItemsPerPage: jest.fn((pageSize) => { const pagination = component.props().pagination; component.setProps({ pagination: { ...pagination, pageSize }, }); }), }} /> ); expect(extractGridData(component)).toEqual([ ['Column'], ['0'], ['1'], ['2'], ]); findTestSubject(component, 'tablePaginationPopoverButton').simulate( 'click' ); const rowButtons: NodeListOf = document.body.querySelectorAll( '.ouiContextMenuItem' ); expect( Array.prototype.map.call( rowButtons, (button: HTMLDivElement) => button.textContent || '' ) ).toEqual(['3 rows', '6 rows', '10 rows']); rowButtons[1].click(); expect( component.props().pagination.onChangeItemsPerPage ).toHaveBeenCalledTimes(1); const firstCallPageIndex = component.props().pagination .onChangeItemsPerPage.mock.calls[0][0]; expect(firstCallPageIndex).toBe(6); expect(extractGridData(component)).toEqual([ ['Column'], ['0'], ['1'], ['2'], ['3'], ['4'], ['5'], ]); }); }); describe('column sizing', () => { it('uses a columns initialWidth', () => { const component = mount( {}, }} rowCount={3} renderCellValue={() => 'value'} /> ); const originalCellWidths = extractColumnWidths(component); expect(originalCellWidths).toEqual({ 'Column 1': 400, 'Column 2': 100, }); }); describe('resizing', () => { it('resizes a column by grab handles', () => { const component = mount( {}, }} rowCount={3} renderCellValue={() => 'value'} /> ); const originalCellWidths = extractColumnWidths(component); expect(originalCellWidths).toEqual({ 'Column 1': 100, 'Column 2': 100, }); resizeColumn(component, 'Column 1', 150); const updatedCellWidths = extractColumnWidths(component); expect(updatedCellWidths).toEqual({ 'Column 1': 150, 'Column 2': 100, }); }); it('should listen for column resize', () => { const onColumnResizeCallback = jest.fn(); const component = mount( {}, }} rowCount={3} renderCellValue={() => 'value'} onColumnResize={(args) => onColumnResizeCallback(args)} /> ); resizeColumn(component, 'Column 1', 150); resizeColumn(component, 'Column 2', 100); expect(onColumnResizeCallback.mock.calls.length).toBe(2); expect(onColumnResizeCallback.mock.calls[0][0]).toEqual({ columnId: 'Column 1', width: 150, }); expect(onColumnResizeCallback.mock.calls[1][0]).toEqual({ columnId: 'Column 2', width: 100, }); }); it('is prevented by isResizable:false', () => { const component = mount( {}, }} rowCount={3} renderCellValue={() => 'value'} /> ); const originalCellWidths = extractColumnWidths(component); expect(originalCellWidths).toEqual({ 'Column 1': 100, 'Column 2': 100, }); // verify there is no resizer on Column 1 but that there is on Column 2 expect( component.find('OuiDataGridColumnResizer[columnId="Column 1"]').length ).toBe(0); expect( component.find('OuiDataGridColumnResizer[columnId="Column 2"]').length ).toBe(1); }); it('does not trigger value re-renders', () => { const renderCellValue = jest.fn(() => 'value'); const component = mount( {}, }} rowCount={3} renderCellValue={renderCellValue} /> ); expect(renderCellValue).toHaveBeenCalledTimes(3); renderCellValue.mockClear(); resizeColumn(component, 'ColumnA', 200); expect(extractColumnWidths(component)).toEqual({ ColumnA: 200 }); expect(renderCellValue).toHaveBeenCalledTimes(0); }); }); }); describe('column options', () => { it('column visibility can be toggled', () => { const columnVisibility = { visibleColumns: ['ColumnA', 'ColumnB'], setVisibleColumns: (visibleColumns: string[]) => { columnVisibility.visibleColumns = visibleColumns; component.setProps({ columnVisibility }); }, }; const component = mount( `${rowIndex}-${columnId}` } /> ); expect(extractGridData(component)).toEqual([ ['ColumnA', 'ColumnB'], ['0-ColumnA', '0-ColumnB'], ['1-ColumnA', '1-ColumnB'], ]); setColumnVisibility(component, 'ColumnA', false); expect(extractGridData(component)).toEqual([ ['ColumnB'], ['0-ColumnB'], ['1-ColumnB'], ]); setColumnVisibility(component, 'ColumnA', true); expect(extractGridData(component)).toEqual([ ['ColumnA', 'ColumnB'], ['0-ColumnA', '0-ColumnB'], ['1-ColumnA', '1-ColumnB'], ]); }); it('column order can be changed', () => { const columnVisibility = { visibleColumns: ['ColumnA', 'ColumnB'], setVisibleColumns: (visibleColumns: string[]) => { columnVisibility.visibleColumns = visibleColumns; component.setProps({ columnVisibility }); }, }; const component = mount( `${rowIndex}-${columnId}` } /> ); expect(extractGridData(component)).toEqual([ ['ColumnA', 'ColumnB'], ['0-ColumnA', '0-ColumnB'], ['1-ColumnA', '1-ColumnB'], ]); moveColumnToIndex(component, 'ColumnB', 0); expect(extractGridData(component)).toEqual([ ['ColumnB', 'ColumnA'], ['0-ColumnB', '0-ColumnA'], ['1-ColumnB', '1-ColumnA'], ]); }); }); describe('column sorting', () => { it('calls the onSort callback', () => { const onSort = jest.fn((columns) => { component.setProps({ sorting: { columns, onSort } }); component.update(); }); const component = mount( {}, }} rowCount={1} sorting={{ columns: [], onSort, }} renderCellValue={() => 'hello'} /> ); sortByColumn(component, 'ColumnA', 'desc'); expect(onSort).toHaveBeenCalledTimes(2); expect(onSort).toHaveBeenCalledWith([ { id: 'ColumnA', direction: 'asc' }, ]); expect(onSort).toHaveBeenCalledWith([ { id: 'ColumnA', direction: 'desc' }, ]); const [, sortDirection] = getColumnSortDirection(component, 'ColumnA'); expect(sortDirection).toBe('desc'); }); describe('in-memory sorting', () => { it('sorts on initial render', () => { const component = mount( {}, }} rowCount={5} renderCellValue={({ rowIndex, columnId }) => // render A 0->4 and B 9->5 columnId === 'A' ? rowIndex : 9 - rowIndex } inMemory={{ level: 'sorting' }} sorting={{ columns: [{ id: 'A', direction: 'desc' }], onSort: () => {}, }} /> ); expect(extractGridData(component)).toEqual([ ['A', 'B'], ['4', '5'], ['3', '6'], ['2', '7'], ['1', '8'], ['0', '9'], ]); }); it('sorts on multiple columns', () => { const component = mount( {}, }} rowCount={5} renderCellValue={({ rowIndex, columnId }) => // render A as 0, 1, 0, 1, 0 and B as 9->5 columnId === 'A' ? rowIndex % 2 : 9 - rowIndex } inMemory={{ level: 'sorting' }} sorting={{ columns: [ { id: 'A', direction: 'desc' }, { id: 'B', direction: 'asc' }, ], onSort: () => {}, }} /> ); expect(extractGridData(component)).toEqual([ ['A', 'B'], ['1', '6'], ['1', '8'], ['0', '5'], ['0', '7'], ['0', '9'], ]); }); it('sorts in response to user interaction', () => { const onSort = jest.fn((columns) => { component.setProps({ sorting: { columns, onSort } }); component.update(); }); const component = mount( {}, }} rowCount={5} renderCellValue={({ rowIndex, columnId }) => // render A as 0, 1, 0, 1, 0 and B as 9->5 columnId === 'A' ? rowIndex % 2 : 9 - rowIndex } inMemory={{ level: 'sorting' }} sorting={{ columns: [], onSort, }} /> ); expect(extractGridData(component)).toEqual([ ['A', 'B'], ['0', '9'], ['1', '8'], ['0', '7'], ['1', '6'], ['0', '5'], ]); sortByColumn(component, 'A', 'desc'); expect(extractGridData(component)).toEqual([ ['A', 'B'], ['1', '8'], ['1', '6'], ['0', '9'], ['0', '7'], ['0', '5'], ]); sortByColumn(component, 'B', 'asc'); expect(extractGridData(component)).toEqual([ ['A', 'B'], ['1', '6'], ['1', '8'], ['0', '5'], ['0', '7'], ['0', '9'], ]); }); it('sorts with all digit groups in numerical-like', () => { const onSort = jest.fn((columns) => { component.setProps({ sorting: { columns, onSort } }); component.update(); }); const component = mount( {}, }} rowCount={5} renderCellValue={ ({ rowIndex }) => `1.0.${(rowIndex % 3) + rowIndex}` // computes as 0,2,4,3,5 } inMemory={{ level: 'sorting' }} sorting={{ columns: [], onSort, }} /> ); // verify rows are unordered expect(extractGridData(component)).toEqual([ ['version'], ['1.0.0'], ['1.0.2'], ['1.0.4'], ['1.0.3'], ['1.0.5'], ]); sortByColumn(component, 'version', 'asc'); expect(extractGridData(component)).toEqual([ ['version'], ['1.0.0'], ['1.0.2'], ['1.0.3'], ['1.0.4'], ['1.0.5'], ]); }); }); it('uses schema information to sort', () => { const component = mount( {}, }} rowCount={5} renderCellValue={({ rowIndex, columnId }) => // render A 0->4 and B 12->8 columnId === 'A' ? rowIndex : 12 - rowIndex } inMemory={{ level: 'sorting' }} sorting={{ columns: [{ id: 'B', direction: 'asc' }], onSort: () => {}, }} /> ); expect(extractGridData(component)).toEqual([ ['A', 'B'], ['4', '8'], ['3', '9'], ['2', '10'], ['1', '11'], ['0', '12'], ]); }); }); describe('updating column definitions', () => { it('renders the new set', () => { const component = mount( {}, }} rowCount={2} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}-${columnId}` } /> ); expect(extractGridData(component)).toEqual([ ['A', 'B'], ['0-A', '0-B'], ['1-A', '1-B'], ]); component.setProps({ columns: [{ id: 'A' }, { id: 'C' }], columnVisibility: { visibleColumns: ['A', 'C'], setVisibleColumns: () => {}, }, }); expect(extractGridData(component)).toEqual([ ['A', 'C'], ['0-A', '0-C'], ['1-A', '1-C'], ]); }); it('"Hide fields" updates', () => { const component = mount( {}, }} rowCount={2} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}-${columnId}` } /> ); // verify original column list is A, B let popover = openColumnSelector(component); expect( popover .find('.ouiDataGridColumnSelector__item') .map((item) => item.text()) ).toEqual(['A', 'B']); closeColumnSelector(component); // update columns component.setProps({ columns: [{ id: 'A' }, { id: 'C' }], columnVisibility: { visibleColumns: ['A', 'C'], setVisibleColumns: () => {}, }, }); // test that the column list updated to A,C popover = openColumnSelector(component); expect( popover .find('.ouiDataGridColumnSelector__item') .map((item) => item.text()) ).toEqual(['A', 'C']); closeColumnSelector(component); }); it('"Sort fields" updates', () => { const component = mount( {}, }} sorting={{ onSort: () => {}, columns: [], }} rowCount={2} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}-${columnId}` } /> ); // verify original column list is A, B openColumnSorter(component); let popover = openColumnSorterSelection(component); expect( popover .find('.ouiDataGridColumnSorting__field') .map((item) => item.text()) ).toEqual(['A', 'B']); closeColumnSorterSelection(component); closeColumnSorter(component); // update columns component.setProps({ columns: [{ id: 'A' }, { id: 'C' }], columnVisibility: { visibleColumns: ['A', 'C'], setVisibleColumns: () => {}, }, }); // test that the column list updated to A,C openColumnSorter(component); popover = openColumnSorterSelection(component); expect( popover .find('.ouiDataGridColumnSorting__field') .map((item) => item.text()) ).toEqual(['A', 'C']); closeColumnSorterSelection(component); closeColumnSorter(component); }); }); describe('render column actions', () => { it('renders various column actions configurations', () => { const component = mount( {}, }} columns={[ { id: 'A', actions: false }, { id: 'B', isSortable: true }, { id: 'C', isSortable: true, actions: { showHide: false, showMoveRight: false, showMoveLeft: false, showSortAsc: false, showSortDesc: false, additional: [{ label: 'test' }], }, }, { id: 'D', isSortable: true, actions: { showHide: false, showMoveRight: false, showMoveLeft: false, additional: [{ label: 'test' }], }, }, { id: 'E', isSortable: true, actions: { showHide: { label: '1' }, showSortAsc: { label: '2' }, showSortDesc: { label: '3' }, showMoveLeft: { label: '4' }, showMoveRight: { label: '5' }, additional: [{ label: 'test' }], }, }, ]} columnVisibility={{ visibleColumns: ['A', 'B', 'C', 'D', 'E'], setVisibleColumns: () => {}, }} rowCount={2} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}-${columnId}` } /> ); const buttonA = findTestSubject( component, 'dataGridHeaderCellActionButton-A' ); expect(buttonA.length).toBe(0); for (const col of ['B', 'C', 'D', 'E']) { const button = findTestSubject( component, `dataGridHeaderCellActionButton-${col}` ); button.simulate('click'); component.update(); const actionGroup = findTestSubject( component, `dataGridHeaderCellActionGroup-${col}` ); expect(actionGroup).toMatchSnapshot(); } }); }); describe('render sorting arrows', () => { it('renders sorting arrows when direction is given', () => { const component = mount( {}, }} columns={[ { id: 'A', isSortable: true }, { id: 'B', isSortable: true }, ]} columnVisibility={{ visibleColumns: ['A', 'B'], setVisibleColumns: () => {}, }} rowCount={2} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}-${columnId}` } /> ); const arrowA = findTestSubject( component, 'dataGridHeaderCellSortingIcon-A' ); expect(arrowA.length).toBe(1); const arrowB = findTestSubject( component, 'dataGridHeaderCellSortingIcon-B' ); expect(arrowB.length).toBe(1); }); it('does not render the arrows if the column is not sorted', () => { const component = mount( {}, }} columns={[ { id: 'C', isSortable: true, actions: { showHide: false, showMoveRight: false, showMoveLeft: false, showSortAsc: false, showSortDesc: false, additional: [{ label: 'test' }], }, }, ]} columnVisibility={{ visibleColumns: ['C'], setVisibleColumns: () => {}, }} rowCount={2} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}-${columnId}` } /> ); const arrowC = findTestSubject( component, 'dataGridHeaderCellSortingIcon-C' ); expect(arrowC.length).toBe(0); }); it('renders the icons if they are sorted but user is not allowed to perform any action', () => { const component = mount( {}, }} columns={[{ id: 'D', actions: false }]} columnVisibility={{ visibleColumns: ['D'], setVisibleColumns: () => {}, }} rowCount={2} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}-${columnId}` } /> ); const arrowD = findTestSubject( component, 'dataGridHeaderCellSortingIcon-D' ); expect(arrowD.length).toBe(1); }); }); describe('render column cell actions', () => { it('renders various column cell actions configurations after cell gets hovered', async () => { const alertFn = jest.fn(); const happyFn = jest.fn(); const component = mount( {}, }} columns={[ { id: 'A', isSortable: true, cellActions: [ ({ rowIndex, columnId, Component, isExpanded }) => { return ( alertFn(rowIndex, columnId)} iconType="alert" aria-label="test1 aria label" data-test-subj={ isExpanded ? 'alertActionPopover' : 'alertAction' }> test1 ); }, ({ rowIndex, columnId, Component, isExpanded }) => { return ( happyFn(rowIndex, columnId)} iconType="faceHappy" aria-label="test2 aria label" data-test-subj={ isExpanded ? 'happyActionPopover' : 'happyAction' }> test2 ); }, ], }, ]} columnVisibility={{ visibleColumns: ['A'], setVisibleColumns: () => {}, }} rowCount={2} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}-${columnId}` } /> ); // cell buttons should not get rendered for unfocused, unhovered cell expect(findTestSubject(component, 'alertAction').exists()).toBe(false); expect(findTestSubject(component, 'happyAction').exists()).toBe(false); findTestSubject(component, 'dataGridRowCell').at(1).prop('onMouseEnter')!( {} as React.MouseEvent ); component.update(); findTestSubject(component, 'alertAction').at(0).simulate('click'); expect(alertFn).toHaveBeenCalledWith(1, 'A'); findTestSubject(component, 'happyAction').at(0).simulate('click'); expect(happyFn).toHaveBeenCalledWith(1, 'A'); alertFn.mockReset(); happyFn.mockReset(); findTestSubject(component, 'dataGridRowCell') .at(1) .simulate('keydown', { key: keys.ENTER }); component.update(); findTestSubject(component, 'alertActionPopover').simulate('click'); expect(alertFn).toHaveBeenCalledWith(1, 'A'); findTestSubject(component, 'happyActionPopover').simulate('click'); expect(happyFn).toHaveBeenCalledWith(1, 'A'); }); }); describe('rowHeighsOptions', () => { it('all row heights options applied correctly', async () => { const component = mount( {}, }} rowCount={3} renderCellValue={() => 'value'} rowHeightsOptions={{ defaultHeight: 50, rowHeights: { 0: 70, 1: { lineCount: 3, }, }, }} /> ); const cellHeights = extractRowHeights(component); expect(cellHeights).toEqual({ 0: 70, 1: 3, 2: 50, }); }); it('render cells with correct height during pagination', () => { const component = mount( {}, }} rowCount={8} renderCellValue={({ rowIndex }) => rowIndex} rowHeightsOptions={{ defaultHeight: 50, rowHeights: { 0: 70, 1: { lineCount: 3, }, }, }} pagination={{ pageIndex: 0, pageSize: 3, pageSizeOptions: [3, 6, 10], onChangePage: jest.fn((pageIndex) => { const pagination = component.props().pagination; component.setProps({ pagination: { ...pagination, pageIndex }, }); }), onChangeItemsPerPage: jest.fn(), }} /> ); expect(extractRowHeights(component)).toEqual({ 0: 70, 1: 3, 2: 50, }); findTestSubject(component, 'pagination-button-next').simulate('click'); expect(extractRowHeights(component)).toEqual({ 3: 50, 4: 50, 5: 50, }); findTestSubject(component, 'pagination-button-previous').simulate( 'click' ); expect(extractRowHeights(component)).toEqual({ 0: 70, 1: 3, 2: 50, }); }); }); describe('keyboard controls', () => { it('supports simple arrow navigation', async () => { let pagination = { pageIndex: 0, pageSize: 3, pageSizeOptions: [3, 6, 10], onChangePage: (pageIndex: number) => { pagination = { ...pagination, pageIndex, }; component.setProps({ pagination }); }, onChangeItemsPerPage: () => {}, }; const component = mount( {}, }} rowCount={8} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}, ${columnId}` } pagination={pagination} /> ); // enable the grid to accept focus act(() => component .find('div [data-test-subj="dataGridWrapper"][onFocus]') .props().onFocus!({} as React.FocusEvent) ); component.update(); let focusableCell = getFocusableCell(component); // focus should begin at the first cell expect(focusableCell.length).toEqual(1); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('0, A'); // focus should not move when up against the left edge focusableCell .simulate('focus') .simulate('keydown', { key: keys.ARROW_LEFT }); focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('0, A'); // focus should not move when up against the top edge focusableCell.simulate('keydown', { key: keys.ARROW_UP }); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('0, A'); // move down focusableCell.simulate('keydown', { key: keys.ARROW_DOWN }); focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('1, A'); // move right focusableCell.simulate('keydown', { key: keys.ARROW_RIGHT }); focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('1, B'); // move up focusableCell.simulate('keydown', { key: keys.ARROW_UP }); focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('0, B'); // move left focusableCell.simulate('keydown', { key: keys.ARROW_LEFT }); focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('0, A'); // move down and to the end of the row focusableCell.simulate('keydown', { key: keys.ARROW_DOWN }); focusableCell = getFocusableCell(component); focusableCell.simulate('keydown', { key: keys.END }); focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('1, C'); // move up and to the beginning of the row focusableCell .simulate('keydown', { key: keys.ARROW_UP }) .simulate('keydown', { key: keys.HOME }); focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('0, A'); // jump to the last cell focusableCell.simulate('keydown', { ctrlKey: true, key: keys.END, }); focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('2, C'); // jump to the first cell focusableCell.simulate('keydown', { ctrlKey: true, key: keys.HOME, }); focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('0, A'); // page should not change when moving before the first entry // but the last row should remain focused focusableCell.simulate('keydown', { key: keys.PAGE_UP, }); focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('2, A'); // advance to the next page focusableCell.simulate('keydown', { key: keys.PAGE_DOWN, }); focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('3, A'); // move over one column and advance one more page focusableCell .simulate('keydown', { key: keys.ARROW_RIGHT }) // 3, B .simulate('keydown', { key: keys.PAGE_DOWN, }); // 6, B focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('6, B'); // does not advance beyond the last page focusableCell.simulate('keydown', { key: keys.PAGE_DOWN, }); focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('6, B'); // move left one column, return to the previous page focusableCell .simulate('keydown', { key: keys.ARROW_LEFT }) // 6, A .simulate('keydown', { key: keys.PAGE_UP, }); // 5, A focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('5, A'); // return to the previous (first) page focusableCell.simulate('keydown', { key: keys.PAGE_UP, }); focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('2, A'); // move to the last cell of the page then advance one page focusableCell .simulate('keydown', { ctrlKey: true, key: keys.END, }) // 2, C (last cell of the first page) .simulate('keydown', { key: keys.PAGE_DOWN, }); // 3, C (first cell of the second page, same cell position as previous page) focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('3, C'); // advance to the final page focusableCell.simulate('keydown', { key: keys.PAGE_DOWN, }); // 6, C focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('6, C'); }); it('does not break arrow key focus control behavior when also using a mouse', async () => { const component = mount( {}, }} rowCount={3} renderCellValue={({ rowIndex, columnId }) => `${rowIndex}, ${columnId}` } /> ); // enable the grid to accept focus act(() => component .find('div [data-test-subj="dataGridWrapper"][onFocus]') .props().onFocus!({} as React.FocusEvent) ); component.update(); let focusableCell = getFocusableCell(component); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('0, A'); findTestSubject(component, 'dataGridRowCell').at(3).simulate('focus'); // wait for a tick to give focus logic time to run await act(async () => { await new Promise((r) => setTimeout(r, 0)); }); component.update(); focusableCell = getFocusableCell(component); expect(focusableCell.length).toEqual(1); expect( focusableCell.find('[data-test-subj="cell-content"]').text() ).toEqual('1, B'); }); it.skip('supports arrow navigation through grids with different interactive cells', () => { const component = mount( {}, }} rowCount={2} renderCellValue={({ rowIndex, columnId }) => { if (columnId === 'A') { return `${rowIndex}, A`; } if (columnId === 'B') { return ; } if (columnId === 'C') { return ( <> , ); } if (columnId === 'D') { return (
{rowIndex},
); } return 'error'; }} /> ); /** * Make sure we start from a happy state */ let focusableCell = getFocusableCell(component); expect(focusableCell.length).toEqual(1); expect(focusableCell.text()).toEqual('0, A'); focusableCell .simulate('focus') .simulate('keydown', { key: keys.ARROW_DOWN }); /** * On text only cells, the cell receives focus */ focusableCell = getFocusableCell(component); expect(focusableCell.text()).toEqual('1, A'); // make sure we're on the right cell expect(focusableCell.getDOMNode()).toBe(document.activeElement); focusableCell.simulate('keydown', { key: keys.ARROW_RIGHT }); /** * On cells with 1 interactive item, the interactive item receives focus */ focusableCell = getFocusableCell(component); expect(focusableCell.text()).toEqual('1, B'); expect(focusableCell.find('button').getDOMNode()).toBe( document.activeElement ); focusableCell.simulate('keydown', { key: keys.ARROW_RIGHT }); /** * On cells with multiple interactive items, the cell receives focus */ focusableCell = getFocusableCell(component); expect(focusableCell.text()).toEqual('1, C'); expect(focusableCell.getDOMNode()).toBe(document.activeElement); focusableCell.simulate('keydown', { key: keys.ARROW_RIGHT }); /** * On cells with 1 interactive item and non-interactive item(s), the cell receives focus */ focusableCell = getFocusableCell(component); expect(focusableCell.text()).toEqual('1, D'); expect(focusableCell.getDOMNode()).toBe(document.activeElement); }); it.skip('allows user to enter and exit grid navigation', async () => { const component = mount( {}, }} rowCount={3} renderCellValue={({ rowIndex, columnId }) => ( <> , )} /> ); /** * Make sure we start from a happy state */ let focusableCell = getFocusableCell(component); expect(focusableCell.length).toEqual(1); expect(focusableCell.text()).toEqual('0, A'); focusableCell .simulate('focus') .simulate('keydown', { key: keys.ARROW_DOWN }); focusableCell = getFocusableCell(component); /** * Confirm initial state is with grid navigation turn on */ expect(focusableCell.text()).toEqual('1, A'); expect(focusableCell.getDOMNode()).toBe(document.activeElement); expect(takeMountedSnapshot(component)).toMatchSnapshot(); /** * Disable grid navigation using ENTER */ focusableCell .simulate('keydown', { key: keys.ENTER }) .simulate('keydown', { key: keys.ARROW_DOWN }); let buttons = focusableCell.find('button'); // grid navigation is disabled, location should not move expect(buttons.at(0).text()).toEqual('1'); expect(buttons.at(1).text()).toEqual('A'); expect(buttons.at(0).getDOMNode()).toBe(document.activeElement); // focus should move to first button expect(takeMountedSnapshot(component)).toMatchSnapshot(); // should prove focus lock is on /** * Enable grid navigation ESCAPE */ focusableCell.simulate('keydown', { key: keys.ESCAPE }); focusableCell = getFocusableCell(component); expect(focusableCell.getDOMNode()).toBe(document.activeElement); // focus should move back to cell focusableCell.simulate('keydown', { key: keys.ARROW_RIGHT }); focusableCell = getFocusableCell(component); expect(focusableCell.text()).toEqual('1, B'); // grid navigation is enabled again, check that we can move expect(takeMountedSnapshot(component)).toMatchSnapshot(); /** * Disable grid navigation using F2 */ focusableCell = getFocusableCell(component); focusableCell .simulate('keydown', { key: keys.F2 }) .simulate('keydown', { key: keys.ARROW_UP }); buttons = focusableCell.find('button'); // grid navigation is disabled, location should not move expect(buttons.at(0).text()).toEqual('1'); expect(buttons.at(1).text()).toEqual('B'); expect(buttons.at(0).getDOMNode()).toBe(document.activeElement); // focus should move to first button expect(takeMountedSnapshot(component)).toMatchSnapshot(); // should prove focus lock is on /** * Enable grid navigation using F2 */ focusableCell.simulate('keydown', { key: keys.F2 }); focusableCell = getFocusableCell(component); expect(focusableCell.getDOMNode()).toBe(document.activeElement); // focus should move back to cell focusableCell.simulate('keydown', { key: keys.ARROW_UP }); focusableCell = getFocusableCell(component); expect(focusableCell.text()).toEqual('0, B'); // grid navigation is enabled again, check that we can move expect(takeMountedSnapshot(component)).toMatchSnapshot(); }); }); });