diff --git a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap index efec1e2bc6e..083bfe42557 100644 --- a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap +++ b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap @@ -1141,7 +1141,7 @@ Array [ data-gridcell-visible-row-index="0" data-test-subj="dataGridRowCell" role="gridcell" - style="position:absolute;left:0;top:0px;height:34px;width:100px" + style="position:absolute;left:0;top:0;height:34px;width:100px" tabindex="-1" >
{ const component = shallow(); expect(component.find('EuiDataGridCell').exists()).toBe(true); }); - - describe('stripes', () => { - it('renders odd rows with .euiDataGridRowCell--stripe', () => { - const component = shallow(); - expect(component.hasClass('euiDataGridRowCell--stripe')).toBe(true); - - component.setProps({ rowIndex: 4 }); - expect(component.hasClass('euiDataGridRowCell--stripe')).toBe(false); - }); - }); }); diff --git a/src/components/datagrid/body/data_grid_body.tsx b/src/components/datagrid/body/data_grid_body.tsx index 7acd43a0f1b..abf00427c89 100644 --- a/src/components/datagrid/body/data_grid_body.tsx +++ b/src/components/datagrid/body/data_grid_body.tsx @@ -79,7 +79,6 @@ export const Cell: FunctionComponent = ({ const isFirstColumn = columnIndex === 0; const isLastColumn = columnIndex === visibleColCount - 1; - const isStripableRow = visibleRowIndex % 2 !== 0; const isLeadingControlColumn = columnIndex < leadingControlColumns.length; const isTrailingControlColumn = @@ -99,7 +98,6 @@ export const Cell: FunctionComponent = ({ const textTransform = transformClass?.textTransform; const classes = classNames({ - 'euiDataGridRowCell--stripe': isStripableRow, 'euiDataGridRowCell--firstColumn': isFirstColumn, 'euiDataGridRowCell--lastColumn': isLastColumn, 'euiDataGridRowCell--controlColumn': diff --git a/src/components/datagrid/body/data_grid_cell.tsx b/src/components/datagrid/body/data_grid_cell.tsx index eff79dc47f6..1337ef0a773 100644 --- a/src/components/datagrid/body/data_grid_cell.tsx +++ b/src/components/datagrid/body/data_grid_cell.tsx @@ -522,6 +522,7 @@ export class EuiDataGridCell extends Component< cellProps.style = { ...style, // from react-window + top: 0, // The cell's row will handle top positioning width, // column width, can be undefined lineHeight: rowHeightsOptions?.lineHeight ?? undefined, // lineHeight configuration ...cellProps.style, // apply anything from setCellProps({style}) @@ -690,7 +691,15 @@ export class EuiDataGridCell extends Component< ); return rowManager && !IS_JEST_ENVIRONMENT - ? createPortal(content, rowManager.getRow(rowIndex)) + ? createPortal( + content, + rowManager.getRow({ + rowIndex, + visibleRowIndex, + top: style!.top as string, // comes in as a `{float}px` string from react-window + height: style!.height as number, // comes in as an integer from react-window + }) + ) : content; } } diff --git a/src/components/datagrid/body/data_grid_footer_row.test.tsx b/src/components/datagrid/body/data_grid_footer_row.test.tsx index d99e6288bba..345a775ccb0 100644 --- a/src/components/datagrid/body/data_grid_footer_row.test.tsx +++ b/src/components/datagrid/body/data_grid_footer_row.test.tsx @@ -203,4 +203,11 @@ describe('EuiDataGridFooterRow', () => { .prop('renderCellValue'); expect(renderCellValue()).toEqual(null); }); + + it('renders striped styling if the footer row is odd', () => { + const component = shallow( + + ); + expect(component.hasClass('euiDataGridRow--striped')).toBe(true); + }); }); diff --git a/src/components/datagrid/body/data_grid_footer_row.tsx b/src/components/datagrid/body/data_grid_footer_row.tsx index 9e32656f689..ecfa63563a2 100644 --- a/src/components/datagrid/body/data_grid_footer_row.tsx +++ b/src/components/datagrid/body/data_grid_footer_row.tsx @@ -36,6 +36,7 @@ const EuiDataGridFooterRow = memo( ) => { const classes = classnames( 'euiDataGridRow', + { 'euiDataGridRow--striped': visibleRowIndex % 2 !== 0 }, 'euiDataGridFooter', className ); diff --git a/src/components/datagrid/body/data_grid_row_manager.test.ts b/src/components/datagrid/body/data_grid_row_manager.test.ts new file mode 100644 index 00000000000..9ddef3ed2ae --- /dev/null +++ b/src/components/datagrid/body/data_grid_row_manager.test.ts @@ -0,0 +1,106 @@ +/* + * 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 { makeRowManager } from './data_grid_row_manager'; + +describe('row manager', () => { + const mockContainerRef = { current: document.createElement('div') } as any; + const rowManager = makeRowManager(mockContainerRef); + + beforeEach(() => jest.clearAllMocks()); + + describe('getRow', () => { + describe('when the row DOM element does not already exist', () => { + beforeAll(() => { + expect(mockContainerRef.current.children).toHaveLength(0); + }); + + it('creates a row DOM element', () => { + const row = rowManager.getRow({ + rowIndex: 0, + visibleRowIndex: 0, + top: '15px', + height: 30, + }); + expect(row).toMatchInlineSnapshot(` +
+ `); + }); + + it('adds a striped class if the visible row index is odd', () => { + const row = rowManager.getRow({ + rowIndex: 1, + visibleRowIndex: 1, + top: '15px', + height: 30, + }); + expect(row).toMatchInlineSnapshot(` +
+ `); + mockContainerRef.current.removeChild(row); + }); + + it('sets the parent innerGrid container to position relative', () => { + expect(mockContainerRef.current.style.position).toEqual('relative'); + }); + + it('appends the row DOM element to the grid body container', () => { + expect(mockContainerRef.current.children).toHaveLength(1); + }); + }); + + describe('when the row DOM element already exists', () => { + it('does not create a new DOM element but fetches the existing one', () => { + rowManager.getRow({ + rowIndex: 0, + visibleRowIndex: 0, + top: '15px', + height: 30, + }); + expect(mockContainerRef.current.children).toHaveLength(1); + }); + + it("updates the row's top and height values", () => { + const row = rowManager.getRow({ + rowIndex: 0, + visibleRowIndex: 0, + top: '100px', + height: 200, + }); + expect(row).toMatchInlineSnapshot(` +
+ `); + }); + }); + + describe("when the row's child cells are all removed", () => { + it('deletes the row element node and its mapping', () => { + // TODO: Write a Cypress test for this + // or upgrade Jest/jsdom to v14+ which supports Mutation Observers + }); + }); + }); +}); diff --git a/src/components/datagrid/body/data_grid_row_manager.ts b/src/components/datagrid/body/data_grid_row_manager.ts index cf6351278cc..007c0622c60 100644 --- a/src/components/datagrid/body/data_grid_row_manager.ts +++ b/src/components/datagrid/body/data_grid_row_manager.ts @@ -15,17 +15,30 @@ export const makeRowManager = ( const rowIdToElements = new Map(); return { - getRow(rowIndex) { + getRow({ rowIndex, visibleRowIndex, top, height }) { let rowElement = rowIdToElements.get(rowIndex); if (rowElement == null) { rowElement = document.createElement('div'); rowElement.setAttribute('role', 'row'); + rowElement.dataset.gridRowIndex = String(rowIndex); // Row index from data, affected by sorting/pagination + rowElement.dataset.gridVisibleRowIndex = String(visibleRowIndex); // Affected by sorting/pagination rowElement.classList.add('euiDataGridRow'); - rowIdToElements.set(rowIndex, rowElement); + const isOddRow = visibleRowIndex % 2 !== 0; + if (isOddRow) rowElement.classList.add('euiDataGridRow--striped'); + rowElement.style.position = 'absolute'; + rowElement.style.left = '0'; + rowElement.style.right = '0'; + + // In order for the rowElement's left and right position to correctly inherit + // from the innerGrid width, we need to make its position relative + containerRef.current!.style.position = 'relative'; // add the element to the wrapping container - containerRef.current?.appendChild(rowElement); + containerRef.current!.appendChild(rowElement); + + // add the element to the row map + rowIdToElements.set(rowIndex, rowElement); // watch the row's children, if they all disappear then remove this row const observer = new MutationObserver((records) => { @@ -38,6 +51,10 @@ export const makeRowManager = ( observer.observe(rowElement, { childList: true }); } + // Ensure that the row's dimensions are always correct by having each cell update position styles + rowElement.style.top = top; + rowElement.style.height = `${height}px`; + return rowElement; }, }; diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index 42b3568df26..fbb30353353 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -557,7 +557,7 @@ describe('EuiDataGrid', () => { "lineHeight": undefined, "position": "absolute", "right": undefined, - "top": "100px", + "top": 0, "width": 100, }, "tabIndex": -1, @@ -583,13 +583,13 @@ describe('EuiDataGrid', () => { "lineHeight": undefined, "position": "absolute", "right": undefined, - "top": "100px", + "top": 0, "width": 100, }, "tabIndex": -1, }, Object { - "className": "euiDataGridRowCell euiDataGridRowCell--stripe euiDataGridRowCell--firstColumn customClass", + "className": "euiDataGridRowCell euiDataGridRowCell--firstColumn customClass", "data-gridcell-column-id": "A", "data-gridcell-column-index": 0, "data-gridcell-id": "0,1", @@ -609,13 +609,13 @@ describe('EuiDataGrid', () => { "lineHeight": undefined, "position": "absolute", "right": undefined, - "top": "134px", + "top": 0, "width": 100, }, "tabIndex": -1, }, Object { - "className": "euiDataGridRowCell euiDataGridRowCell--stripe euiDataGridRowCell--lastColumn customClass", + "className": "euiDataGridRowCell euiDataGridRowCell--lastColumn customClass", "data-gridcell-column-id": "B", "data-gridcell-column-index": 1, "data-gridcell-id": "1,1", @@ -635,7 +635,7 @@ describe('EuiDataGrid', () => { "lineHeight": undefined, "position": "absolute", "right": undefined, - "top": "134px", + "top": 0, "width": 100, }, "tabIndex": -1, @@ -836,10 +836,10 @@ describe('EuiDataGrid', () => { "euiDataGridRowCell euiDataGridRowCell--numeric euiDataGridRowCell--firstColumn", "euiDataGridRowCell--lastColumn", "euiDataGridRowCell euiDataGridRowCell--customFormatName euiDataGridRowCell--lastColumn", - "euiDataGridRowCell--stripe euiDataGridRowCell--firstColumn", - "euiDataGridRowCell euiDataGridRowCell--numeric euiDataGridRowCell--stripe euiDataGridRowCell--firstColumn", - "euiDataGridRowCell--stripe euiDataGridRowCell--lastColumn", - "euiDataGridRowCell euiDataGridRowCell--customFormatName euiDataGridRowCell--stripe euiDataGridRowCell--lastColumn", + "euiDataGridRowCell--firstColumn", + "euiDataGridRowCell euiDataGridRowCell--numeric euiDataGridRowCell--firstColumn", + "euiDataGridRowCell--lastColumn", + "euiDataGridRowCell euiDataGridRowCell--customFormatName euiDataGridRowCell--lastColumn", "euiDataGridRowCell--firstColumn", "euiDataGridRowCell euiDataGridRowCell--numeric euiDataGridRowCell--firstColumn", "euiDataGridRowCell--lastColumn", @@ -879,9 +879,9 @@ describe('EuiDataGrid', () => { "euiDataGridRowCell euiDataGridRowCell--numeric euiDataGridRowCell--firstColumn", "euiDataGridRowCell euiDataGridRowCell--boolean", "euiDataGridRowCell euiDataGridRowCell--lastColumn", - "euiDataGridRowCell euiDataGridRowCell--numeric euiDataGridRowCell--stripe euiDataGridRowCell--firstColumn", - "euiDataGridRowCell euiDataGridRowCell--boolean euiDataGridRowCell--stripe", - "euiDataGridRowCell euiDataGridRowCell--stripe euiDataGridRowCell--lastColumn", + "euiDataGridRowCell euiDataGridRowCell--numeric euiDataGridRowCell--firstColumn", + "euiDataGridRowCell euiDataGridRowCell--boolean", + "euiDataGridRowCell euiDataGridRowCell--lastColumn", ] `); }); @@ -910,8 +910,8 @@ describe('EuiDataGrid', () => { Array [ "euiDataGridRowCell euiDataGridRowCell--numeric euiDataGridRowCell--firstColumn", "euiDataGridRowCell euiDataGridRowCell--alphanumeric euiDataGridRowCell--lastColumn", - "euiDataGridRowCell euiDataGridRowCell--numeric euiDataGridRowCell--stripe euiDataGridRowCell--firstColumn", - "euiDataGridRowCell euiDataGridRowCell--alphanumeric euiDataGridRowCell--stripe euiDataGridRowCell--lastColumn", + "euiDataGridRowCell euiDataGridRowCell--numeric euiDataGridRowCell--firstColumn", + "euiDataGridRowCell euiDataGridRowCell--alphanumeric euiDataGridRowCell--lastColumn", ] `); }); diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 617388e57af..2f5084f2fb8 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -832,5 +832,10 @@ export interface EuiDataGridRowHeightsOptions { } export interface EuiDataGridRowManager { - getRow(rowIndex: number): HTMLDivElement; + getRow(args: { + rowIndex: number; + visibleRowIndex: number; + top: string; + height: number; + }): HTMLDivElement; } diff --git a/src/components/datagrid/utils/scrolling.test.tsx b/src/components/datagrid/utils/scrolling.test.tsx index 3f5cdbd3c59..b7711d11942 100644 --- a/src/components/datagrid/utils/scrolling.test.tsx +++ b/src/components/datagrid/utils/scrolling.test.tsx @@ -18,7 +18,7 @@ describe('useScrollCellIntoView', () => { const scrollTo = jest.fn(); const mockCell = { - offsetTop: 30, + parentNode: { offsetTop: 30 }, offsetLeft: 0, offsetWidth: 100, offsetHeight: 20, @@ -91,7 +91,7 @@ describe('useScrollCellIntoView', () => { it("does nothing if the current cell is in view and not outside the grid's scroll bounds", () => { getCell.mockReturnValue({ ...mockCell, - offsetTop: 50, + parentNode: { offsetTop: 50 }, offsetLeft: 50, }); const { @@ -184,7 +184,7 @@ describe('useScrollCellIntoView', () => { describe('bottom scroll adjustments', () => { const cell = { ...mockCell, - offsetTop: 400, + parentNode: { offsetTop: 400 }, offsetHeight: 100, }; const grid = { @@ -256,7 +256,7 @@ describe('useScrollCellIntoView', () => { describe('top scroll adjustments', () => { const cell = { ...mockCell, - offsetTop: 50, + parentNode: { offsetTop: 50 }, offsetHeight: 25, }; const grid = { @@ -295,7 +295,7 @@ describe('useScrollCellIntoView', () => { it('scrolls to the top side over the bottom if the cell height is larger than the grid height', () => { const cell = { ...mockCell, - offsetTop: 100, + parentNode: { offsetTop: 100 }, offsetHeight: 600, }; const grid = { diff --git a/src/components/datagrid/utils/scrolling.tsx b/src/components/datagrid/utils/scrolling.tsx index bb6cfbd35c1..9e796d7384a 100644 --- a/src/components/datagrid/utils/scrolling.tsx +++ b/src/components/datagrid/utils/scrolling.tsx @@ -149,8 +149,10 @@ export const useScrollCellIntoView = ({ const isStickyFooter = hasStickyFooter && rowIndex === visibleRowCount; if (!isStickyHeader && !isStickyFooter) { + const parentRow = cell.parentNode as HTMLDivElement; + // Check if the cell's bottom side is outside the current scrolling bounds - const cellBottomPos = cell.offsetTop + cell.offsetHeight; + const cellBottomPos = parentRow.offsetTop + cell.offsetHeight; let bottomScrollBound = scrollTop + outerGridRef.current.clientHeight; // Note: We specifically want clientHeight and not offsetHeight here to account for scrollbars if (hasStickyFooter) bottomScrollBound -= footerRowHeight; // Sticky footer is not always present const bottomHeightOutOfView = cellBottomPos - bottomScrollBound; @@ -159,7 +161,7 @@ export const useScrollCellIntoView = ({ } // Check if the cell's top side is outside the current scrolling bounds - const cellTopPos = cell.offsetTop; + const cellTopPos = parentRow.offsetTop; const topScrollBound = adjustedScrollTop ?? scrollTop + headerRowHeight; // Sticky header is always present const topHeightOutOfView = topScrollBound - cellTopPos; if (topHeightOutOfView > 0) {