diff --git a/src-docs/src/views/datagrid/datagrid_ref_example.js b/src-docs/src/views/datagrid/datagrid_ref_example.js
index ee550b19479..40a2c47f54b 100644
--- a/src-docs/src/views/datagrid/datagrid_ref_example.js
+++ b/src-docs/src/views/datagrid/datagrid_ref_example.js
@@ -28,7 +28,7 @@ export const DataGridRefExample = {
{
source: [
{
- type: GuideSectionTypes.JS,
+ type: GuideSectionTypes.TSX,
code: dataGridRefSource,
},
],
@@ -63,10 +63,30 @@ export const DataGridRefExample = {
diff --git a/src-docs/src/views/datagrid/ref.js b/src-docs/src/views/datagrid/ref.tsx
similarity index 78%
rename from src-docs/src/views/datagrid/ref.js
rename to src-docs/src/views/datagrid/ref.tsx
index 2ce537c6884..267892df41e 100644
--- a/src-docs/src/views/datagrid/ref.js
+++ b/src-docs/src/views/datagrid/ref.tsx
@@ -1,4 +1,5 @@
import React, { useCallback, useMemo, useState, useRef } from 'react';
+// @ts-ignore - faker does not have type declarations
import { fake } from 'faker';
import {
@@ -9,15 +10,16 @@ import {
EuiFieldNumber,
EuiButton,
EuiDataGrid,
+ EuiDataGridRefProps,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiText,
-} from '../../../../src/components/';
+} from '../../../../src/components';
-const raw_data = [];
+const raw_data: Array<{ [key: string]: string }> = [];
for (let i = 1; i < 100; i++) {
raw_data.push({
name: fake('{{name.lastName}}, {{name.firstName}}'),
@@ -29,20 +31,23 @@ for (let i = 1; i < 100; i++) {
}
export default () => {
- const dataGridRef = useRef();
+ const dataGridRef = useRef(null);
// Modal
const [isModalVisible, setIsModalVisible] = useState(false);
- const [lastFocusedCell, setLastFocusedCell] = useState({});
+ const [lastFocusedCell, setLastFocusedCell] = useState({
+ rowIndex: 0,
+ colIndex: 0,
+ });
const closeModal = useCallback(() => {
setIsModalVisible(false);
- dataGridRef.current.setFocusedCell(lastFocusedCell); // Set the data grid focus back to the cell that opened the modal
+ dataGridRef.current!.setFocusedCell(lastFocusedCell); // Set the data grid focus back to the cell that opened the modal
}, [lastFocusedCell]);
const showModal = useCallback(({ rowIndex, colIndex }) => {
setIsModalVisible(true);
- dataGridRef.current.closeCellPopover(); // Close any open cell popovers
+ dataGridRef.current!.closeCellPopover(); // Close any open cell popovers
setLastFocusedCell({ rowIndex, colIndex }); // Store the cell that opened this modal
}, []);
@@ -101,11 +106,18 @@ export default () => {
// Pagination
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 });
- const onChangePage = useCallback(
- (pageIndex) =>
- setPagination((pagination) => ({ ...pagination, pageIndex })),
- []
- );
+ const onChangePage = useCallback((pageIndex) => {
+ setPagination((pagination) => ({ ...pagination, pageIndex }));
+ }, []);
+ const onChangePageSize = useCallback((pageSize) => {
+ setPagination((pagination) => ({ ...pagination, pageSize }));
+ }, []);
+
+ // Sorting
+ const [sortingColumns, setSortingColumns] = useState([]);
+ const onSort = useCallback((sortingColumns) => {
+ setSortingColumns(sortingColumns);
+ }, []);
// Manual cell focus
const [rowIndexAction, setRowIndexAction] = useState(0);
@@ -118,7 +130,7 @@ export default () => {
setRowIndexAction(Number(e.target.value))}
compressed
@@ -129,7 +141,7 @@ export default () => {
setColIndexAction(Number(e.target.value))}
compressed
@@ -140,7 +152,7 @@ export default () => {
- dataGridRef.current.setFocusedCell({
+ dataGridRef.current!.setFocusedCell({
rowIndex: rowIndexAction,
colIndex: colIndexAction,
})
@@ -153,7 +165,7 @@ export default () => {
- dataGridRef.current.openCellPopover({
+ dataGridRef.current!.openCellPopover({
rowIndex: rowIndexAction,
colIndex: colIndexAction,
})
@@ -165,7 +177,7 @@ export default () => {
dataGridRef.current.setIsFullScreen(true)}
+ onClick={() => dataGridRef.current!.setIsFullScreen(true)}
>
Set grid to full screen
@@ -177,14 +189,17 @@ export default () => {
aria-label="Data grid demo"
columns={columns}
columnVisibility={{ visibleColumns, setVisibleColumns }}
+ sorting={{ columns: sortingColumns, onSort }}
+ inMemory={{ level: 'sorting' }}
rowCount={raw_data.length}
renderCellValue={({ rowIndex, columnId }) =>
raw_data[rowIndex][columnId]
}
pagination={{
...pagination,
- pageSizeOptions: [25],
+ pageSizeOptions: [25, 50],
onChangePage: onChangePage,
+ onChangeItemsPerPage: onChangePageSize,
}}
height={400}
ref={dataGridRef}
diff --git a/src/components/datagrid/body/data_grid_cell.tsx b/src/components/datagrid/body/data_grid_cell.tsx
index 7413dfc58e8..b6041330b0f 100644
--- a/src/components/datagrid/body/data_grid_cell.tsx
+++ b/src/components/datagrid/body/data_grid_cell.tsx
@@ -490,7 +490,7 @@ export class EuiDataGridCell extends Component<
rowManager,
...rest
} = this.props;
- const { rowIndex, colIndex } = rest;
+ const { rowIndex, visibleRowIndex, colIndex } = rest;
const popoverIsOpen = this.isPopoverOpen();
const hasCellButtons = isExpandable || column?.cellActions;
@@ -534,7 +534,7 @@ export class EuiDataGridCell extends Component<
case keys.ENTER:
case keys.F2:
event.preventDefault();
- openCellPopover({ rowIndex, colIndex });
+ openCellPopover({ rowIndex: visibleRowIndex, colIndex });
break;
}
} else {
@@ -641,7 +641,7 @@ export class EuiDataGridCell extends Component<
if (popoverIsOpen) {
closeCellPopover();
} else {
- openCellPopover({ rowIndex, colIndex });
+ openCellPopover({ rowIndex: visibleRowIndex, colIndex });
}
}}
/>
diff --git a/src/components/datagrid/body/header/data_grid_header_cell.test.tsx b/src/components/datagrid/body/header/data_grid_header_cell.test.tsx
index e17c2f65a82..2e26839e72f 100644
--- a/src/components/datagrid/body/header/data_grid_header_cell.test.tsx
+++ b/src/components/datagrid/body/header/data_grid_header_cell.test.tsx
@@ -49,7 +49,7 @@ describe('EuiDataGridHeaderCell', () => {
diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx
index 019b20824b7..846d8f27d73 100644
--- a/src/components/datagrid/data_grid.test.tsx
+++ b/src/components/datagrid/data_grid.test.tsx
@@ -6,10 +6,10 @@
* Side Public License, v 1.
*/
-import React, { useEffect, useState, createRef } from 'react';
+import React, { useEffect, useState } from 'react';
import { mount, ReactWrapper, render } from 'enzyme';
import { EuiDataGrid } from './';
-import { EuiDataGridProps, EuiDataGridRefProps } from './data_grid_types';
+import { EuiDataGridProps } from './data_grid_types';
import {
findTestSubject,
requiredProps,
@@ -2749,29 +2749,4 @@ describe('EuiDataGrid', () => {
expect(takeMountedSnapshot(component)).toMatchSnapshot();
});
});
-
- it('returns a ref which exposes internal imperative APIs', () => {
- const gridRef = createRef();
-
- mount(
- {},
- }}
- rowCount={1}
- renderCellValue={() => 'value'}
- ref={gridRef}
- />
- );
-
- expect(gridRef.current).toEqual({
- setIsFullScreen: expect.any(Function),
- setFocusedCell: expect.any(Function),
- openCellPopover: expect.any(Function),
- closeCellPopover: expect.any(Function),
- });
- });
});
diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx
index e84c0caa00a..bed2f94fb14 100644
--- a/src/components/datagrid/data_grid.tsx
+++ b/src/components/datagrid/data_grid.tsx
@@ -13,7 +13,6 @@ import React, {
useMemo,
useRef,
useState,
- useImperativeHandle,
} from 'react';
import {
VariableSizeGrid as Grid,
@@ -56,6 +55,7 @@ import {
schemaDetectors as providedSchemaDetectors,
useMergedSchema,
} from './utils/data_grid_schema';
+import { useImperativeGridRef } from './utils/ref';
import {
EuiDataGridColumn,
EuiDataGridProps,
@@ -302,23 +302,18 @@ export const EuiDataGrid = forwardRef(
};
/**
- * Expose internal APIs as ref to consumer
+ * Expose certain internal APIs as ref to consumer
*/
- const { setFocusedCell } = focusContext; // eslint complains about the dependency array otherwise
- const { openCellPopover, closeCellPopover } = cellPopoverContext;
-
- useImperativeHandle(
+ useImperativeGridRef({
ref,
- () => ({
- setIsFullScreen,
- setFocusedCell: ({ rowIndex, colIndex }) => {
- setFocusedCell([colIndex, rowIndex]); // Transmog args from obj to array
- },
- openCellPopover,
- closeCellPopover,
- }),
- [setFocusedCell, openCellPopover, closeCellPopover]
- );
+ setIsFullScreen,
+ focusContext,
+ cellPopoverContext,
+ sortingContext,
+ pagination,
+ rowCount,
+ visibleColCount,
+ });
/**
* Classes
diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts
index d3d8abdbb1e..48618ab813d 100644
--- a/src/components/datagrid/data_grid_types.ts
+++ b/src/components/datagrid/data_grid_types.ts
@@ -179,10 +179,13 @@ export interface EuiDataGridVisibleRows {
export interface DataGridSortingContextShape {
sorting?: EuiDataGridSorting;
- sortedRowMap: { [key: number]: number };
- getCorrectRowIndex: (rowIndex: number) => number;
+ sortedRowMap: number[];
+ getCorrectRowIndex: (visibleRowIndex: number) => number;
}
+// An array of [x,y] coordinates. Note that the `y` value expected internally is a `visibleRowIndex`
+export type EuiDataGridFocusedCell = [number, number];
+
export interface DataGridFocusContextShape {
focusedCell?: EuiDataGridFocusedCell;
setFocusedCell: (cell: EuiDataGridFocusedCell) => void;
@@ -196,6 +199,7 @@ export interface DataGridFocusContextShape {
export interface DataGridCellPopoverContextShape {
popoverIsOpen: boolean;
+ // Note that the rowIndex used to locate cells internally is a `visibleRowIndex`
cellLocation: { rowIndex: number; colIndex: number };
openCellPopover(args: { rowIndex: number; colIndex: number }): void;
closeCellPopover(): void;
@@ -764,8 +768,6 @@ export interface EuiDataGridInMemory {
skipColumns?: string[];
}
-export type EuiDataGridFocusedCell = [number, number];
-
export interface EuiDataGridInMemoryValues {
[rowIndex: string]: { [columnId: string]: string };
}
diff --git a/src/components/datagrid/utils/ref.spec.tsx b/src/components/datagrid/utils/ref.spec.tsx
new file mode 100644
index 00000000000..59f8937ddd3
--- /dev/null
+++ b/src/components/datagrid/utils/ref.spec.tsx
@@ -0,0 +1,198 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { useState, createRef, forwardRef } from 'react';
+import { EuiDataGrid } from '../';
+import { EuiDataGridRefProps } from '../data_grid_types';
+
+// We need to set up a test component here for sorting/pagination state to work
+// The underlying imperative ref should still be forwarded and work as normal
+const GridTest = forwardRef((_, ref) => {
+ // Pagination
+ const [pageIndex, setPageIndex] = useState(0);
+ const onChangePage = (pageIndex) => setPageIndex(pageIndex);
+
+ // Sorting
+ const [sortingColumns, setSortingColumns] = useState([]);
+ const onSort = (sortingColumns) => setSortingColumns(sortingColumns);
+
+ return (
+ {},
+ }}
+ rowCount={100}
+ renderCellValue={({ rowIndex, columnId }) => `${columnId}, ${rowIndex}`}
+ pagination={{
+ pageIndex,
+ pageSize: 25,
+ onChangePage,
+ onChangeItemsPerPage: () => {},
+ }}
+ sorting={{ columns: sortingColumns, onSort }}
+ inMemory={{ level: 'sorting' }}
+ />
+ );
+});
+GridTest.displayName = 'GridTest';
+
+describe('useImperativeGridRef', () => {
+ const ref = createRef();
+
+ beforeEach(() => {
+ cy.mount();
+ cy.get('[data-test-subj="euiDataGridBody"]'); // Wait for the grid to finish rendering and pass back the ref
+ });
+
+ describe('setIsFullScreen', () => {
+ it('allows the consumer to manually toggle full screen mode', () => {
+ ref.current.setIsFullScreen(true);
+ cy.get('[data-test-subj="euiDataGrid"]').should(
+ 'have.class',
+ 'euiDataGrid--fullScreen'
+ );
+ // Has to be a separate .then() block from the above for some Cypress-y reason
+ cy.then(() => {
+ ref.current.setIsFullScreen(false);
+ cy.get('[data-test-subj="euiDataGrid"]').should(
+ 'not.have.class',
+ 'euiDataGrid--fullScreen'
+ );
+ });
+ });
+ });
+
+ describe('setFocusedCell', () => {
+ it('allows the consumer to manually focus into a specific grid cell', () => {
+ ref.current.setFocusedCell({ rowIndex: 1, colIndex: 1 });
+ cy.focused()
+ .should('have.attr', 'data-gridcell-visible-row-index', '1')
+ .should('have.attr', 'data-gridcell-column-index', '1');
+ });
+
+ it('should scroll to cells that are not in view', () => {
+ ref.current.setFocusedCell({ rowIndex: 24, colIndex: 5 });
+ cy.focused()
+ .should('have.attr', 'data-gridcell-visible-row-index', '24')
+ .should('have.attr', 'data-gridcell-column-index', '5');
+ });
+
+ it('should paginate to cells that are not on the current page', () => {
+ ref.current.setFocusedCell({ rowIndex: 50, colIndex: 0 });
+ cy.get('.euiPagination .euiScreenReaderOnly').should(
+ 'have.text',
+ 'Page 3 of 4'
+ );
+ cy.focused()
+ .should('have.attr', 'data-gridcell-visible-row-index', '0')
+ .should('have.attr', 'data-gridcell-column-index', '0');
+ });
+
+ it('should correctly find the specified rowIndex when sorted', () => {
+ cy.get('[data-test-subj="dataGridHeaderCell-A"]').click();
+ cy.contains('Sort High-Low').click();
+ cy.then(() => {
+ ref.current.setFocusedCell({ rowIndex: 95, colIndex: 0 });
+ cy.focused()
+ .should('have.attr', 'data-gridcell-visible-row-index', '4')
+ .should('have.attr', 'data-gridcell-column-index', '0');
+ });
+ });
+
+ it('should throw an error if the passed cell indices are invalid', () => {
+ cy.on('fail', (err) => {
+ expect(err.message).to.equal(
+ 'Row 150 is not a valid row. The maximum visible row index is 99.'
+ );
+ });
+ ref.current.setFocusedCell({ rowIndex: 150, colIndex: 0 });
+ });
+ });
+
+ describe('openCellPopover', () => {
+ it("allows the consumer to manually open a specific grid cell's popover", () => {
+ ref.current.openCellPopover({ rowIndex: 2, colIndex: 2 });
+ cy.focused()
+ .should('have.attr', 'data-test-subj', 'euiDataGridExpansionPopover')
+ .find('.euiText')
+ .should('have.text', 'C, 2');
+ });
+
+ it('should scroll to cells that are not in view', () => {
+ ref.current.openCellPopover({ rowIndex: 23, colIndex: 0 });
+ cy.focused()
+ .should('have.attr', 'data-test-subj', 'euiDataGridExpansionPopover')
+ .find('.euiText')
+ .should('have.text', 'A, 23');
+ });
+
+ it('should paginate to cells that are not on the current page', () => {
+ ref.current.openCellPopover({ rowIndex: 99, colIndex: 5 });
+ cy.get('.euiPagination .euiScreenReaderOnly').should(
+ 'have.text',
+ 'Page 4 of 4'
+ );
+ cy.focused()
+ .should('have.attr', 'data-test-subj', 'euiDataGridExpansionPopover')
+ .find('.euiText')
+ .should('have.text', 'F, 99');
+ });
+
+ it('should correctly find the specified rowIndex when sorted', () => {
+ cy.get('[data-test-subj="dataGridHeaderCell-A"]').click();
+ cy.contains('Sort High-Low').click();
+ cy.then(() => {
+ ref.current.openCellPopover({ rowIndex: 98, colIndex: 1 });
+ cy.focused()
+ .should('have.attr', 'data-test-subj', 'euiDataGridExpansionPopover')
+ .find('.euiText')
+ .should('have.text', 'B, 98');
+ });
+ });
+
+ it('should throw an error if the passed cell indices are invalid', () => {
+ cy.on('fail', (err) => {
+ expect(err.message).to.equal(
+ 'Column 10 is not a valid column. The maximum visible column index is 5.'
+ );
+ });
+ ref.current.openCellPopover({ rowIndex: 0, colIndex: 10 });
+ });
+ });
+
+ describe('closeCellPopover', () => {
+ it('allows the consumer to manually close any open popovers', () => {
+ ref.current.setFocusedCell({ colIndex: 0, rowIndex: 0 });
+ cy.realPress('Enter');
+ cy.get('[data-test-subj="euiDataGridExpansionPopover"]').should(
+ 'have.length',
+ 1
+ );
+ cy.then(() => {
+ ref.current.closeCellPopover();
+ cy.get('[data-test-subj="euiDataGridExpansionPopover"]').should(
+ 'have.length',
+ 0
+ );
+ });
+ });
+ });
+});
diff --git a/src/components/datagrid/utils/ref.test.ts b/src/components/datagrid/utils/ref.test.ts
new file mode 100644
index 00000000000..bf49b5fcd87
--- /dev/null
+++ b/src/components/datagrid/utils/ref.test.ts
@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { testCustomHook } from '../../../test/test_custom_hook.test_helper';
+import { useCellLocationCheck, useSortPageCheck } from './ref';
+
+// see ref.spec.tsx for E2E useImperativeGridRef tests
+
+describe('useCellLocationCheck', () => {
+ const {
+ return: { checkCellExists },
+ } = testCustomHook(() => useCellLocationCheck(10, 5));
+
+ it("throws an error if the passed rowIndex is higher than the grid's rowCount", () => {
+ expect(() => {
+ checkCellExists({ rowIndex: 12, colIndex: 0 });
+ }).toThrow(
+ 'Row 12 is not a valid row. The maximum visible row index is 9.'
+ );
+ });
+
+ it("throws an error if the passed colIndex is higher than the grid's visibleColCount", () => {
+ expect(() => {
+ checkCellExists({ rowIndex: 1, colIndex: 5 });
+ }).toThrow(
+ 'Column 5 is not a valid column. The maximum visible column index is 4.'
+ );
+ });
+
+ it('does not throw if the rowIndex and colIndex are within grid bounds', () => {
+ expect(() => {
+ checkCellExists({ rowIndex: 0, colIndex: 0 });
+ }).not.toThrow();
+ });
+});
+
+describe('useSortPageCheck', () => {
+ describe('if the grid is not sorted or paginated', () => {
+ const pagination = undefined;
+ const sortedRowMap: number[] = [];
+
+ it('returns the passed rowIndex as-is', () => {
+ const {
+ return: { findVisibleRowIndex },
+ } = testCustomHook(() => useSortPageCheck(pagination, sortedRowMap));
+
+ expect(findVisibleRowIndex(5)).toEqual(5);
+ });
+ });
+
+ describe('if the grid is sorted', () => {
+ const pagination = undefined;
+ const sortedRowMap = [3, 4, 1, 2, 0];
+
+ it('returns the visibleRowIndex of the passed rowIndex (which is the index of the sortedRowMap)', () => {
+ const {
+ return: { findVisibleRowIndex },
+ } = testCustomHook(() => useSortPageCheck(pagination, sortedRowMap));
+
+ expect(findVisibleRowIndex(0)).toEqual(4);
+ expect(findVisibleRowIndex(1)).toEqual(2);
+ expect(findVisibleRowIndex(2)).toEqual(3);
+ expect(findVisibleRowIndex(3)).toEqual(0);
+ expect(findVisibleRowIndex(4)).toEqual(1);
+ });
+ });
+
+ describe('if the grid is paginated', () => {
+ const pagination = {
+ pageSize: 20,
+ pageIndex: 0,
+ onChangePage: jest.fn(),
+ onChangeItemsPerPage: jest.fn(),
+ };
+ const sortedRowMap: number[] = [];
+
+ beforeEach(() => jest.clearAllMocks());
+
+ it('calculates what page the row should be on, paginates to that page, and returns the index of the row on that page', () => {
+ const {
+ return: { findVisibleRowIndex },
+ } = testCustomHook(() => useSortPageCheck(pagination, sortedRowMap));
+
+ expect(findVisibleRowIndex(20)).toEqual(0); // First item on 2nd page
+ expect(pagination.onChangePage).toHaveBeenLastCalledWith(1);
+
+ expect(findVisibleRowIndex(75)).toEqual(15); // 16th item on 4th page
+ expect(pagination.onChangePage).toHaveBeenLastCalledWith(3);
+ });
+
+ it('does not paginate if the user is already on the correct page', () => {
+ const {
+ return: { findVisibleRowIndex },
+ } = testCustomHook(() => useSortPageCheck(pagination, sortedRowMap));
+
+ expect(findVisibleRowIndex(5)).toEqual(5);
+ expect(pagination.onChangePage).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/components/datagrid/utils/ref.ts b/src/components/datagrid/utils/ref.ts
new file mode 100644
index 00000000000..88aebb8eb8d
--- /dev/null
+++ b/src/components/datagrid/utils/ref.ts
@@ -0,0 +1,154 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { useImperativeHandle, useCallback, Ref } from 'react';
+import {
+ EuiDataGridRefProps,
+ EuiDataGridProps,
+ DataGridFocusContextShape,
+ DataGridCellPopoverContextShape,
+ DataGridSortingContextShape,
+} from '../data_grid_types';
+
+interface Dependencies {
+ ref: Ref;
+ setIsFullScreen: EuiDataGridRefProps['setIsFullScreen'];
+ focusContext: DataGridFocusContextShape;
+ cellPopoverContext: DataGridCellPopoverContextShape;
+ sortingContext: DataGridSortingContextShape;
+ pagination: EuiDataGridProps['pagination'];
+ rowCount: number;
+ visibleColCount: number;
+}
+
+export const useImperativeGridRef = ({
+ ref,
+ setIsFullScreen,
+ focusContext,
+ cellPopoverContext,
+ sortingContext: { sortedRowMap },
+ pagination,
+ rowCount,
+ visibleColCount,
+}: Dependencies) => {
+ // Cell location helpers
+ const { checkCellExists } = useCellLocationCheck(rowCount, visibleColCount);
+ const { findVisibleRowIndex } = useSortPageCheck(pagination, sortedRowMap);
+
+ // Focus APIs
+ const { setFocusedCell: _setFocusedCell } = focusContext; // eslint complains about the dependency array otherwise
+
+ // When we pass this API to the consumer, we can't know for sure that
+ // the targeted cell is valid or in view (unlike our internal state, where
+ // both of those states can be guaranteed), so we need to do some extra
+ // checks here to make sure the grid automatically handles all cells
+ const setFocusedCell = useCallback(
+ ({ rowIndex, colIndex }) => {
+ checkCellExists({ rowIndex, colIndex });
+ const visibleRowIndex = findVisibleRowIndex(rowIndex);
+ _setFocusedCell([colIndex, visibleRowIndex]); // Transmog args from obj to array
+ },
+ [_setFocusedCell, checkCellExists, findVisibleRowIndex]
+ );
+
+ // Popover APIs
+ const {
+ openCellPopover: _openCellPopover,
+ closeCellPopover,
+ } = cellPopoverContext;
+
+ // When we pass this API to the consumer, we can't know for sure that
+ // the targeted cell is valid or in view (unlike our internal state, where
+ // both of those states can be guaranteed), so we need to do some extra
+ // checks here to make sure the grid automatically handles all cells
+ const openCellPopover = useCallback(
+ ({ rowIndex, colIndex }) => {
+ checkCellExists({ rowIndex, colIndex });
+ const visibleRowIndex = findVisibleRowIndex(rowIndex);
+ _openCellPopover({ rowIndex: visibleRowIndex, colIndex });
+ },
+ [_openCellPopover, checkCellExists, findVisibleRowIndex]
+ );
+
+ // Set the ref APIs
+ useImperativeHandle(
+ ref,
+ () => ({
+ setIsFullScreen,
+ setFocusedCell,
+ openCellPopover,
+ closeCellPopover,
+ }),
+ [setIsFullScreen, setFocusedCell, openCellPopover, closeCellPopover]
+ );
+};
+
+/**
+ * Throw a digestible error if the consumer attempts to focus into an invalid
+ * cell range, which should also stop the APIs from continuing
+ */
+export const useCellLocationCheck = (rowCount: number, colCount: number) => {
+ const checkCellExists = useCallback(
+ ({ rowIndex, colIndex }) => {
+ if (rowIndex >= rowCount || rowIndex < 0) {
+ throw new Error(
+ `Row ${rowIndex} is not a valid row. The maximum visible row index is ${
+ rowCount - 1
+ }.`
+ );
+ }
+ if (colIndex >= colCount) {
+ throw new Error(
+ `Column ${colIndex} is not a valid column. The maximum visible column index is ${
+ colCount - 1
+ }.`
+ );
+ }
+ },
+ [rowCount, colCount]
+ );
+
+ return { checkCellExists };
+};
+
+/**
+ * The rowIndex passed from the consumer is the unsorted and unpaginated
+ * index derived from their original data. We need to convert that rowIndex
+ * into a visibleRowIndex (which is what our internal cell APIs use) and, if
+ * the row is not on the current page, the grid should automatically handle
+ * paginating to that row.
+ */
+export const useSortPageCheck = (
+ pagination: EuiDataGridProps['pagination'],
+ sortedRowMap: DataGridSortingContextShape['sortedRowMap']
+) => {
+ const findVisibleRowIndex = useCallback(
+ (rowIndex: number): number => {
+ // Account for sorting
+ const visibleRowIndex = sortedRowMap.length
+ ? sortedRowMap.findIndex((mappedIndex) => mappedIndex === rowIndex)
+ : rowIndex;
+
+ // Account for pagination
+ if (pagination) {
+ const pageIndex = Math.floor(visibleRowIndex / pagination.pageSize);
+ // If the targeted row is on a different page than the current page,
+ // we should automatically navigate the user to the correct page
+ if (pageIndex !== pagination.pageIndex) {
+ pagination.onChangePage(pageIndex);
+ }
+ // Get the row's visible row index on that page
+ return visibleRowIndex % pagination.pageSize;
+ }
+ return visibleRowIndex;
+ },
+ [pagination, sortedRowMap]
+ );
+
+ return { findVisibleRowIndex };
+};
diff --git a/src/components/datagrid/utils/scrolling.ts b/src/components/datagrid/utils/scrolling.ts
index 4aaf3df4ee3..486cecfc983 100644
--- a/src/components/datagrid/utils/scrolling.ts
+++ b/src/components/datagrid/utils/scrolling.ts
@@ -74,6 +74,9 @@ export const useScrollCellIntoView = ({
hasStickyFooter,
}: Dependencies) => {
const scrollCellIntoView = useCallback(
+ // Note: in order for this UX to work correctly with react-window's APIs,
+ // the `rowIndex` arg expected is actually our internal `visibleRowIndex`,
+ // not the `rowIndex` from the raw unsorted/unpaginated user data
async ({ rowIndex, colIndex }: ScrollCellIntoView) => {
if (!gridRef.current || !outerGridRef.current || !innerGridRef.current) {
return; // Grid isn't rendered yet or is empty
diff --git a/src/components/datagrid/utils/sorting.ts b/src/components/datagrid/utils/sorting.ts
index 9a4f17e6eed..59a9d11fce7 100644
--- a/src/components/datagrid/utils/sorting.ts
+++ b/src/components/datagrid/utils/sorting.ts
@@ -21,7 +21,7 @@ export const DataGridSortingContext = createContext<
DataGridSortingContextShape
>({
sorting: undefined,
- sortedRowMap: {},
+ sortedRowMap: [],
getCorrectRowIndex: (number) => number,
});
@@ -43,7 +43,7 @@ export const useSorting = ({
const sortingColumns = sorting?.columns;
const sortedRowMap = useMemo(() => {
- const rowMap: DataGridSortingContextShape['sortedRowMap'] = {};
+ const rowMap: DataGridSortingContextShape['sortedRowMap'] = [];
if (
inMemory?.level === 'sorting' &&
@@ -103,19 +103,21 @@ export const useSorting = ({
schemaDetectors,
]);
+ // Given a visible row index, obtain the unpaginated & unsorted
+ // row index from the passed cell data
const getCorrectRowIndex = useCallback(
- (rowIndex: number) => {
- let rowIndexWithOffset = rowIndex;
-
- if (rowIndex - startRow < 0) {
- rowIndexWithOffset = rowIndex + startRow;
- }
-
- const correctRowIndex = sortedRowMap.hasOwnProperty(rowIndexWithOffset)
- ? sortedRowMap[rowIndexWithOffset]
- : rowIndexWithOffset;
-
- return correctRowIndex;
+ (visibleRowIndex: number) => {
+ const isPaginated = visibleRowIndex - startRow < 0;
+ const unpaginatedRowIndex = isPaginated
+ ? visibleRowIndex + startRow
+ : visibleRowIndex;
+
+ const unsortedRowIndex =
+ unpaginatedRowIndex in sortedRowMap
+ ? sortedRowMap[unpaginatedRowIndex]
+ : unpaginatedRowIndex;
+
+ return unsortedRowIndex;
},
[startRow, sortedRowMap]
);