diff --git a/src-docs/src/views/datagrid/advanced/custom_renderer.tsx b/src-docs/src/views/datagrid/advanced/custom_renderer.tsx index 5f2359059af..dc6f6924f07 100644 --- a/src-docs/src/views/datagrid/advanced/custom_renderer.tsx +++ b/src-docs/src/views/datagrid/advanced/custom_renderer.tsx @@ -19,6 +19,7 @@ import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumnSortingConfig, + RenderCellValue, } from '../../../../../src'; const raw_data: Array<{ [key: string]: string }> = []; @@ -67,6 +68,14 @@ const columns = [ }, ]; +const checkboxRowCellRender: RenderCellValue = ({ rowIndex }) => ( + {}} + /> +); + const leadingControlColumns: EuiDataGridProps['leadingControlColumns'] = [ { id: 'selection', @@ -78,13 +87,7 @@ const leadingControlColumns: EuiDataGridProps['leadingControlColumns'] = [ onChange={() => {}} /> ), - rowCellRender: ({ rowIndex }) => ( - {}} - /> - ), + rowCellRender: checkboxRowCellRender, }, ]; @@ -103,6 +106,22 @@ const trailingControlColumns: EuiDataGridProps['trailingControlColumns'] = [ }, ]; +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 @@ -120,21 +139,7 @@ const rowDetails: EuiDataGridProps['trailingControlColumns'] = [ // When rendering this custom cell, we'll want to override // the automatic width/heights calculated by EuiDataGrid - rowCellRender: ({ 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.{' '} - - - ); - }, + rowCellRender: RowCellRender, }, ]; @@ -144,10 +149,10 @@ const footerCellValues: { [key: string]: string } = { .toLocaleString('en-US', { style: 'currency', currency: 'USD' })}`, }; -const RenderFooterCellValue: EuiDataGridProps['renderFooterCellValue'] = ({ - columnId, - setCellProps, -}) => { +const renderCellValue: RenderCellValue = ({ rowIndex, columnId }) => + raw_data[rowIndex][columnId]; + +const RenderFooterCellValue: RenderCellValue = ({ columnId, setCellProps }) => { const value = footerCellValues[columnId]; useEffect(() => { @@ -298,9 +303,7 @@ export default () => { onChangeItemsPerPage: onChangePageSize, }} rowCount={raw_data.length} - renderCellValue={({ rowIndex, columnId }) => - raw_data[rowIndex][columnId] - } + renderCellValue={renderCellValue} renderFooterCellValue={RenderFooterCellValue} renderCustomGridBody={RenderCustomGridBody} height={autoHeight ? undefined : 400} diff --git a/src-docs/src/views/datagrid/advanced/ref.tsx b/src-docs/src/views/datagrid/advanced/ref.tsx index 256ca249ec1..89bca493b91 100644 --- a/src-docs/src/views/datagrid/advanced/ref.tsx +++ b/src-docs/src/views/datagrid/advanced/ref.tsx @@ -20,6 +20,7 @@ import { EuiDataGridColumnSortingConfig, EuiDataGridPaginationProps, EuiDataGridSorting, + RenderCellValue, } from '../../../../../src'; const raw_data: Array<{ [key: string]: string }> = []; @@ -33,6 +34,9 @@ for (let i = 1; i < 100; i++) { }); } +const renderCellValue: RenderCellValue = ({ rowIndex, columnId }) => + raw_data[rowIndex][columnId]; + export default () => { const dataGridRef = useRef(null); @@ -219,9 +223,7 @@ export default () => { sorting={{ columns: sortingColumns, onSort }} inMemory={{ level: 'sorting' }} rowCount={raw_data.length} - renderCellValue={({ rowIndex, columnId }) => - raw_data[rowIndex][columnId] - } + renderCellValue={renderCellValue} pagination={{ ...pagination, onChangePage: onChangePage, diff --git a/src-docs/src/views/datagrid/cells_popovers/cell_popover_is_expandable.tsx b/src-docs/src/views/datagrid/cells_popovers/cell_popover_is_expandable.tsx index deaf3080358..6f4a8623822 100644 --- a/src-docs/src/views/datagrid/cells_popovers/cell_popover_is_expandable.tsx +++ b/src-docs/src/views/datagrid/cells_popovers/cell_popover_is_expandable.tsx @@ -5,6 +5,7 @@ import { EuiDataGrid, EuiDataGridColumnCellAction, EuiDataGridColumn, + RenderCellValue as RenderCellValueType, } from '../../../../../src'; const cellActions: EuiDataGridColumnCellAction[] = [ @@ -50,6 +51,22 @@ for (let i = 1; i < 5; i++) { }); } +const RenderCellValue: RenderCellValueType = ({ + rowIndex, + columnId, + setCellProps, +}) => { + const value = data[rowIndex][columnId]; + + useEffect(() => { + if (columnId === 'boolean' && value === 'false') { + setCellProps({ isExpandable: false }); + } + }, [columnId, value, setCellProps]); + + return value; +}; + export default () => { const [visibleColumns, setVisibleColumns] = useState( columns.map(({ id }) => id) @@ -61,17 +78,7 @@ export default () => { columns={columns} columnVisibility={{ visibleColumns, setVisibleColumns }} rowCount={data.length} - renderCellValue={({ rowIndex, columnId, setCellProps }) => { - const value = data[rowIndex][columnId]; - - useEffect(() => { - if (columnId === 'boolean' && value === 'false') { - setCellProps({ isExpandable: false }); - } - }, [columnId, value, setCellProps]); - - return value; - }} + renderCellValue={RenderCellValue} /> ); }; diff --git a/src-docs/src/views/datagrid/cells_popovers/cell_popover_rendercellpopover.tsx b/src-docs/src/views/datagrid/cells_popovers/cell_popover_rendercellpopover.tsx index 6171f46f9d1..a16fa0a34be 100644 --- a/src-docs/src/views/datagrid/cells_popovers/cell_popover_rendercellpopover.tsx +++ b/src-docs/src/views/datagrid/cells_popovers/cell_popover_rendercellpopover.tsx @@ -14,6 +14,7 @@ import { EuiCopy, EuiText, EuiImage, + RenderCellValue as RenderCellValueType, } from '../../../../../src'; const cellActions: EuiDataGridColumnCellAction[] = [ @@ -163,6 +164,9 @@ const RenderCellPopover = (props: EuiDataGridCellPopoverElementProps) => { ); }; +const renderCellValue: RenderCellValueType = ({ rowIndex, columnId }) => + data[rowIndex][columnId]; + export default () => { const [visibleColumns, setVisibleColumns] = useState( columns.map(({ id }) => id) @@ -174,7 +178,7 @@ export default () => { columns={columns} columnVisibility={{ visibleColumns, setVisibleColumns }} rowCount={data.length} - renderCellValue={({ rowIndex, columnId }) => data[rowIndex][columnId]} + renderCellValue={renderCellValue} renderCellPopover={RenderCellPopover} /> ); diff --git a/src-docs/src/views/datagrid/cells_popovers/visible_cell_actions.tsx b/src-docs/src/views/datagrid/cells_popovers/visible_cell_actions.tsx index 91d76498619..c469e65a345 100644 --- a/src-docs/src/views/datagrid/cells_popovers/visible_cell_actions.tsx +++ b/src-docs/src/views/datagrid/cells_popovers/visible_cell_actions.tsx @@ -5,6 +5,7 @@ import { EuiDataGrid, EuiDataGridColumnCellAction, EuiDataGridColumn, + RenderCellValue as RenderCellValueType, } from '../../../../../src/components'; const cellActions1: EuiDataGridColumnCellAction[] = [ @@ -79,6 +80,9 @@ for (let i = 1; i < 5; i++) { }); } +const renderCellValue: RenderCellValueType = ({ rowIndex, columnId }) => + data[rowIndex][columnId]; + export default () => { const [visibleColumns, setVisibleColumns] = useState( columns.map(({ id }) => id) @@ -90,7 +94,7 @@ export default () => { columns={columns} columnVisibility={{ visibleColumns, setVisibleColumns }} rowCount={data.length} - renderCellValue={({ rowIndex, columnId }) => data[rowIndex][columnId]} + renderCellValue={renderCellValue} /> ); }; diff --git a/src-docs/src/views/datagrid/styling/row_height_auto.tsx b/src-docs/src/views/datagrid/styling/row_height_auto.tsx index b963efcdfc3..304668576a7 100644 --- a/src-docs/src/views/datagrid/styling/row_height_auto.tsx +++ b/src-docs/src/views/datagrid/styling/row_height_auto.tsx @@ -10,7 +10,7 @@ import githubData from '../_row_auto_height_data.json'; import { EuiDataGrid, - EuiDataGridProps, + RenderCellValue as RenderCellValueType, EuiLink, EuiAvatar, EuiBadge, @@ -68,7 +68,7 @@ const columns = [ // instead of loading up front, generate entries on the fly const raw_data: DataShape[] = githubData; -const RenderCellValue: EuiDataGridProps['renderCellValue'] = ({ +const RenderCellValue: RenderCellValueType = ({ rowIndex, columnId, isDetails, diff --git a/src-docs/src/views/datagrid/styling/row_height_fixed.tsx b/src-docs/src/views/datagrid/styling/row_height_fixed.tsx index ba96749e2d0..10eb3f20e6e 100644 --- a/src-docs/src/views/datagrid/styling/row_height_fixed.tsx +++ b/src-docs/src/views/datagrid/styling/row_height_fixed.tsx @@ -10,7 +10,7 @@ import githubData from '../_row_auto_height_data.json'; import { EuiDataGrid, - EuiDataGridProps, + RenderCellValue as RenderCellValueType, EuiLink, EuiAvatar, EuiBadge, @@ -68,7 +68,7 @@ const columns = [ // instead of loading up front, generate entries on the fly const raw_data: DataShape[] = githubData; -const RenderCellValue: EuiDataGridProps['renderCellValue'] = ({ +const RenderCellValue: RenderCellValueType = ({ rowIndex, columnId, isDetails, diff --git a/src-docs/src/views/datagrid/styling/row_line_height.tsx b/src-docs/src/views/datagrid/styling/row_line_height.tsx index b2f87a92ef2..917973d3188 100644 --- a/src-docs/src/views/datagrid/styling/row_line_height.tsx +++ b/src-docs/src/views/datagrid/styling/row_line_height.tsx @@ -12,7 +12,7 @@ import { EuiDataGrid, EuiDataGridColumnSortingConfig, EuiDataGridPaginationProps, - EuiDataGridProps, + RenderCellValue as RenderCellValueType, EuiDataGridSorting, formatDate, } from '../../../../../src'; @@ -62,7 +62,7 @@ const columns = [ // instead of loading up front, generate entries on the fly const raw_data: DataShape[] = githubData; -const RenderCellValue: EuiDataGridProps['renderCellValue'] = ({ +const RenderCellValue: RenderCellValueType = ({ rowIndex, columnId, isDetails, diff --git a/src-docs/src/views/datagrid/toolbar/additional_controls.tsx b/src-docs/src/views/datagrid/toolbar/additional_controls.tsx index b45ba919abc..41d42843c72 100644 --- a/src-docs/src/views/datagrid/toolbar/additional_controls.tsx +++ b/src-docs/src/views/datagrid/toolbar/additional_controls.tsx @@ -18,6 +18,7 @@ import { EuiContextMenuPanel, EuiPopover, EuiDataGridPaginationProps, + RenderCellValue, } from '../../../../../src'; const columns = [ @@ -50,6 +51,9 @@ for (let i = 1; i < 20; i++) { }); } +const renderCellValue: RenderCellValue = ({ rowIndex, columnId }) => + data[rowIndex][columnId]; + export default () => { const [pagination, setPagination] = useState({ pageIndex: 0 }); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); @@ -127,7 +131,7 @@ export default () => { border: 'horizontal', header: 'underline', }} - renderCellValue={({ rowIndex, columnId }) => data[rowIndex][columnId]} + renderCellValue={renderCellValue} pagination={{ ...pagination, onChangeItemsPerPage: setPageSize, diff --git a/src-docs/src/views/datagrid/toolbar/render_custom_toolbar.tsx b/src-docs/src/views/datagrid/toolbar/render_custom_toolbar.tsx index 0657f7ff511..1f3944e4a0b 100644 --- a/src-docs/src/views/datagrid/toolbar/render_custom_toolbar.tsx +++ b/src-docs/src/views/datagrid/toolbar/render_custom_toolbar.tsx @@ -13,6 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, euiScreenReaderOnly, + RenderCellValue, } from '../../../../../src'; const raw_data: Array<{ [key: string]: string }> = []; @@ -82,6 +83,9 @@ const renderCustomToolbar: EuiDataGridToolbarProps['renderCustomToolbar'] = ({ ); }; +const renderCellValue: RenderCellValue = ({ rowIndex, columnId }) => + raw_data[rowIndex][columnId]; + // Some additional custom settings to show in the Display popover const AdditionalDisplaySettings = () => { const [exampleSettingValue, setExampleSettingValue] = useState(10); @@ -126,7 +130,7 @@ export default () => { columnVisibility={{ visibleColumns, setVisibleColumns }} sorting={{ columns: sortingColumns, onSort }} rowCount={raw_data.length} - renderCellValue={({ rowIndex, columnId }) => raw_data[rowIndex][columnId]} + renderCellValue={renderCellValue} gridStyle={{ border: 'none', header: 'underline' }} renderCustomToolbar={renderCustomToolbar} toolbarVisibility={{ diff --git a/src/components/datagrid/body/cell/data_grid_cell.test.tsx b/src/components/datagrid/body/cell/data_grid_cell.test.tsx index 2e02393b49c..aae8013f6c2 100644 --- a/src/components/datagrid/body/cell/data_grid_cell.test.tsx +++ b/src/components/datagrid/body/cell/data_grid_cell.test.tsx @@ -175,6 +175,10 @@ describe('EuiDataGridCell', () => { style: { top: 0, left: 0, width: 50, height: 10 }, }); }); + it('cellContext', () => { + component.setProps({ cellContext: { someData: true } }); + component.setProps({ cellContext: { someData: false } }); + }); }); describe('when state changes:', () => { diff --git a/src/components/datagrid/body/cell/data_grid_cell.tsx b/src/components/datagrid/body/cell/data_grid_cell.tsx index 059a2f90493..2f019022b8f 100644 --- a/src/components/datagrid/body/cell/data_grid_cell.tsx +++ b/src/components/datagrid/body/cell/data_grid_cell.tsx @@ -15,8 +15,9 @@ import React, { JSXElementConstructor, KeyboardEvent, memo, + useMemo, MutableRefObject, - ReactNode, + ReactElement, } from 'react'; import { createPortal } from 'react-dom'; @@ -48,13 +49,15 @@ import { HandleInteractiveChildren } from './focus_utils'; const EuiDataGridCellContent: FunctionComponent< EuiDataGridCellValueProps & { setCellProps: EuiDataGridCellValueElementProps['setCellProps']; - setCellContentsRef: EuiDataGridCell['setCellContentsRef']; + setCellContentsRef: (ref: HTMLDivElement | null) => void; + showCellActions: boolean; isExpanded: boolean; + onExpandClick: () => void; + popoverAnchorRef: MutableRefObject; isControlColumn: boolean; isFocused: boolean; ariaRowIndex: number; rowHeight?: EuiDataGridRowHeightOption; - cellActions?: ReactNode; } > = memo( ({ @@ -69,82 +72,91 @@ const EuiDataGridCellContent: FunctionComponent< rowHeightUtils, isControlColumn, isFocused, - cellActions, + showCellActions, + onExpandClick, + popoverAnchorRef, ...rest }) => { - // React is more permissible than the TS types indicate + // React is more permissive than the TS types indicate const CellElement = renderCellValue as JSXElementConstructor; - const cellHeightType = - rowHeightUtils?.getHeightType(rowHeight) || 'default'; - - const classes = classNames( - 'euiDataGridRowCell__content', - `euiDataGridRowCell__content--${cellHeightType}Height`, - !isControlColumn && { - 'eui-textBreakWord': cellHeightType !== 'default', - 'eui-textTruncate': cellHeightType === 'default', - } + const cellHeightType = useMemo( + () => rowHeightUtils?.getHeightType(rowHeight) || 'default', + [rowHeightUtils, rowHeight] ); - let cellContent = ( -
- -
- ); - if (cellHeightType === 'lineCount' && !isControlColumn) { - const lines = rowHeightUtils!.getLineCount(rowHeight)!; - cellContent = ( - - {cellContent} - - ); - } - - const screenReaderText = ( - - - + const classes = useMemo( + () => + classNames( + 'euiDataGridRowCell__content', + `euiDataGridRowCell__content--${cellHeightType}Height`, + !isControlColumn && { + 'eui-textBreakWord': cellHeightType !== 'default', + 'eui-textTruncate': cellHeightType === 'default', + } + ), + [cellHeightType, isControlColumn] ); return ( <> - {cellContent} - {screenReaderText} - {cellActions} + +
+ +
+
+ + + + + + {showCellActions && ( + + )} ); } @@ -380,6 +392,7 @@ export class EuiDataGridCell extends Component< if (nextProps.style.width !== this.props.style.width) return true; } + if (nextProps.cellContext !== this.props.cellContext) return true; if (nextState.cellProps !== this.state.cellProps) return true; if (nextState.isFocused !== this.state.isFocused) return true; if (nextState.isHovered !== this.state.isHovered) return true; @@ -608,82 +621,96 @@ export class EuiDataGridCell extends Component< rowHeightsOptions ); - const cellContentProps = { - ...rest, - setCellProps: this.setCellProps, - column, - columnType, - isExpandable, - isExpanded: popoverIsOpen, - isDetails: false, - isFocused: this.state.isFocused, - setCellContentsRef: this.setCellContentsRef, - rowHeight, - rowHeightUtils, - isControlColumn: cellClasses.includes( - 'euiDataGridRowCell--controlColumn' - ), - ariaRowIndex, - }; - - const cell = ( -
- - - - {/* Give the cell expansion popover a separate div/ref - otherwise the - extra popover wrappers mess up the absolute positioning and cause - animation stuttering */} -
- - ) - } - /> - -
- ); - - return rowManager && !IS_JEST_ENVIRONMENT - ? createPortal( - cell, - rowManager.getRow({ + const row = + rowManager && !IS_JEST_ENVIRONMENT + ? 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 }) - ) - : cell; + : undefined; + + return ( + +
+ + + +
+
+ ); } } + +/** + * Function component utilities for conditional rendering. + * Used for DRYness and performance + */ + +const RenderCellInRow: FunctionComponent<{ + children: ReactElement; + row?: HTMLElement; +}> = memo(({ row, children }) => + row ? createPortal(children, row) : children +); +RenderCellInRow.displayName = 'RenderCellInRow'; + +const RenderTruncatedCellContent: FunctionComponent<{ + children: ReactElement; + hasLineCountTruncation: boolean; + rowHeight?: EuiDataGridRowHeightOption; +}> = memo(({ children, hasLineCountTruncation, rowHeight }) => { + // If `hasLineCountTruncation` is true, we can rely on rowHeight being the correct type + const lines = hasLineCountTruncation + ? (rowHeight as { lineCount: number }).lineCount + : undefined; + + return lines ? ( + + {children} + + ) : ( + children + ); +}); +RenderTruncatedCellContent.displayName = 'RenderTruncatedCellContent'; diff --git a/src/components/datagrid/body/cell/data_grid_cell_actions.test.tsx b/src/components/datagrid/body/cell/data_grid_cell_actions.test.tsx index fe16e085dd9..f17711d7d3e 100644 --- a/src/components/datagrid/body/cell/data_grid_cell_actions.test.tsx +++ b/src/components/datagrid/body/cell/data_grid_cell_actions.test.tsx @@ -22,6 +22,7 @@ const MockAction: EuiDataGridColumnCellAction = ({ Component }) => ( describe('EuiDataGridCellActions', () => { const requiredProps = { onExpandClick: jest.fn(), + popoverAnchorRef: () => {}, rowIndex: 0, colIndex: 0, cellHeightType: 'default', @@ -31,17 +32,22 @@ describe('EuiDataGridCellActions', () => { const component = shallow(); expect(component).toMatchInlineSnapshot(` -
- +
- - -
+ + + +
+
+ `); const button: Function = component.find('EuiI18n').renderProp('children'); @@ -68,7 +74,7 @@ describe('EuiDataGridCellActions', () => { /> ); - const button = component.childAt(0).renderProp('Component'); + const button = component.childAt(0).childAt(0).renderProp('Component'); expect(button({ iconType: 'eye' })).toMatchInlineSnapshot(` { ); expect(component).toMatchInlineSnapshot(` -
- - +
- - -
+ + + + +
+
+ `); }); diff --git a/src/components/datagrid/body/cell/data_grid_cell_actions.tsx b/src/components/datagrid/body/cell/data_grid_cell_actions.tsx index 701b25655f2..3f278e1f9a0 100644 --- a/src/components/datagrid/body/cell/data_grid_cell_actions.tsx +++ b/src/components/datagrid/body/cell/data_grid_cell_actions.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { JSXElementConstructor, useMemo, useCallback } from 'react'; +import React, { JSXElementConstructor, Ref, useMemo, useCallback } from 'react'; import { EuiDataGridColumn, EuiDataGridColumnCellAction, @@ -24,11 +24,13 @@ import { EuiPopoverFooter } from '../../../popover'; export const EuiDataGridCellActions = ({ onExpandClick, + popoverAnchorRef, column, rowIndex, colIndex, }: { onExpandClick: () => void; + popoverAnchorRef: Ref; column?: EuiDataGridColumn; rowIndex: number; colIndex: number; @@ -37,26 +39,29 @@ export const EuiDataGridCellActions = ({ // column.cellActions is present (regardless of column.isExpandable). // This is because cell actions are not otherwise accessible to keyboard // or screen reader users - const expandButton = ( - - {(expandButtonTitle: string) => ( - - )} - + const expandButton = useMemo( + () => ( + + {(expandButtonTitle: string) => ( + + )} + + ), + [onExpandClick] ); const additionalButtons = useMemo(() => { @@ -99,9 +104,15 @@ export const EuiDataGridCellActions = ({ }, [column, colIndex, rowIndex]); return ( -
- {[...additionalButtons, expandButton]} -
+ <> +
+ {[...additionalButtons, expandButton]} +
+ {/* The cell expansion popover needs a separate div/ref - otherwise the + extra popover wrappers mess up the absolute positioning and cause + animation stuttering on the cell actions */} +
+ ); }; diff --git a/src/components/datagrid/body/cell/data_grid_cell_popover.tsx b/src/components/datagrid/body/cell/data_grid_cell_popover.tsx index 4504e1e5862..38da6799bea 100644 --- a/src/components/datagrid/body/cell/data_grid_cell_popover.tsx +++ b/src/components/datagrid/body/cell/data_grid_cell_popover.tsx @@ -6,7 +6,13 @@ * Side Public License, v 1. */ -import React, { createContext, useState, useCallback, ReactNode } from 'react'; +import React, { + createContext, + useState, + useCallback, + useMemo, + ReactNode, +} from 'react'; import classNames from 'classnames'; import { keys } from '../../../../services'; @@ -72,17 +78,6 @@ export const useCellPopover = (): { [popoverIsOpen, cellLocation] ); - const cellPopoverContext = { - popoverIsOpen, - closeCellPopover, - openCellPopover, - cellLocation, - setPopoverAnchor, - setPopoverAnchorPosition, - setPopoverContent, - setCellPopoverProps, - }; - // Override the default EuiPopover `onClickOutside` behavior, since the toggling // popover button isn't actually the DOM node we pass to `button`. Otherwise, // clicking the expansion cell action triggers an outside click @@ -97,57 +92,93 @@ export const useCellPopover = (): { [popoverAnchor, closeCellPopover] ); - // Note that this popover is rendered once at the top grid level, rather than one popover per cell - const cellPopover = popoverIsOpen && popoverAnchor && ( - { - if (event.key === keys.F2 || event.key === keys.ESCAPE) { - event.preventDefault(); - event.stopPropagation(); - closeCellPopover(); - const cell = - popoverAnchor.parentElement?.parentElement?.parentElement; - - // Prevent cell animation flash while focus is being shifted between popover and cell - cell?.setAttribute('data-keyboard-closing', 'true'); - // Ensure focus is returned to the parent cell, and remove animation stopgap - requestAnimationFrame(() => { - popoverAnchor.parentElement!.focus(); - cell?.removeAttribute('data-keyboard-closing'); - }); - } - }} - button={popoverAnchor} - closePopover={closeCellPopover} - > - {popoverContent} - + const onKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === keys.F2 || event.key === keys.ESCAPE) { + event.preventDefault(); + event.stopPropagation(); + closeCellPopover(); + const cell = popoverAnchor?.parentElement?.parentElement?.parentElement; + + // Prevent cell animation flash while focus is being shifted between popover and cell + cell?.setAttribute('data-keyboard-closing', 'true'); + // Ensure focus is returned to the parent cell, and remove animation stopgap + requestAnimationFrame(() => { + popoverAnchor?.parentElement!.focus(); + cell?.removeAttribute('data-keyboard-closing'); + }); + } + }, + [popoverAnchor, closeCellPopover] ); - return { cellPopoverContext, cellPopover }; + const cellPopoverContext = useMemo(() => { + return { + popoverIsOpen, + closeCellPopover, + openCellPopover, + cellLocation, + setPopoverAnchorPosition, + setPopoverAnchor, + setPopoverContent, + setCellPopoverProps, + }; + }, [popoverIsOpen, closeCellPopover, openCellPopover, cellLocation]); + + const cellPopover = useMemo(() => { + if (!popoverIsOpen || !popoverAnchor) return null; + + // Note that this popover is rendered once at the top grid level, rather than one popover per cell + return ( + + {popoverContent} + + ); + }, [ + popoverIsOpen, + popoverAnchor, + popoverContent, + cellPopoverProps, + closeCellPopover, + onClickOutside, + onKeyDown, + popoverAnchorPosition, + ]); + + return useMemo( + () => ({ + cellPopoverContext, + cellPopover, + }), + [cellPopoverContext, cellPopover] + ); }; /** diff --git a/src/components/datagrid/body/cell/data_grid_cell_wrapper.test.tsx b/src/components/datagrid/body/cell/data_grid_cell_wrapper.test.tsx index 6b822ab55a8..2311456987c 100644 --- a/src/components/datagrid/body/cell/data_grid_cell_wrapper.test.tsx +++ b/src/components/datagrid/body/cell/data_grid_cell_wrapper.test.tsx @@ -12,7 +12,7 @@ import { shallow } from 'enzyme'; import { RowHeightUtils } from '../../utils/__mocks__/row_heights'; import { schemaDetectors } from '../../utils/data_grid_schema'; -import { Cell } from './data_grid_cell_wrapper'; +import { CellWrapper } from './data_grid_cell_wrapper'; describe('Cell', () => { const requiredProps = { @@ -33,13 +33,13 @@ describe('Cell', () => { }; it('is a light wrapper around EuiDataGridCell', () => { - const component = shallow(); + const component = shallow(); expect(component.find('EuiDataGridCell').exists()).toBe(true); }); it('renders leading control column cells', () => { const component = shallow( - { it('renders trailing control column cells', () => { const component = shallow( - { it('renders text transform classes based on schema', () => { const component = shallow( - { it('allows passing optional EuiDataGridCellProps overrides', () => { const component = shallow( - & Pick< EuiDataGridBodyProps, @@ -52,133 +53,152 @@ type CellProps = Pick< * It grabs context, determines the type of cell being rendered * (e.g. control vs data cell), & sets shared props between all cells */ -export const Cell: FunctionComponent = ({ - colIndex, - visibleRowIndex, - style, - schema, - schemaDetectors, - pagination, - columns, - leadingControlColumns, - trailingControlColumns, - visibleColCount, - columnWidths, - defaultColumnWidth, - renderCellValue, - cellContext, - renderCellPopover, - interactiveCellId, - setRowHeight, - rowHeightsOptions, - rowHeightUtils, - rowManager, - ...rest -}) => { - const popoverContext = useContext(DataGridCellPopoverContext); - const { getCorrectRowIndex } = useContext(DataGridSortingContext); - - let cellContent; - - const isFirstColumn = colIndex === 0; - const isLastColumn = colIndex === visibleColCount - 1; - - const isLeadingControlColumn = colIndex < leadingControlColumns.length; - const isTrailingControlColumn = - colIndex >= leadingControlColumns.length + columns.length; - - const datacolIndex = colIndex - leadingControlColumns.length; - const column = columns[datacolIndex]; - const columnId = column?.id; - - const textTransform = useMemo( - () => - schemaDetectors.filter((row: EuiDataGridSchemaDetector) => { - return column?.schema - ? column?.schema === row.type - : columnId === row.type; - })[0]?.textTransform, - [columnId, column?.schema, schemaDetectors] - ); - - const classes = classNames({ - 'euiDataGridRowCell--firstColumn': isFirstColumn, - 'euiDataGridRowCell--lastColumn': isLastColumn, - 'euiDataGridRowCell--controlColumn': - isLeadingControlColumn || isTrailingControlColumn, - [`euiDataGridRowCell--${textTransform}`]: textTransform, - }); - - const sharedCellProps = { - rowIndex: getCorrectRowIndex(visibleRowIndex), - visibleRowIndex, +export const CellWrapper: FunctionComponent = memo( + ({ colIndex, - interactiveCellId, - className: classes, + visibleRowIndex, style, + schema, + schemaDetectors, + pagination, + columns, + leadingControlColumns, + trailingControlColumns, + visibleColCount, + columnWidths, + defaultColumnWidth, + renderCellValue, + cellContext, + renderCellPopover, + interactiveCellId, + setRowHeight, rowHeightsOptions, rowHeightUtils, - setRowHeight: isFirstColumn ? setRowHeight : undefined, rowManager, - popoverContext, - pagination, - cellContext, - }; - - if (isLeadingControlColumn) { - const leadingColumn = leadingControlColumns[colIndex]; - const { id, rowCellRender } = leadingColumn; - - cellContent = ( - - ); - } else if (isTrailingControlColumn) { - const columnOffset = columns.length + leadingControlColumns.length; - const trailingcolIndex = colIndex - columnOffset; - const trailingColumn = trailingControlColumns[trailingcolIndex]; - const { id, rowCellRender } = trailingColumn; - - cellContent = ( - - ); - } else { - // this is a normal data cell - const columnType = schema[columnId] ? schema[columnId].columnType : null; - - const isExpandable = - column.isExpandable !== undefined ? column.isExpandable : true; - - const width = columnWidths[columnId] || defaultColumnWidth; - - cellContent = ( - + ...rest + }) => { + const popoverContext = useContext(DataGridCellPopoverContext); + const { getCorrectRowIndex } = useContext(DataGridSortedContext); + + const isFirstColumn = colIndex === 0; + const isLastColumn = colIndex === visibleColCount - 1; + + const isLeadingControlColumn = colIndex < leadingControlColumns.length; + const isTrailingControlColumn = + colIndex >= leadingControlColumns.length + columns.length; + + const datacolIndex = colIndex - leadingControlColumns.length; + const column = columns[datacolIndex]; + const columnId = column?.id; + + const textTransform = useMemo( + () => + schemaDetectors.filter((row: EuiDataGridSchemaDetector) => { + return column?.schema + ? column?.schema === row.type + : columnId === row.type; + })[0]?.textTransform, + [columnId, column?.schema, schemaDetectors] ); + + const sharedCellProps = useMemo(() => { + const classes = classNames({ + 'euiDataGridRowCell--firstColumn': isFirstColumn, + 'euiDataGridRowCell--lastColumn': isLastColumn, + 'euiDataGridRowCell--controlColumn': + isLeadingControlColumn || isTrailingControlColumn, + [`euiDataGridRowCell--${textTransform}`]: textTransform, + }); + return { + rowIndex: getCorrectRowIndex(visibleRowIndex), + visibleRowIndex, + colIndex, + interactiveCellId, + className: classes, + style, + rowHeightsOptions, + rowHeightUtils, + setRowHeight: isFirstColumn ? setRowHeight : undefined, + rowManager, + popoverContext, + pagination, + cellContext, + }; + }, [ + colIndex, + setRowHeight, + visibleRowIndex, + getCorrectRowIndex, + interactiveCellId, + style, + rowHeightsOptions, + rowHeightUtils, + rowManager, + popoverContext, + pagination, + cellContext, + isFirstColumn, + isLastColumn, + isLeadingControlColumn, + isTrailingControlColumn, + textTransform, + ]); + + if (isLeadingControlColumn) { + const leadingColumn = leadingControlColumns[colIndex]; + const { id, rowCellRender } = leadingColumn; + + return ( + + ); + } else if (isTrailingControlColumn) { + const columnOffset = columns.length + leadingControlColumns.length; + const trailingcolIndex = colIndex - columnOffset; + const trailingColumn = trailingControlColumns[trailingcolIndex]; + const { id, rowCellRender } = trailingColumn; + + return ( + + ); + } else { + // this is a normal data cell + const columnType = schema[columnId] ? schema[columnId].columnType : null; + + const isExpandable = + column?.isExpandable !== undefined ? column?.isExpandable : true; + + const width = columnWidths[columnId] || defaultColumnWidth; + + return ( + + ); + } } +); - return cellContent; -}; +CellWrapper.displayName = 'CellWrapper'; diff --git a/src/components/datagrid/body/cell/index.ts b/src/components/datagrid/body/cell/index.ts index ed2338e8de2..97cbb9d8106 100644 --- a/src/components/datagrid/body/cell/index.ts +++ b/src/components/datagrid/body/cell/index.ts @@ -8,7 +8,7 @@ export { EuiDataGridCell } from './data_grid_cell'; -export { Cell } from './data_grid_cell_wrapper'; +export { CellWrapper } from './data_grid_cell_wrapper'; export { DataGridCellPopoverContext, diff --git a/src/components/datagrid/body/data_grid_body_custom.tsx b/src/components/datagrid/body/data_grid_body_custom.tsx index df119b6b84b..bec60b9fd59 100644 --- a/src/components/datagrid/body/data_grid_body_custom.tsx +++ b/src/components/datagrid/body/data_grid_body_custom.tsx @@ -24,31 +24,32 @@ import { } from '../data_grid_types'; import { useDataGridHeader } from './header'; import { useDataGridFooter } from './footer'; -import { Cell } from './cell'; +import { CellWrapper } from './cell'; export const EuiDataGridBodyCustomRender: FunctionComponent< EuiDataGridBodyProps > = ({ renderCustomGridBody, - leadingControlColumns, - trailingControlColumns, - columns, - visibleColCount, - schema, - schemaDetectors, - visibleRows, renderCellValue, cellContext, renderCellPopover, renderFooterCellValue, interactiveCellId, + visibleRows, + visibleColCount, + leadingControlColumns, + trailingControlColumns, + columns, setVisibleColumns, switchColumnPos, onColumnResize, - gridWidth, - gridStyles, + schema, + schemaDetectors, + sorting, pagination, rowHeightsOptions, + gridWidth, + gridStyles, }) => { /** * Columns & widths @@ -91,14 +92,15 @@ export const EuiDataGridBodyCustomRender: FunctionComponent< * Header & footer */ const { headerRow } = useDataGridHeader({ - switchColumnPos, - setVisibleColumns, leadingControlColumns, trailingControlColumns, columns, columnWidths, defaultColumnWidth, setColumnWidth, + setVisibleColumns, + switchColumnPos, + sorting, schema, schemaDetectors, }); @@ -120,7 +122,26 @@ export const EuiDataGridBodyCustomRender: FunctionComponent< /** * Cell render fn */ - const cellProps = { + const cellProps = useMemo(() => { + return { + schema, + schemaDetectors, + pagination, + columns, + leadingControlColumns, + trailingControlColumns, + visibleColCount, + columnWidths, + defaultColumnWidth, + renderCellValue, + cellContext, + renderCellPopover, + interactiveCellId, + setRowHeight, + rowHeightsOptions, + rowHeightUtils, + }; + }, [ schema, schemaDetectors, pagination, @@ -137,9 +158,9 @@ export const EuiDataGridBodyCustomRender: FunctionComponent< setRowHeight, rowHeightsOptions, rowHeightUtils, - }; + ]); - const _Cell = useCallback( + const Cell = useCallback( ({ colIndex, visibleRowIndex, ...rest }) => { const style = { height: rowHeightUtils.isAutoHeight(visibleRowIndex, rowHeightsOptions) @@ -152,9 +173,9 @@ export const EuiDataGridBodyCustomRender: FunctionComponent< style, ...cellProps, }; - return ; + return ; }, - [...Object.values(cellProps), getRowHeight] // eslint-disable-line react-hooks/exhaustive-deps + [cellProps, getRowHeight, rowHeightUtils, rowHeightsOptions] ); // Allow consumers to pass custom props/attributes/listeners etc. to the wrapping div @@ -173,7 +194,7 @@ export const EuiDataGridBodyCustomRender: FunctionComponent< {renderCustomGridBody!({ visibleColumns, visibleRowData: visibleRows, - Cell: _Cell, + Cell, setCustomGridBodyProps, })} {footerRow} diff --git a/src/components/datagrid/body/data_grid_body_virtualized.tsx b/src/components/datagrid/body/data_grid_body_virtualized.tsx index fc70ed71b3a..aeaee32047c 100644 --- a/src/components/datagrid/body/data_grid_body_virtualized.tsx +++ b/src/components/datagrid/body/data_grid_body_virtualized.tsx @@ -11,20 +11,25 @@ import React, { forwardRef, FunctionComponent, createContext, + useCallback, useContext, useEffect, useRef, + useMemo, PropsWithChildren, + memo, } from 'react'; import { GridChildComponentProps, VariableSizeGrid as Grid, VariableSizeGridProps, + GridOnItemsRenderedProps, } from 'react-window'; +import { useDeepEqual } from '../../../services'; import { useResizeObserver } from '../../observer/resize_observer'; import { useDataGridHeader } from './header'; import { useDataGridFooter } from './footer'; -import { Cell } from './cell'; +import { CellWrapper } from './cell'; import { EuiDataGridBodyProps, DataGridWrapperRowsContentsShape, @@ -40,25 +45,28 @@ import { useRowHeightUtils, useDefaultRowHeight } from '../utils/row_heights'; import { useScrollBars, useScroll } from '../utils/scrolling'; import { IS_JEST_ENVIRONMENT } from '../../../utils'; -export const _Cell: FunctionComponent = ({ - columnIndex, - rowIndex, - style, - data, -}) => { - const { headerRowHeight } = useContext(DataGridWrapperRowsContext); - return ( - - ); -}; +export const Cell: FunctionComponent = memo( + ({ columnIndex, rowIndex, style, data }) => { + const memoizedStyles = useDeepEqual(style); + const cellStyles = useMemo(() => { + const { headerRowHeight } = data; + return { + ...memoizedStyles, + top: `${parseFloat(memoizedStyles.top as string) + headerRowHeight}px`, + }; + }, [memoizedStyles, data]); + + return ( + + ); + } +); +Cell.displayName = 'Cell'; // Context is required to pass props to react-window's innerElementType // @see https://github.com/bvaughn/react-window/issues/404 @@ -75,248 +83,239 @@ type InnerElementProps = PropsWithChildren & { }; }; -const InnerElement: VariableSizeGridProps['innerElementType'] = forwardRef< - HTMLDivElement, - InnerElementProps ->(({ children, style, ...rest }, ref) => { - const { headerRowHeight, headerRow, footerRow } = useContext( - DataGridWrapperRowsContext - ); - return ( - <> -
- {headerRow} - {children} -
- {footerRow} - - ); -}); +const InnerElement: VariableSizeGridProps['innerElementType'] = memo( + forwardRef( + ({ children, style, ...rest }, ref) => { + const { headerRowHeight, headerRow, footerRow } = useContext( + DataGridWrapperRowsContext + ); + const memoizedStyles = useDeepEqual(style); + const innerElementStyles = useMemo(() => { + return { + ...memoizedStyles, + height: memoizedStyles.height + headerRowHeight, + }; + }, [memoizedStyles, headerRowHeight]); + + return ( + <> +
+ {headerRow} + {children} +
+ {footerRow} + + ); + } + ) +); InnerElement.displayName = 'EuiDataGridInnerElement'; -export const EuiDataGridBodyVirtualized: FunctionComponent< - EuiDataGridBodyProps -> = ({ - leadingControlColumns, - trailingControlColumns, - columns, - visibleColCount, - schema, - schemaDetectors, - rowCount, - visibleRows: { startRow, endRow, visibleRowCount }, - renderCellValue, - cellContext, - renderCellPopover, - renderFooterCellValue, - interactiveCellId, - pagination, - setVisibleColumns, - switchColumnPos, - onColumnResize, - rowHeightsOptions, - virtualizationOptions, - isFullScreen, - gridStyles, - gridWidth, - gridRef, - gridItemsRendered, - wrapperRef, -}) => { - /** - * Grid refs & observers - */ - const wrapperDimensions = useResizeObserver(wrapperRef.current); - const outerGridRef = useRef(null); // container that becomes scrollable - const innerGridRef = useRef(null); // container sized to fit all content +export const EuiDataGridBodyVirtualized: FunctionComponent = + memo( + ({ + leadingControlColumns, + trailingControlColumns, + columns, + visibleColCount, + schema, + schemaDetectors, + rowCount, + visibleRows: { startRow, endRow, visibleRowCount }, + renderCellValue, + cellContext, + renderCellPopover, + renderFooterCellValue, + interactiveCellId, + pagination, + sorting, + setVisibleColumns, + switchColumnPos, + onColumnResize, + rowHeightsOptions, + virtualizationOptions, + isFullScreen, + gridStyles, + gridWidth, + gridRef, + gridItemsRendered, + wrapperRef, + }) => { + /** + * Grid refs & observers + */ + const wrapperDimensions = useResizeObserver(wrapperRef.current); + const outerGridRef = useRef(null); // container that becomes scrollable + const innerGridRef = useRef(null); // container sized to fit all content - /** - * Scroll bars - */ - const { - scrollBarHeight, - hasVerticalScroll, - hasHorizontalScroll, - scrollBorderOverlay, - } = useScrollBars(outerGridRef, gridStyles.border); + /** + * Scroll bars + */ + const { + scrollBarHeight, + hasVerticalScroll, + hasHorizontalScroll, + scrollBorderOverlay, + } = useScrollBars(outerGridRef, gridStyles.border); - /** - * Widths - */ - const virtualizeContainerWidth = useVirtualizeContainerWidth( - outerGridRef.current, - gridWidth, - pagination?.pageSize - ); + /** + * Widths + */ + const virtualizeContainerWidth = useVirtualizeContainerWidth( + outerGridRef.current, + gridWidth, + pagination?.pageSize + ); - // compute the default column width from the container's width and count of visible columns - const defaultColumnWidth = useDefaultColumnWidth( - virtualizeContainerWidth, - leadingControlColumns, - trailingControlColumns, - columns - ); + // compute the default column width from the container's width and count of visible columns + const defaultColumnWidth = useDefaultColumnWidth( + virtualizeContainerWidth, + leadingControlColumns, + trailingControlColumns, + columns + ); - const { columnWidths, setColumnWidth, getColumnWidth } = useColumnWidths({ - columns, - leadingControlColumns, - trailingControlColumns, - defaultColumnWidth, - onColumnResize, - }); + const { columnWidths, setColumnWidth, getColumnWidth } = useColumnWidths({ + columns, + leadingControlColumns, + trailingControlColumns, + defaultColumnWidth, + onColumnResize, + }); - /** - * Header & footer - */ - const { headerRow, headerRowHeight } = useDataGridHeader({ - switchColumnPos, - setVisibleColumns, - leadingControlColumns, - trailingControlColumns, - columns, - columnWidths, - defaultColumnWidth, - setColumnWidth, - schema, - schemaDetectors, - }); + /** + * Header & footer + */ + const { headerRow, headerRowHeight } = useDataGridHeader({ + leadingControlColumns, + trailingControlColumns, + columns, + columnWidths, + defaultColumnWidth, + setColumnWidth, + setVisibleColumns, + switchColumnPos, + sorting, + schema, + schemaDetectors, + }); - const { footerRow, footerRowHeight } = useDataGridFooter({ - renderFooterCellValue, - renderCellPopover, - rowIndex: visibleRowCount, - visibleRowIndex: visibleRowCount, - interactiveCellId, - leadingControlColumns, - trailingControlColumns, - columns, - columnWidths, - defaultColumnWidth, - schema, - }); + const { footerRow, footerRowHeight } = useDataGridFooter({ + renderFooterCellValue, + renderCellPopover, + rowIndex: visibleRowCount, + visibleRowIndex: visibleRowCount, + interactiveCellId, + leadingControlColumns, + trailingControlColumns, + columns, + columnWidths, + defaultColumnWidth, + schema, + }); - /** - * Handle scrolling cells fully into view - */ - useScroll({ - gridRef, - outerGridRef, - hasGridScrolling: hasVerticalScroll || hasHorizontalScroll, - headerRowHeight, - footerRowHeight, - visibleRowCount, - hasStickyFooter: !!(renderFooterCellValue && gridStyles.stickyFooter), - }); + /** + * Handle scrolling cells fully into view + */ + useScroll({ + gridRef, + outerGridRef, + hasGridScrolling: hasVerticalScroll || hasHorizontalScroll, + headerRowHeight, + footerRowHeight, + visibleRowCount, + hasStickyFooter: !!(renderFooterCellValue && gridStyles.stickyFooter), + }); - /** - * Row manager - */ - const rowManager = useRowManager({ - innerGridRef, - rowClasses: gridStyles.rowClasses, - }); + /** + * Row manager + */ + const rowManager = useRowManager({ + innerGridRef, + rowClasses: gridStyles.rowClasses, + }); - /** - * Heights - */ - const rowHeightUtils = useRowHeightUtils({ - virtualization: { - gridRef, - outerGridElementRef: outerGridRef, - gridItemsRenderedRef: gridItemsRendered, - }, - rowHeightsOptions, - gridStyles, - columns, - }); + /** + * Heights + */ + const rowHeightUtils = useRowHeightUtils({ + virtualization: { + gridRef, + outerGridElementRef: outerGridRef, + gridItemsRenderedRef: gridItemsRendered, + }, + rowHeightsOptions, + gridStyles, + columns, + }); - const { defaultRowHeight, setRowHeight, getRowHeight } = useDefaultRowHeight({ - rowHeightsOptions, - rowHeightUtils, - }); + const { defaultRowHeight, setRowHeight, getRowHeight } = + useDefaultRowHeight({ + rowHeightsOptions, + rowHeightUtils, + }); - const unconstrainedHeight = useUnconstrainedHeight({ - rowHeightUtils, - startRow, - endRow, - rowHeightsOptions, - defaultRowHeight, - headerRowHeight, - footerRowHeight, - scrollBarHeight, - innerGridRef, - }); + const unconstrainedHeight = useUnconstrainedHeight({ + rowHeightUtils, + startRow, + endRow, + rowHeightsOptions, + defaultRowHeight, + headerRowHeight, + footerRowHeight, + scrollBarHeight, + innerGridRef, + }); - /** - * Final grid height & width - */ - const { finalWidth, finalHeight } = useFinalGridDimensions({ - unconstrainedHeight, - unconstrainedWidth: 0, // unable to determine this until the container's size is known - wrapperDimensions, - wrapperRef, - isFullScreen, - rowCount, - }); + /** + * Final grid height & width + */ + const { finalWidth, finalHeight } = useFinalGridDimensions({ + unconstrainedHeight, + unconstrainedWidth: 0, // unable to determine this until the container's size is known + wrapperDimensions, + wrapperRef, + isFullScreen, + rowCount, + }); - /** - * Grid resets - */ - useEffect(() => { - if (gridRef.current) { - gridRef.current.resetAfterColumnIndex(0); - } - }, [gridRef, columns, columnWidths, defaultColumnWidth]); + /** + * Grid resets + */ + useEffect(() => { + if (gridRef.current) { + gridRef.current.resetAfterColumnIndex(0); + } + }, [gridRef, columns, columnWidths, defaultColumnWidth]); - useEffect(() => { - if (gridRef.current && rowHeightsOptions) { - gridRef.current.resetAfterRowIndex(0); - } - }, [ - gridRef, - pagination?.pageIndex, - rowHeightsOptions, - gridStyles?.cellPadding, - gridStyles?.fontSize, - ]); + useEffect(() => { + if (gridRef.current && rowHeightsOptions) { + gridRef.current.resetAfterRowIndex(0); + } + }, [ + gridRef, + pagination?.pageIndex, + rowHeightsOptions, + gridStyles?.cellPadding, + gridStyles?.fontSize, + ]); - useEffect(() => { - if (gridRef.current) { - gridRef.current.resetAfterRowIndex(0); - } - }, [gridRef, getRowHeight]); + useEffect(() => { + if (gridRef.current) { + gridRef.current.resetAfterRowIndex(0); + } + }, [gridRef, getRowHeight]); - return IS_JEST_ENVIRONMENT || finalWidth > 0 ? ( - - { + const onItemsRendered = useCallback( + (itemsRendered: GridOnItemsRenderedProps) => { gridItemsRendered.current = itemsRendered; virtualizationOptions?.onItemsRendered?.(itemsRendered); - }} - innerElementType={InnerElement} - outerRef={outerGridRef} - innerRef={innerGridRef} - columnCount={visibleColCount} - width={finalWidth} - columnWidth={getColumnWidth} - height={finalHeight} - rowHeight={getRowHeight} - itemData={{ + }, + [gridItemsRendered, virtualizationOptions] + ); + + const itemData = useMemo(() => { + return { schemaDetectors, setRowHeight, leadingControlColumns, @@ -334,14 +333,61 @@ export const EuiDataGridBodyVirtualized: FunctionComponent< rowHeightUtils, rowManager, pagination, - }} - rowCount={ - IS_JEST_ENVIRONMENT || headerRowHeight > 0 ? visibleRowCount : 0 - } - > - {_Cell} - - {scrollBorderOverlay} - - ) : null; -}; + headerRowHeight, + }; + }, [ + schemaDetectors, + setRowHeight, + leadingControlColumns, + trailingControlColumns, + columns, + visibleColCount, + schema, + columnWidths, + defaultColumnWidth, + renderCellValue, + cellContext, + renderCellPopover, + interactiveCellId, + rowHeightsOptions, + rowHeightUtils, + rowManager, + pagination, + headerRowHeight, + ]); + + const rowWrapperContextValue = useMemo(() => { + return { headerRowHeight, headerRow, footerRow }; + }, [headerRowHeight, headerRow, footerRow]); + + return IS_JEST_ENVIRONMENT || finalWidth > 0 ? ( + + 0 ? visibleRowCount : 0 + } + > + {Cell} + + {scrollBorderOverlay} + + ) : null; + } + ); +EuiDataGridBodyVirtualized.displayName = 'EuiDataGridBodyVirtualized'; diff --git a/src/components/datagrid/body/data_grid_row_manager.ts b/src/components/datagrid/body/data_grid_row_manager.ts index b8acbde5a15..6b3bd743e50 100644 --- a/src/components/datagrid/body/data_grid_row_manager.ts +++ b/src/components/datagrid/body/data_grid_row_manager.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { useRef, useCallback, RefObject } from 'react'; +import { useRef, useCallback, RefObject, useMemo } from 'react'; import { useUpdateEffect } from '../../../services'; import { EuiDataGridRowManager, EuiDataGridStyle } from '../data_grid_types'; @@ -96,5 +96,7 @@ export const useRowManager = ({ } }, [rowClasses]); - return { getRow }; + return useMemo(() => { + return { getRow }; + }, [getRow]); }; diff --git a/src/components/datagrid/body/footer/use_data_grid_footer.tsx b/src/components/datagrid/body/footer/use_data_grid_footer.tsx index 03873087592..5b8e2e71cf7 100644 --- a/src/components/datagrid/body/footer/use_data_grid_footer.tsx +++ b/src/components/datagrid/body/footer/use_data_grid_footer.tsx @@ -35,7 +35,7 @@ export const useDataGridFooter = (props: Props) => { {...footerProps} /> ); - }, Object.values(props)); // eslint-disable-line react-hooks/exhaustive-deps + }, [props]); return { footerRow, footerRowHeight }; }; diff --git a/src/components/datagrid/body/header/data_grid_control_header_cell.tsx b/src/components/datagrid/body/header/data_grid_control_header_cell.tsx index b765185c04a..f6bc746cbe9 100644 --- a/src/components/datagrid/body/header/data_grid_control_header_cell.tsx +++ b/src/components/datagrid/body/header/data_grid_control_header_cell.tsx @@ -6,36 +6,37 @@ * Side Public License, v 1. */ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, memo } from 'react'; import classNames from 'classnames'; import { EuiDataGridControlHeaderCellProps } from '../../data_grid_types'; import { EuiDataGridHeaderCellWrapper } from './data_grid_header_cell_wrapper'; -export const EuiDataGridControlHeaderCell: FunctionComponent< - EuiDataGridControlHeaderCellProps -> = ({ controlColumn, index }) => { - const { - headerCellRender: HeaderCellRender, - headerCellProps, - width, - id, - } = controlColumn; +export const EuiDataGridControlHeaderCell: FunctionComponent = + memo(({ controlColumn, index }) => { + const { + headerCellRender: HeaderCellRender, + headerCellProps, + width, + id, + } = controlColumn; - return ( - -
- -
-
- ); -}; + return ( + +
+ +
+
+ ); + }); + +EuiDataGridControlHeaderCell.displayName = 'EuiDataGridControlHeaderCell'; diff --git a/src/components/datagrid/body/header/data_grid_header_cell.tsx b/src/components/datagrid/body/header/data_grid_header_cell.tsx index 6e67ce1ac5e..af1d7a9b953 100644 --- a/src/components/datagrid/body/header/data_grid_header_cell.tsx +++ b/src/components/datagrid/body/header/data_grid_header_cell.tsx @@ -10,11 +10,14 @@ import classnames from 'classnames'; import React, { AriaAttributes, FunctionComponent, + PropsWithChildren, + ReactNode, useContext, useState, useRef, useCallback, useMemo, + memo, } from 'react'; import { tabbable, FocusableElement } from 'tabbable'; import { keys } from '../../../../services'; @@ -24,7 +27,6 @@ import { EuiI18n } from '../../../i18n'; import { EuiIcon } from '../../../icon'; import { EuiListGroup } from '../../../list_group'; import { EuiPopover } from '../../../popover'; -import { DataGridSortingContext } from '../../utils/sorting'; import { DataGridFocusContext } from '../../utils/focus'; import { EuiDataGridHeaderCellProps, @@ -35,175 +37,203 @@ import { getColumnActions } from './column_actions'; import { EuiDataGridColumnResizer } from './data_grid_column_resizer'; import { EuiDataGridHeaderCellWrapper } from './data_grid_header_cell_wrapper'; -export const EuiDataGridHeaderCell: FunctionComponent< - EuiDataGridHeaderCellProps -> = ({ - column, - index, - columns, - columnWidths, - schema, - schemaDetectors, - defaultColumnWidth, - setColumnWidth, - setVisibleColumns, - switchColumnPos, -}) => { - const { id, display, displayAsText, displayHeaderCellProps } = column; - const width = columnWidths[id] || defaultColumnWidth; - const columnType = schema[id] ? schema[id].columnType : null; +const CellContent: FunctionComponent< + PropsWithChildren & { title: string; arrow?: ReactNode } +> = ({ children, title, arrow }) => { + return ( + <> +
+ {children} +
+ {arrow} + + ); +}; - const { setFocusedCell, focusFirstVisibleInteractiveCell } = - useContext(DataGridFocusContext); - const { sorting } = useContext(DataGridSortingContext); +export const EuiDataGridHeaderCell: FunctionComponent = + memo( + ({ + index, + column, + columns, + columnWidths, + defaultColumnWidth, + setColumnWidth, + setVisibleColumns, + switchColumnPos, + sorting, + schema, + schemaDetectors, + }) => { + const { id, display, displayAsText, displayHeaderCellProps } = column; + const width = columnWidths[id] || defaultColumnWidth; + const columnType = schema[id] ? schema[id].columnType : null; - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const popoverArrowNavigationProps = usePopoverArrowNavigation(); + const { setFocusedCell, focusFirstVisibleInteractiveCell } = + useContext(DataGridFocusContext); - const columnActions = getColumnActions({ - column, - columns, - schema, - schemaDetectors, - setVisibleColumns, - focusFirstVisibleInteractiveCell, - setIsPopoverOpen, - sorting, - switchColumnPos, - setFocusedCell, - }); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const popoverArrowNavigationProps = usePopoverArrowNavigation(); - const showColumnActions = columnActions && columnActions.length > 0; - const actionsButtonRef = useRef(null); - const focusActionsButton = useCallback(() => { - actionsButtonRef.current?.focus(); - }, []); - const [isActionsButtonFocused, setIsActionsButtonFocused] = useState(false); + const columnActions = useMemo(() => { + return getColumnActions({ + column, + columns, + schema, + schemaDetectors, + setVisibleColumns, + focusFirstVisibleInteractiveCell, + setIsPopoverOpen, + sorting, + switchColumnPos, + setFocusedCell, + }); + }, [ + column, + columns, + schema, + schemaDetectors, + setVisibleColumns, + focusFirstVisibleInteractiveCell, + setIsPopoverOpen, + sorting, + switchColumnPos, + setFocusedCell, + ]); - const { sortingArrow, ariaSort, sortingScreenReaderText } = useSortingUtils({ - sorting, - id, - showColumnActions, - }); - const sortingAriaId = useGeneratedHtmlId({ - prefix: 'euiDataGridCellHeader', - suffix: 'sorting', - }); - const actionsAriaId = useGeneratedHtmlId({ - prefix: 'euiDataGridCellHeader', - suffix: 'actions', - }); + const showColumnActions = columnActions && columnActions.length > 0; + const actionsButtonRef = useRef(null); + const focusActionsButton = useCallback(() => { + actionsButtonRef.current?.focus(); + }, []); + const [isActionsButtonFocused, setIsActionsButtonFocused] = + useState(false); - const cellContent = ( - <> -
- {display || displayAsText || id} -
- {sortingArrow} - - ); + const { sortingArrow, ariaSort, sortingScreenReaderText } = + useSortingUtils({ + sorting, + id, + showColumnActions, + }); + const sortingAriaId = useGeneratedHtmlId({ + prefix: 'euiDataGridCellHeader', + suffix: 'sorting', + }); + const actionsAriaId = useGeneratedHtmlId({ + prefix: 'euiDataGridCellHeader', + suffix: 'actions', + }); - const classes = classnames( - { - [`euiDataGridHeaderCell--${columnType}`]: columnType, - 'euiDataGridHeaderCell--hasColumnActions': showColumnActions, - 'euiDataGridHeaderCell--isActionsPopoverOpen': isPopoverOpen, - }, - displayHeaderCellProps?.className - ); + const classes = classnames( + { + [`euiDataGridHeaderCell--${columnType}`]: columnType, + 'euiDataGridHeaderCell--hasColumnActions': showColumnActions, + 'euiDataGridHeaderCell--isActionsPopoverOpen': isPopoverOpen, + }, + displayHeaderCellProps?.className + ); - return ( - - {column.isResizable !== false && width != null ? ( - - ) : null} + const title = displayAsText || id; + const children = display || displayAsText || id; - {!showColumnActions ? ( - <> - {cellContent} - {sortingScreenReaderText && ( - -

{sortingScreenReaderText}

-
- )} - - ) : ( - <> - + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + {...popoverArrowNavigationProps} + > + + + - - - - )} -
+ + + + )} + + ); + } ); -}; +EuiDataGridHeaderCell.displayName = 'EuiDataGridHeaderCell'; /** * Column sorting utility helpers @@ -227,14 +257,16 @@ export const useSortingUtils = ({ /** * Arrow icon */ - const sortingArrow = isColumnSorted ? ( - - ) : null; + const sortingArrow = useMemo(() => { + return isColumnSorted ? ( + + ) : null; + }, [id, isColumnSorted, sortedColumn]); /** * aria-sort attribute - should only be used when a single column is being sorted diff --git a/src/components/datagrid/body/header/data_grid_header_row.tsx b/src/components/datagrid/body/header/data_grid_header_row.tsx index 8e3aee31e98..83ccce4aa90 100644 --- a/src/components/datagrid/body/header/data_grid_header_row.tsx +++ b/src/components/datagrid/body/header/data_grid_header_row.tsx @@ -7,7 +7,7 @@ */ import classnames from 'classnames'; -import React, { forwardRef } from 'react'; +import React, { forwardRef, memo } from 'react'; import { EuiDataGridControlHeaderCell } from './data_grid_control_header_cell'; import { EuiDataGridHeaderCell } from './data_grid_header_cell'; import { @@ -15,69 +15,70 @@ import { EuiDataGridHeaderRowProps, } from '../../data_grid_types'; -const EuiDataGridHeaderRow = forwardRef< - HTMLDivElement, - EuiDataGridHeaderRowProps ->((props, ref) => { - const { - leadingControlColumns = emptyControlColumns, - trailingControlColumns = emptyControlColumns, - columns, - schema, - schemaDetectors, - columnWidths, - defaultColumnWidth, - className, - setColumnWidth, - setVisibleColumns, - switchColumnPos, - 'data-test-subj': _dataTestSubj, - ...rest - } = props; +const EuiDataGridHeaderRow = memo( + forwardRef((props, ref) => { + const { + className, + 'data-test-subj': _dataTestSubj, + leadingControlColumns = emptyControlColumns, + trailingControlColumns = emptyControlColumns, + columns, + columnWidths, + defaultColumnWidth, + setColumnWidth, + setVisibleColumns, + switchColumnPos, + sorting, + schema, + schemaDetectors, + ...rest + } = props; - const classes = classnames('euiDataGridHeader', className); - const dataTestSubj = classnames('dataGridHeader', _dataTestSubj); + const classes = classnames('euiDataGridHeader', className); + const dataTestSubj = classnames('dataGridHeader', _dataTestSubj); - return ( -
- {leadingControlColumns.map((controlColumn, index) => ( - - ))} - {columns.map((column, index) => ( - - ))} - {trailingControlColumns.map((controlColumn, index) => ( - - ))} -
- ); -}); + return ( +
+ {leadingControlColumns.map((controlColumn, index) => ( + + ))} + {columns.map((column, index) => ( + + ))} + {trailingControlColumns.map((controlColumn, index) => ( + + ))} +
+ ); + }) +); EuiDataGridHeaderRow.displayName = 'EuiDataGridHeaderRow'; diff --git a/src/components/datagrid/body/header/use_data_grid_header.tsx b/src/components/datagrid/body/header/use_data_grid_header.tsx index 529b059028b..597bbff16c8 100644 --- a/src/components/datagrid/body/header/use_data_grid_header.tsx +++ b/src/components/datagrid/body/header/use_data_grid_header.tsx @@ -22,7 +22,9 @@ export const useDataGridHeader = (props: EuiDataGridHeaderRowProps) => { const headerRow = useMemo(() => { return ; - }, Object.values(props)); // eslint-disable-line react-hooks/exhaustive-deps + }, [props]); - return { headerRow, headerRowHeight }; + return useMemo(() => { + return { headerRow, headerRowHeight }; + }, [headerRow, headerRowHeight]); }; diff --git a/src/components/datagrid/controls/__snapshots__/column_sorting.test.tsx.snap b/src/components/datagrid/controls/__snapshots__/column_sorting.test.tsx.snap index cd7eee17d4d..a671818d6bd 100644 --- a/src/components/datagrid/controls/__snapshots__/column_sorting.test.tsx.snap +++ b/src/components/datagrid/controls/__snapshots__/column_sorting.test.tsx.snap @@ -1,811 +1,306 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`useDataGridColumnSorting columnSorting [React 16] renders a toolbar button/popover allowing users to set column sorting 1`] = ` -
- -
-`; - -exports[`useDataGridColumnSorting columnSorting [React 16] renders a toolbar button/popover allowing users to set column sorting 2`] = ` -