diff --git a/packages/eui/changelogs/upcoming/8028.md b/packages/eui/changelogs/upcoming/8028.md new file mode 100644 index 000000000000..0affe589d581 --- /dev/null +++ b/packages/eui/changelogs/upcoming/8028.md @@ -0,0 +1,5 @@ +**Breaking changes** + +- EuiDataGrid's custom grid body (rendered via `renderCustomGridBody`) no longer automatically renders the column header row or footer rows. It instead now passes the `headerRow` and `footerRow` React elements, which require manual rendering. + - This change was made to allow consumers to sync header/footer rows with their own custom virtualization libraries. + - To facilitate this, a `gridWidth` prop is now also passed to custom grid body renderers. diff --git a/packages/eui/src-docs/src/views/datagrid/advanced/custom_renderer.tsx b/packages/eui/src-docs/src/views/datagrid/advanced/custom_renderer.tsx deleted file mode 100644 index dc6f6924f076..000000000000 --- a/packages/eui/src-docs/src/views/datagrid/advanced/custom_renderer.tsx +++ /dev/null @@ -1,314 +0,0 @@ -import React, { useEffect, useCallback, useState, useRef } from 'react'; -import { css } from '@emotion/react'; -import { faker } from '@faker-js/faker'; - -import { - EuiDataGrid, - EuiDataGridProps, - EuiDataGridCustomBodyProps, - EuiDataGridColumnCellActionProps, - EuiScreenReaderOnly, - EuiCheckbox, - EuiButtonIcon, - EuiIcon, - EuiFlexGroup, - EuiSwitch, - EuiSpacer, - useEuiTheme, - logicalCSS, - EuiDataGridPaginationProps, - EuiDataGridSorting, - EuiDataGridColumnSortingConfig, - RenderCellValue, -} from '../../../../../src'; - -const raw_data: Array<{ [key: string]: string }> = []; -for (let i = 1; i < 100; i++) { - raw_data.push({ - name: `${faker.person.lastName()}, ${faker.person.firstName()}`, - email: faker.internet.email(), - location: `${faker.location.city()}, ${faker.location.country()}`, - date: `${faker.date.past()}`, - amount: faker.commerce.price({ min: 1, max: 1000, dec: 2, symbol: '$' }), - }); -} - -const columns = [ - { - id: 'name', - displayAsText: 'Name', - cellActions: [ - ({ Component }: EuiDataGridColumnCellActionProps) => ( - alert('action')} - iconType="faceHappy" - aria-label="Some action" - > - Some action - - ), - ], - }, - { - id: 'email', - displayAsText: 'Email address', - initialWidth: 130, - }, - { - id: 'location', - displayAsText: 'Location', - }, - { - id: 'date', - displayAsText: 'Date', - }, - { - id: 'amount', - displayAsText: 'Amount', - }, -]; - -const checkboxRowCellRender: RenderCellValue = ({ rowIndex }) => ( - {}} - /> -); - -const leadingControlColumns: EuiDataGridProps['leadingControlColumns'] = [ - { - id: 'selection', - width: 32, - headerCellRender: () => ( - {}} - /> - ), - rowCellRender: checkboxRowCellRender, - }, -]; - -const trailingControlColumns: EuiDataGridProps['trailingControlColumns'] = [ - { - id: 'actions', - width: 40, - headerCellRender: () => ( - - Actions - - ), - rowCellRender: () => ( - - ), - }, -]; - -const RowCellRender: RenderCellValue = ({ setCellProps, rowIndex }) => { - setCellProps({ style: { width: '100%', height: 'auto' } }); - - const firstName = raw_data[rowIndex].name.split(', ')[1]; - const isGood = faker.datatype.boolean(); - return ( - <> - {firstName}'s account has {isGood ? 'no' : ''} outstanding fees.{' '} - - - ); -}; - -// The custom row details is actually a trailing control column cell with -// a hidden header. This is important for accessibility and markup reasons -// @see https://fuschia-stretch.glitch.me/ for more -const rowDetails: EuiDataGridProps['trailingControlColumns'] = [ - { - id: 'row-details', - - // The header cell should be visually hidden, but available to screen readers - width: 0, - headerCellRender: () => <>Row details, - headerCellProps: { className: 'euiScreenReaderOnly' }, - - // The footer cell can be hidden to both visual & SR users, as it does not contain meaningful information - footerCellProps: { style: { display: 'none' } }, - - // When rendering this custom cell, we'll want to override - // the automatic width/heights calculated by EuiDataGrid - rowCellRender: RowCellRender, - }, -]; - -const footerCellValues: { [key: string]: string } = { - amount: `Total: ${raw_data - .reduce((acc, { amount }) => acc + Number(amount.split('$')[1]), 0) - .toLocaleString('en-US', { style: 'currency', currency: 'USD' })}`, -}; - -const renderCellValue: RenderCellValue = ({ rowIndex, columnId }) => - raw_data[rowIndex][columnId]; - -const RenderFooterCellValue: RenderCellValue = ({ columnId, setCellProps }) => { - const value = footerCellValues[columnId]; - - useEffect(() => { - // Turn off the cell expansion button if the footer cell is empty - if (!value) setCellProps({ isExpandable: false }); - }, [value, setCellProps, columnId]); - - return value || null; -}; - -export default () => { - const [autoHeight, setAutoHeight] = useState(true); - const [showRowDetails, setShowRowDetails] = useState(false); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState(() => - columns.map(({ id }) => id) - ); - - // Pagination - const [pagination, setPagination] = useState({ pageIndex: 0 }); - const onChangePage = useCallback( - (pageIndex) => { - setPagination((pagination) => ({ ...pagination, pageIndex })); - }, - [] - ); - const onChangePageSize = useCallback< - EuiDataGridPaginationProps['onChangeItemsPerPage'] - >((pageSize) => { - setPagination((pagination) => ({ ...pagination, pageSize })); - }, []); - - // Sorting - const [sortingColumns, setSortingColumns] = useState< - EuiDataGridColumnSortingConfig[] - >([]); - const onSort = useCallback((sortingColumns) => { - setSortingColumns(sortingColumns); - }, []); - - const { euiTheme } = useEuiTheme(); - - // Custom grid body renderer - const RenderCustomGridBody = useCallback( - ({ - Cell, - visibleColumns, - visibleRowData, - setCustomGridBodyProps, - }: EuiDataGridCustomBodyProps) => { - // Ensure we're displaying correctly-paginated rows - const visibleRows = raw_data.slice( - visibleRowData.startRow, - visibleRowData.endRow - ); - - // Add styling needed for custom grid body rows - const styles = { - row: css` - ${logicalCSS('width', 'fit-content')}; - ${logicalCSS('border-bottom', euiTheme.border.thin)}; - background-color: ${euiTheme.colors.emptyShade}; - `, - rowCellsWrapper: css` - display: flex; - `, - rowDetailsWrapper: css` - text-align: center; - background-color: ${euiTheme.colors.body}; - `, - }; - - // Set custom props onto the grid body wrapper - const bodyRef = useRef(null); - useEffect(() => { - setCustomGridBodyProps({ - ref: bodyRef, - onScroll: () => - console.debug('scrollTop:', bodyRef.current?.scrollTop), - }); - }, [setCustomGridBodyProps]); - - return ( - <> - {visibleRows.map((row, rowIndex) => ( -
-
- {visibleColumns.map((column, colIndex) => { - // Skip the row details cell - we'll render it manually outside of the flex wrapper - if (column.id !== 'row-details') { - return ( - - ); - } - })} -
- {showRowDetails && ( -
- -
- )} -
- ))} - - ); - }, - [showRowDetails, euiTheme] - ); - - return ( - <> - - setAutoHeight(!autoHeight)} - /> - setShowRowDetails(!showRowDetails)} - /> - - - - - ); -}; diff --git a/packages/eui/src-docs/src/views/datagrid/advanced/datagrid_advanced_example.js b/packages/eui/src-docs/src/views/datagrid/advanced/datagrid_advanced_example.js index ce49e6dda5a5..41c6868c5850 100644 --- a/packages/eui/src-docs/src/views/datagrid/advanced/datagrid_advanced_example.js +++ b/packages/eui/src-docs/src/views/datagrid/advanced/datagrid_advanced_example.js @@ -1,5 +1,4 @@ import React from 'react'; -import { Link } from 'react-router-dom'; import { GuideSectionTypes } from '../../../components'; import { @@ -10,12 +9,10 @@ import { EuiLink, } from '../../../../../src/components'; -import { - EuiDataGridRefProps, - EuiDataGridCustomBodyProps, -} from '!!prop-loader!../../../../../src/components/datagrid/data_grid_types'; +import { EuiDataGridRefProps } from '!!prop-loader!../../../../../src/components/datagrid/data_grid_types'; import { DataGridMemoryExample } from './datagrid_memory_example'; +import { DataGridCustomBodyExample } from './render_custom_grid_body/example'; import DataGridRef from './ref'; const dataGridRefSource = require('!!raw-loader!./ref'); @@ -35,28 +32,6 @@ dataGridRef.current.openCellPopover({ rowIndex, colIndex }); dataGridRef.current.closeCellPopover(); `; -import CustomRenderer from './custom_renderer'; -const customRendererSource = require('!!raw-loader!./custom_renderer'); -const customRendererSnippet = `const CustomGridBody = ({ visibleColumns, visibleRowData, Cell }) => { - const visibleRows = raw_data.slice( - visibleRowData.startRow, - visibleRowData.endRow - ); - return ( - <> - {visibleRows.map((row, rowIndex) => ( -
- {visibleColumns.map((column, colIndex) => ( - - ))} -
- ))} - - ); -}; - -`; - export const DataGridAdvancedExample = { title: 'Data grid advanced', sections: [ @@ -211,47 +186,6 @@ export const DataGridAdvancedExample = { props: { EuiDataGridRefProps }, }, ...DataGridMemoryExample.sections, - { - title: 'Custom body renderer', - source: [ - { - type: GuideSectionTypes.TSX, - code: customRendererSource, - }, - ], - text: ( - <> -

- For extremely advanced use cases, the{' '} - renderCustomGridBody prop may be used to take - complete control over rendering the grid body. This may be useful - for scenarios where the default{' '} - - virtualized - {' '} - rendering is not desired, or where custom row layouts (e.g., the - conditional row details cell below) are required. -

-

- Please note that this prop is meant to be an{' '} - escape hatch, and should only be used if you know - exactly what you are doing. Once a custom renderer is used, you are - in charge of ensuring the grid has all the correct semantic and aria - labels required by the{' '} - - data grid spec - - , and that keyboard focus and navigation still work in an accessible - manner. -

- - ), - demo: , - snippet: customRendererSnippet, - props: { EuiDataGridCustomBodyProps }, - }, + DataGridCustomBodyExample, ], }; diff --git a/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/data_columns_cells.tsx b/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/data_columns_cells.tsx new file mode 100644 index 000000000000..f0086d3116f7 --- /dev/null +++ b/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/data_columns_cells.tsx @@ -0,0 +1,185 @@ +import React, { useEffect, memo } from 'react'; +import { faker } from '@faker-js/faker'; + +import { + EuiDataGridProps, + EuiDataGridColumnCellActionProps, + EuiScreenReaderOnly, + EuiCheckbox, + EuiCallOut, + EuiButton, + EuiButtonIcon, +} from '../../../../../../src'; + +/** + * Mock data + */ +export const raw_data: Array<{ [key: string]: string }> = []; +for (let i = 1; i < 100; i++) { + raw_data.push({ + name: `${faker.person.lastName()}, ${faker.person.firstName()}`, + email: faker.internet.email(), + location: `${faker.location.city()}, ${faker.location.country()}`, + date: `${faker.date.past()}`, + amount: faker.commerce.price({ min: 1, max: 1000, dec: 2, symbol: '$' }), + feesOwed: faker.datatype.boolean() ? 'true' : '', + }); +} +const footer_data: { [key: string]: string } = { + amount: `Total: ${raw_data + .reduce((acc, { amount }) => acc + Number(amount.split('$')[1]), 0) + .toLocaleString('en-US', { style: 'currency', currency: 'USD' })}`, +}; + +/** + * Columns + */ +export const columns = [ + { + id: 'name', + displayAsText: 'Name', + cellActions: [ + ({ Component }: EuiDataGridColumnCellActionProps) => ( + alert('action')} + iconType="faceHappy" + aria-label="Some action" + > + Some action + + ), + ], + }, + { + id: 'email', + displayAsText: 'Email address', + initialWidth: 130, + }, + { + id: 'location', + displayAsText: 'Location', + }, + { + id: 'date', + displayAsText: 'Date', + }, + { + id: 'amount', + displayAsText: 'Amount', + }, +]; + +/** + * Cell component + */ +export const RenderCellValue: EuiDataGridProps['renderCellValue'] = ({ + rowIndex, + columnId, +}) => raw_data[rowIndex][columnId]; + +/** + * Row details component + */ +// eslint-disable-next-line local/forward-ref +const RenderRowDetails: EuiDataGridProps['renderCellValue'] = memo( + ({ setCellProps, rowIndex }) => { + setCellProps({ style: { width: '100%', height: 'auto' } }); + + // Mock data + const firstName = raw_data[rowIndex].name.split(', ')[1]; + const hasFees = !!raw_data[rowIndex].feesOwed; + + return ( + + {hasFees && ( + + Send an email reminder + + )} + + ); + } +); + +/** + * Control columns + */ +export const leadingControlColumns: EuiDataGridProps['leadingControlColumns'] = + [ + { + id: 'selection', + width: 32, + headerCellRender: () => ( + {}} + /> + ), + rowCellRender: ({ rowIndex }) => ( + {}} + /> + ), + }, + ]; +export const trailingControlColumns: EuiDataGridProps['trailingControlColumns'] = + [ + { + id: 'actions', + width: 40, + headerCellRender: () => ( + + Actions + + ), + rowCellRender: () => ( + + ), + }, + // The custom row details is actually a trailing control column cell with + // a hidden header. This is important for accessibility and markup reasons + // @see https://fuschia-stretch.glitch.me/ for more + { + id: 'row-details', + + // The header cell should be visually hidden, but available to screen readers + width: 0, + headerCellRender: () => <>Row details, + headerCellProps: { className: 'euiScreenReaderOnly' }, + + // The footer cell can be hidden to both visual & SR users, as it does not contain meaningful information + footerCellProps: { style: { display: 'none' } }, + + // When rendering this custom cell, we'll want to override + // the automatic width/heights calculated by EuiDataGrid + rowCellRender: RenderRowDetails, + }, + ]; + +/** + * Footer cell component + */ +export const RenderFooterCellValue: EuiDataGridProps['renderFooterCellValue'] = + ({ columnId, setCellProps }) => { + const value = footer_data[columnId]; + + useEffect(() => { + // Turn off the cell expansion button if the footer cell is empty + if (!value) setCellProps({ isExpandable: false }); + }, [value, setCellProps, columnId]); + + return value || null; + }; diff --git a/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/data_grid.tsx b/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/data_grid.tsx new file mode 100644 index 000000000000..008d1abd4ab3 --- /dev/null +++ b/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/data_grid.tsx @@ -0,0 +1,69 @@ +import React, { useCallback, useState } from 'react'; + +import { + EuiDataGrid, + EuiDataGridProps, + EuiDataGridPaginationProps, + EuiDataGridSorting, + EuiDataGridColumnSortingConfig, +} from '../../../../../../src'; + +import { + raw_data, + columns, + leadingControlColumns, + trailingControlColumns, + RenderCellValue, + RenderFooterCellValue, +} from './data_columns_cells'; + +export default (props: Partial>) => { + // Column visibility + const [visibleColumns, setVisibleColumns] = useState(() => + columns.map(({ id }) => id) + ); + + // Pagination + const [pagination, setPagination] = useState({ pageIndex: 0 }); + const onChangePage = useCallback( + (pageIndex) => { + setPagination((pagination) => ({ ...pagination, pageIndex })); + }, + [] + ); + const onChangePageSize = useCallback< + EuiDataGridPaginationProps['onChangeItemsPerPage'] + >((pageSize) => { + setPagination((pagination) => ({ ...pagination, pageSize })); + }, []); + + // Sorting + const [sortingColumns, setSortingColumns] = useState< + EuiDataGridColumnSortingConfig[] + >([]); + const onSort = useCallback((sortingColumns) => { + setSortingColumns(sortingColumns); + }, []); + + return ( + + ); +}; diff --git a/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/example.js b/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/example.js new file mode 100644 index 000000000000..3edb747ebec6 --- /dev/null +++ b/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/example.js @@ -0,0 +1,123 @@ +import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { + EuiSplitPanel, + EuiFlexGroup, + EuiCode, + EuiSwitch, + EuiSpacer, + EuiLink, +} from '../../../../../../src/components'; + +import { GuideSectionTypes } from '../../../../components'; +import { GuideSectionCodeTypesMap } from '../../../../components/guide_section/guide_section'; +import { GuideSectionExampleTabs } from '../../../../components/guide_section/guide_section_parts/guide_section_tabs'; + +import { EuiDataGridCustomBodyProps } from '!!prop-loader!../../../../../../src/components/datagrid/data_grid_types'; + +import VirtualizedBody from './virtualized_body'; +const virtualizedSource = require('!!raw-loader!./virtualized_body'); + +import UnvirtualizedBody from './unvirtualized_body'; +const unvirtualizedSource = require('!!raw-loader!./unvirtualized_body'); + +const dataGridSource = require('!!raw-loader!./data_grid'); +const dataSource = require('!!raw-loader!./data_columns_cells'); + +export const ConditionalDemo = () => { + const [virtualized, setVirtualized] = useState(false); + const [autoHeight, setAutoHeight] = useState(true); + + return ( + + + + setVirtualized(!virtualized)} + /> + setAutoHeight(!autoHeight)} + /> + + + + {virtualized ? ( + + ) : ( + + )} + + + + + + ); +}; + +export const DataGridCustomBodyExample = { + title: 'Custom body renderer', + text: ( + <> +

+ For extremely advanced use cases, the{' '} + renderCustomGridBody prop may be used to take + complete control over rendering the grid body. This may be useful for + scenarios where the default{' '} + virtualized{' '} + rendering is not desired, or where custom row layouts (e.g., the + full-width row details cell below) are required. +

+

+ Please note that this prop is meant to be an{' '} + escape hatch, and should only be used if you know + exactly what you are doing. Once a custom renderer is used, you are in + charge of ensuring the grid has all the correct semantic and aria labels + required by the{' '} + + data grid spec + + , and that keyboard focus and navigation still work in an accessible + manner. +

+ + ), + children: ( + <> + + + + ), +}; diff --git a/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/unvirtualized_body.tsx b/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/unvirtualized_body.tsx new file mode 100644 index 000000000000..a84a2fb85a3e --- /dev/null +++ b/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/unvirtualized_body.tsx @@ -0,0 +1,96 @@ +import React, { useEffect, useRef, memo } from 'react'; +import { css } from '@emotion/react'; + +import { + EuiDataGridCustomBodyProps, + useEuiTheme, + logicalCSS, +} from '../../../../../../src'; + +import { raw_data } from './data_columns_cells'; +import CustomEuiDataGrid from './data_grid'; + +export const CustomUnvirtualizedGridBody = memo( + ({ + Cell, + visibleColumns, + visibleRowData, + setCustomGridBodyProps, + headerRow, + footerRow, + }: EuiDataGridCustomBodyProps) => { + // Ensure we're displaying correctly-paginated rows + const visibleRows = raw_data.slice( + visibleRowData.startRow, + visibleRowData.endRow + ); + + // Add styling needed for custom grid body rows + const { euiTheme } = useEuiTheme(); + const styles = { + row: css` + ${logicalCSS('width', 'fit-content')}; + ${logicalCSS('border-bottom', euiTheme.border.thin)}; + background-color: ${euiTheme.colors.emptyShade}; + `, + rowCellsWrapper: css` + display: flex; + `, + rowDetailsWrapper: css` + /* Extra specificity needed to override EuiDataGrid's default styles */ + && .euiDataGridRowCell__content { + display: block; + padding: 0; + } + `, + }; + + // Set custom props onto the grid body wrapper + const bodyRef = useRef(null); + useEffect(() => { + setCustomGridBodyProps({ + ref: bodyRef, + onScroll: () => console.debug('scrollTop:', bodyRef.current?.scrollTop), + }); + }, [setCustomGridBodyProps]); + + return ( + <> + {headerRow} + {visibleRows.map((row, rowIndex) => ( +
+
+ {visibleColumns.map((column, colIndex) => { + // Skip the row details cell - we'll render it manually outside of the flex wrapper + if (column.id !== 'row-details') { + return ( + + ); + } + })} +
+
+ +
+
+ ))} + {footerRow} + + ); + } +); +CustomUnvirtualizedGridBody.displayName = 'CustomUnvirtualizedGridBody'; + +export default ({ autoHeight }: { autoHeight: boolean }) => ( + +); diff --git a/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/virtualized_body.tsx b/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/virtualized_body.tsx new file mode 100644 index 000000000000..f53ad032ccda --- /dev/null +++ b/packages/eui/src-docs/src/views/datagrid/advanced/render_custom_grid_body/virtualized_body.tsx @@ -0,0 +1,216 @@ +import React, { + useEffect, + useCallback, + useRef, + useMemo, + memo, + forwardRef, + PropsWithChildren, + CSSProperties, +} from 'react'; +import { VariableSizeList } from 'react-window'; +import { css } from '@emotion/react'; + +import { + EuiDataGridCustomBodyProps, + EuiAutoSizer, + useEuiTheme, + logicalCSS, +} from '../../../../../../src'; + +import { raw_data } from './data_columns_cells'; +import CustomEuiDataGrid from './data_grid'; + +type CustomTimelineDataGridSingleRowProps = { + rowIndex: number; + setRowHeight: (index: number, height: number) => void; + maxWidth: number | undefined; +} & Pick; + +const Row = memo( + ({ + rowIndex, + visibleColumns, + setRowHeight, + Cell, + }: CustomTimelineDataGridSingleRowProps) => { + const { euiTheme } = useEuiTheme(); + const styles = { + row: css` + ${logicalCSS('width', 'fit-content')}; + ${logicalCSS('border-bottom', euiTheme.border.thin)}; + background-color: ${euiTheme.colors.emptyShade}; + `, + rowCellsWrapper: css` + display: flex; + `, + rowDetailsWrapper: css` + /* Extra specificity needed to override EuiDataGrid's default styles */ + && .euiDataGridRowCell__content { + display: block; + padding: 0; + } + `, + }; + const rowRef = useRef(null); + + useEffect(() => { + if (rowRef.current) { + setRowHeight(rowIndex, rowRef.current.offsetHeight); + } + }, [Cell, rowIndex, setRowHeight]); + + return ( +
+
+ {visibleColumns.map((column, colIndex) => { + // Skip the row details cell - we'll render it manually outside of the flex wrapper + if (column.id !== 'row-details') { + return ( + + ); + } + })} +
+
+ +
+
+ ); + } +); +Row.displayName = 'Row'; + +export const CustomVirtualizedGridBody = memo( + ({ + Cell, + visibleColumns, + visibleRowData, + setCustomGridBodyProps, + headerRow, + footerRow, + }: EuiDataGridCustomBodyProps) => { + // Ensure we're displaying correctly-paginated rows + const visibleRows = raw_data.slice( + visibleRowData.startRow, + visibleRowData.endRow + ); + + // Set custom props onto the grid body wrapper + const bodyRef = useRef(null); + useEffect(() => { + setCustomGridBodyProps({ + ref: bodyRef, + onScroll: () => console.debug('scrollTop:', bodyRef.current?.scrollTop), + }); + }, [setCustomGridBodyProps]); + + const listRef = useRef>(null); + const rowHeights = useRef([]); + + const setRowHeight = useCallback((index: number, height: number) => { + if (rowHeights.current[index] === height) return; + listRef.current?.resetAfterIndex(index); + + rowHeights.current[index] = height; + }, []); + + const getRowHeight = useCallback((index: number) => { + return rowHeights.current[index] ?? 100; + }, []); + + const outer = useMemo( + () => + forwardRef>( + ({ children, ...rest }, ref) => { + return ( +
+ {headerRow} + {children} + {footerRow} +
+ ); + } + ), + [headerRow, footerRow] + ); + + const inner = useMemo( + () => + forwardRef>( + ({ children, style, ...rest }, ref) => { + return ( +
+ {children} +
+ ); + } + ), + [] + ); + + return ( + + {({ height }: { height: number }) => { + return ( + `${height}-${visibleRows.length}-${index}`} + outerElementType={outer} + innerElementType={inner} + overscanCount={0} + layout="vertical" + > + {({ index: rowIndex, style }) => { + return ( +
+ +
+ ); + }} +
+ ); + }} +
+ ); + } +); +CustomVirtualizedGridBody.displayName = 'CustomVirtualizedGridBody'; + +export default () => ( + +); diff --git a/packages/eui/src/components/datagrid/body/data_grid_body_custom.spec.tsx b/packages/eui/src/components/datagrid/body/data_grid_body_custom.spec.tsx index bce75af98579..6bfcf859f0b2 100644 --- a/packages/eui/src/components/datagrid/body/data_grid_body_custom.spec.tsx +++ b/packages/eui/src/components/datagrid/body/data_grid_body_custom.spec.tsx @@ -22,6 +22,8 @@ describe('EuiDataGridBodyCustomRender', () => { visibleColumns, visibleRowData, Cell, + headerRow, + footerRow, }) => { const visibleRows = raw_data.slice( visibleRowData.startRow, @@ -29,6 +31,7 @@ describe('EuiDataGridBodyCustomRender', () => { ); return ( <> + {headerRow} {visibleRows.map((row, rowIndex) => (
{visibleColumns.map((column, colIndex) => ( @@ -41,6 +44,7 @@ describe('EuiDataGridBodyCustomRender', () => { ))}
))} + {footerRow} ); }; diff --git a/packages/eui/src/components/datagrid/body/data_grid_body_custom.test.tsx b/packages/eui/src/components/datagrid/body/data_grid_body_custom.test.tsx index 43012117206a..1620054e0274 100644 --- a/packages/eui/src/components/datagrid/body/data_grid_body_custom.test.tsx +++ b/packages/eui/src/components/datagrid/body/data_grid_body_custom.test.tsx @@ -37,6 +37,8 @@ describe('EuiDataGridBodyCustomRender', () => { const CustomGridBody: EuiDataGridProps['renderCustomGridBody'] = ({ visibleColumns, visibleRowData, + headerRow, + footerRow, Cell, }) => { const visibleRows = raw_data.slice( @@ -45,6 +47,7 @@ describe('EuiDataGridBodyCustomRender', () => { ); return ( <> + {headerRow} {visibleRows.map((row, rowIndex) => (
{visibleColumns.map((column, colIndex) => ( @@ -56,6 +59,7 @@ describe('EuiDataGridBodyCustomRender', () => { ))}
))} + {footerRow} ); }; diff --git a/packages/eui/src/components/datagrid/body/data_grid_body_custom.tsx b/packages/eui/src/components/datagrid/body/data_grid_body_custom.tsx index 2cd174c007b9..513623263f56 100644 --- a/packages/eui/src/components/datagrid/body/data_grid_body_custom.tsx +++ b/packages/eui/src/components/datagrid/body/data_grid_body_custom.tsx @@ -8,9 +8,10 @@ import React, { FunctionComponent, + JSXElementConstructor, useState, useMemo, - useCallback, + memo, } from 'react'; import classNames from 'classnames'; @@ -26,186 +27,251 @@ import { useDataGridHeader } from './header'; import { useDataGridFooter } from './footer'; import { CellWrapper } from './cell'; -export const EuiDataGridBodyCustomRender: FunctionComponent< - EuiDataGridBodyProps -> = ({ - renderCustomGridBody, - renderCellValue, - cellContext, - renderCellPopover, - renderFooterCellValue, - interactiveCellId, - visibleRows, - visibleColCount, - leadingControlColumns, - trailingControlColumns, - columns, - setVisibleColumns, - switchColumnPos, - onColumnResize, - schema, - schemaDetectors, - sorting, - pagination, - rowHeightsOptions, - gridWidth, - gridStyles, - className, -}) => { - /** - * Columns & widths - */ - const visibleColumns = useMemo(() => { - return [...leadingControlColumns, ...columns, ...trailingControlColumns]; - }, [columns, leadingControlColumns, trailingControlColumns]); - - // compute the default column width from the container's width and count of visible columns - const defaultColumnWidth = useDefaultColumnWidth( - gridWidth, - leadingControlColumns, - trailingControlColumns, - columns - ); - - const { columnWidths, setColumnWidth } = useColumnWidths({ - columns, - leadingControlColumns, - trailingControlColumns, - defaultColumnWidth, - onColumnResize, - }); - - /** - * Row heights - */ - const rowHeightUtils = useRowHeightUtils({ - rowHeightsOptions, - gridStyles, - columns, - }); - - const { setRowHeight, getRowHeight } = useDefaultRowHeight({ - rowHeightsOptions, - rowHeightUtils, - }); - - /** - * Header & footer - */ - const { headerRow } = useDataGridHeader({ - leadingControlColumns, - trailingControlColumns, - columns, - columnWidths, - defaultColumnWidth, - setColumnWidth, - visibleColCount, - setVisibleColumns, - switchColumnPos, - sorting, - schema, - schemaDetectors, - gridStyles, - }); - - const { footerRow } = useDataGridFooter({ - renderFooterCellValue, - renderCellPopover, - rowIndex: visibleRows.visibleRowCount, - visibleRowIndex: visibleRows.visibleRowCount, - visibleColCount, - interactiveCellId, - leadingControlColumns, - trailingControlColumns, - columns, - columnWidths, - defaultColumnWidth, - schema, - gridStyles, - }); - - /** - * Cell render fn - */ - const cellProps = useMemo(() => { - return { - schema, - schemaDetectors, - pagination, - columns, - leadingControlColumns, - trailingControlColumns, - visibleColCount, - columnWidths, - defaultColumnWidth, +export const EuiDataGridBodyCustomRender: FunctionComponent = + memo( + ({ + renderCustomGridBody, renderCellValue, cellContext, renderCellPopover, + renderFooterCellValue, interactiveCellId, - setRowHeight, + visibleRows, + visibleColCount, + leadingControlColumns, + trailingControlColumns, + columns, + setVisibleColumns, + switchColumnPos, + onColumnResize, + schema, + schemaDetectors, + sorting, + pagination, rowHeightsOptions, - rowHeightUtils, + gridWidth, gridStyles, - }; - }, [ - schema, - schemaDetectors, - pagination, - columns, - leadingControlColumns, - trailingControlColumns, - visibleColCount, - columnWidths, - defaultColumnWidth, - renderCellValue, - cellContext, - renderCellPopover, - interactiveCellId, - setRowHeight, - rowHeightsOptions, - rowHeightUtils, - gridStyles, - ]); - - const Cell = useCallback( - ({ colIndex, visibleRowIndex, ...rest }) => { - const style = { - height: rowHeightUtils.isAutoHeight(visibleRowIndex, rowHeightsOptions) - ? 'auto' - : getRowHeight(visibleRowIndex), - }; - const props = { - colIndex, - visibleRowIndex, - style, - ...cellProps, - }; - return ; - }, - [cellProps, getRowHeight, rowHeightUtils, rowHeightsOptions] - ); + className, + }) => { + /** + * Columns & widths + */ + const visibleColumns = useMemo(() => { + return [ + ...leadingControlColumns, + ...columns, + ...trailingControlColumns, + ]; + }, [columns, leadingControlColumns, trailingControlColumns]); + + // compute the default column width from the container's width and count of visible columns + const defaultColumnWidth = useDefaultColumnWidth( + gridWidth, + leadingControlColumns, + trailingControlColumns, + columns + ); + + const { columnWidths, setColumnWidth } = useColumnWidths({ + columns, + leadingControlColumns, + trailingControlColumns, + defaultColumnWidth, + onColumnResize, + }); + + /** + * Row heights + */ + const rowHeightUtils = useRowHeightUtils({ + rowHeightsOptions, + gridStyles, + columns, + }); + + const { setRowHeight, getRowHeight } = useDefaultRowHeight({ + rowHeightsOptions, + rowHeightUtils, + }); + + const headerRowProps = useMemo(() => { + return { + leadingControlColumns, + trailingControlColumns, + columns, + columnWidths, + defaultColumnWidth, + setColumnWidth, + setVisibleColumns, + visibleColCount, + switchColumnPos, + sorting, + schema, + schemaDetectors, + gridStyles, + }; + }, [ + leadingControlColumns, + trailingControlColumns, + columns, + columnWidths, + defaultColumnWidth, + setColumnWidth, + visibleColCount, + setVisibleColumns, + switchColumnPos, + sorting, + schema, + schemaDetectors, + gridStyles, + ]); + + /** + * Header & footer + */ + const { headerRow } = useDataGridHeader(headerRowProps); + + const footerRowProps = useMemo( + () => ({ + renderFooterCellValue, + renderCellPopover, + rowIndex: visibleRows.visibleRowCount, + visibleRowIndex: visibleRows.visibleRowCount, + visibleColCount, + interactiveCellId, + leadingControlColumns, + trailingControlColumns, + columns, + columnWidths, + defaultColumnWidth, + schema, + gridStyles, + }), + [ + renderFooterCellValue, + renderCellPopover, + visibleRows.visibleRowCount, + visibleColCount, + interactiveCellId, + leadingControlColumns, + trailingControlColumns, + columns, + columnWidths, + defaultColumnWidth, + schema, + gridStyles, + ] + ); - // Allow consumers to pass custom props/attributes/listeners etc. to the wrapping div - const [customGridBodyProps, setCustomGridBodyProps] = - useState({}); - - return ( -
- {headerRow} - {renderCustomGridBody!({ - visibleColumns, - visibleRowData: visibleRows, - Cell, - setCustomGridBodyProps, - })} - {footerRow} -
+ const { footerRow } = useDataGridFooter(footerRowProps); + + /** + * Cell render fn + */ + const cellProps = useMemo(() => { + return { + schema, + schemaDetectors, + pagination, + columns, + leadingControlColumns, + trailingControlColumns, + visibleColCount, + columnWidths, + defaultColumnWidth, + renderCellValue, + cellContext, + renderCellPopover, + interactiveCellId, + setRowHeight, + rowHeightsOptions, + rowHeightUtils, + gridStyles, + }; + }, [ + schema, + schemaDetectors, + pagination, + columns, + leadingControlColumns, + trailingControlColumns, + visibleColCount, + columnWidths, + defaultColumnWidth, + renderCellValue, + cellContext, + renderCellPopover, + interactiveCellId, + setRowHeight, + rowHeightsOptions, + rowHeightUtils, + gridStyles, + ]); + + const Cell: EuiDataGridCustomBodyProps['Cell'] = useMemo( + () => + ({ colIndex, visibleRowIndex, ...rest }) => { + const style = { + height: rowHeightUtils.isAutoHeight( + visibleRowIndex, + rest.rowHeightsOptions ?? rowHeightsOptions + ) + ? 'auto' + : getRowHeight(visibleRowIndex), + }; + + const props = { + colIndex, + visibleRowIndex, + style, + ...cellProps, + }; + return ; + }, + [cellProps, getRowHeight, rowHeightUtils, rowHeightsOptions] + ); + + // Allow consumers to pass custom props/attributes/listeners etc. to the wrapping div + const [customGridBodyProps, setCustomGridBodyProps] = + useState({}); + + const customDataGridBodyProps: EuiDataGridCustomBodyProps = useMemo( + () => ({ + gridWidth, + visibleColumns, + visibleRowData: visibleRows, + Cell, + setCustomGridBodyProps, + headerRow, + footerRow, + }), + [ + gridWidth, + visibleColumns, + visibleRows, + Cell, + setCustomGridBodyProps, + headerRow, + footerRow, + ] + ); + + const BodyElement = + renderCustomGridBody as JSXElementConstructor; + + return ( +
+ +
+ ); + } ); -}; + +EuiDataGridBodyCustomRender.displayName = 'EuiDataGridBodyCustomRender'; diff --git a/packages/eui/src/components/datagrid/data_grid_types.ts b/packages/eui/src/components/datagrid/data_grid_types.ts index d4983b13af83..a2f459f6bbf5 100644 --- a/packages/eui/src/components/datagrid/data_grid_types.ts +++ b/packages/eui/src/components/datagrid/data_grid_types.ts @@ -516,6 +516,19 @@ export interface EuiDataGridCustomBodyProps { * It's best to wrap calls to `setCustomGridBodyProps` in a `useEffect` hook */ setCustomGridBodyProps: (props: EuiDataGridSetCustomGridBodyProps) => void; + + /** + * The width of the grid, can be used by consumer as a layout utility + */ + gridWidth: number; + /** + * Header row component to render by custom renderer + * */ + headerRow: React.JSX.Element; + /** + * Footer row component to render by custom renderer + * */ + footerRow: React.JSX.Element | null; } export type EuiDataGridSetCustomGridBodyProps = CommonProps &