UNPKG

@mui/x-data-grid

Version:

The community edition of the data grid component (MUI X).

596 lines (589 loc) 26.3 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose"; const _excluded = ["style"], _excluded2 = ["style"]; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { unstable_useForkRef as useForkRef, unstable_useEnhancedEffect as useEnhancedEffect, unstable_useEventCallback as useEventCallback } from '@mui/utils'; import { useTheme } from '@mui/material/styles'; import { defaultMemoize } from 'reselect'; import { useGridPrivateApiContext } from '../../utils/useGridPrivateApiContext'; import { useGridRootProps } from '../../utils/useGridRootProps'; import { useGridSelector } from '../../utils/useGridSelector'; import { gridVisibleColumnDefinitionsSelector, gridColumnsTotalWidthSelector, gridColumnPositionsSelector } from '../columns/gridColumnsSelector'; import { gridFocusCellSelector, gridTabIndexCellSelector } from '../focus/gridFocusStateSelector'; import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { clamp } from '../../../utils/utils'; import { selectedIdsLookupSelector } from '../rowSelection/gridRowSelectionSelector'; import { gridRowsMetaSelector } from '../rows/gridRowsMetaSelector'; import { getFirstNonSpannedColumnToRender } from '../columns/gridColumnsUtils'; import { getMinimalContentHeight } from '../rows/gridRowsUtils'; import { gridVirtualizationEnabledSelector, gridVirtualizationColumnEnabledSelector } from './gridVirtualizationSelectors'; // Uses binary search to avoid looping through all possible positions import { jsx as _jsx } from "react/jsx-runtime"; export function binarySearch(offset, positions, sliceStart = 0, sliceEnd = positions.length) { if (positions.length <= 0) { return -1; } if (sliceStart >= sliceEnd) { return sliceStart; } const pivot = sliceStart + Math.floor((sliceEnd - sliceStart) / 2); const itemOffset = positions[pivot]; return offset <= itemOffset ? binarySearch(offset, positions, sliceStart, pivot) : binarySearch(offset, positions, pivot + 1, sliceEnd); } function exponentialSearch(offset, positions, index) { let interval = 1; while (index < positions.length && Math.abs(positions[index]) < offset) { index += interval; interval *= 2; } return binarySearch(offset, positions, Math.floor(index / 2), Math.min(index, positions.length)); } export const getRenderableIndexes = ({ firstIndex, lastIndex, buffer, minFirstIndex, maxLastIndex }) => { return [clamp(firstIndex - buffer, minFirstIndex, maxLastIndex), clamp(lastIndex + buffer, minFirstIndex, maxLastIndex)]; }; export const areRenderContextsEqual = (context1, context2) => { if (context1 === context2) { return true; } return context1.firstRowIndex === context2.firstRowIndex && context1.lastRowIndex === context2.lastRowIndex && context1.firstColumnIndex === context2.firstColumnIndex && context1.lastColumnIndex === context2.lastColumnIndex; }; // The `maxSize` is 3 so that reselect caches the `renderedColumns` values for the pinned left, // unpinned, and pinned right sections. const MEMOIZE_OPTIONS = { maxSize: 3 }; export const useGridVirtualScroller = props => { const apiRef = useGridPrivateApiContext(); const rootProps = useGridRootProps(); const visibleColumns = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector); const enabled = useGridSelector(apiRef, gridVirtualizationEnabledSelector); const enabledForColumns = useGridSelector(apiRef, gridVirtualizationColumnEnabledSelector); const { ref, onRenderZonePositioning, renderZoneMinColumnIndex = 0, renderZoneMaxColumnIndex = visibleColumns.length, getRowProps } = props; const theme = useTheme(); const columnPositions = useGridSelector(apiRef, gridColumnPositionsSelector); const columnsTotalWidth = useGridSelector(apiRef, gridColumnsTotalWidthSelector); const cellFocus = useGridSelector(apiRef, gridFocusCellSelector); const cellTabIndex = useGridSelector(apiRef, gridTabIndexCellSelector); const rowsMeta = useGridSelector(apiRef, gridRowsMetaSelector); const selectedRowsLookup = useGridSelector(apiRef, selectedIdsLookupSelector); const currentPage = useGridVisibleRows(apiRef, rootProps); const renderZoneRef = React.useRef(null); const rootRef = React.useRef(null); const handleRef = useForkRef(ref, rootRef); const [renderContext, setRenderContextState] = React.useState(null); const prevRenderContext = React.useRef(renderContext); const scrollPosition = React.useRef({ top: 0, left: 0 }); const [containerDimensions, setContainerDimensions] = React.useState({ width: null, height: null }); const prevTotalWidth = React.useRef(columnsTotalWidth); // Each visible row (not to be confused with a filter result) is composed of a central row element // and up to two additional row elements for pinned columns (left and right). // When hovering any of these elements, the :hover styles are applied only to the row element that // was actually hovered, not its additional siblings. To make it look like a contiguous row, // we add/remove the .Mui-hovered class to all of the row elements inside one visible row. const [hoveredRowId, setHoveredRowId] = React.useState(null); const rowStyleCache = React.useRef(Object.create(null)); const prevGetRowProps = React.useRef(); const prevRootRowStyle = React.useRef(); const getRenderedColumnsRef = React.useRef(defaultMemoize((columns, firstColumnToRender, lastColumnToRender, minFirstColumn, maxLastColumn, indexOfColumnWithFocusedCell) => { // If the selected column is not within the current range of columns being displayed, // we need to render it at either the left or right of the columns, // depending on whether it is above or below the range. let focusedCellColumnIndexNotInRange; const renderedColumns = columns.slice(firstColumnToRender, lastColumnToRender); if (indexOfColumnWithFocusedCell > -1) { // check if it is not on the left pinned column. if (firstColumnToRender > indexOfColumnWithFocusedCell && indexOfColumnWithFocusedCell >= minFirstColumn) { focusedCellColumnIndexNotInRange = indexOfColumnWithFocusedCell; } // check if it is not on the right pinned column. else if (lastColumnToRender < indexOfColumnWithFocusedCell && indexOfColumnWithFocusedCell < maxLastColumn) { focusedCellColumnIndexNotInRange = indexOfColumnWithFocusedCell; } } return { focusedCellColumnIndexNotInRange, renderedColumns }; }, MEMOIZE_OPTIONS)); const indexOfColumnWithFocusedCell = React.useMemo(() => { if (cellFocus !== null) { return visibleColumns.findIndex(column => column.field === cellFocus.field); } return -1; }, [cellFocus, visibleColumns]); const getNearestIndexToRender = React.useCallback(offset => { const lastMeasuredIndexRelativeToAllRows = apiRef.current.getLastMeasuredRowIndex(); let allRowsMeasured = lastMeasuredIndexRelativeToAllRows === Infinity; if (currentPage.range?.lastRowIndex && !allRowsMeasured) { // Check if all rows in this page are already measured allRowsMeasured = lastMeasuredIndexRelativeToAllRows >= currentPage.range.lastRowIndex; } const lastMeasuredIndexRelativeToCurrentPage = clamp(lastMeasuredIndexRelativeToAllRows - (currentPage.range?.firstRowIndex || 0), 0, rowsMeta.positions.length); if (allRowsMeasured || rowsMeta.positions[lastMeasuredIndexRelativeToCurrentPage] >= offset) { // If all rows were measured (when no row has "auto" as height) or all rows before the offset // were measured, then use a binary search because it's faster. return binarySearch(offset, rowsMeta.positions); } // Otherwise, use an exponential search. // If rows have "auto" as height, their positions will be based on estimated heights. // In this case, we can skip several steps until we find a position higher than the offset. // Inspired by https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/utils/CellSizeAndPositionManager.js return exponentialSearch(offset, rowsMeta.positions, lastMeasuredIndexRelativeToCurrentPage); }, [apiRef, currentPage.range?.firstRowIndex, currentPage.range?.lastRowIndex, rowsMeta.positions]); const computeRenderContext = React.useCallback(() => { if (!enabled) { return { firstRowIndex: 0, lastRowIndex: currentPage.rows.length, firstColumnIndex: 0, lastColumnIndex: visibleColumns.length }; } const { top, left } = scrollPosition.current; // Clamp the value because the search may return an index out of bounds. // In the last index, this is not needed because Array.slice doesn't include it. const firstRowIndex = Math.min(getNearestIndexToRender(top), rowsMeta.positions.length - 1); const lastRowIndex = rootProps.autoHeight ? firstRowIndex + currentPage.rows.length : getNearestIndexToRender(top + containerDimensions.height); let firstColumnIndex = 0; let lastColumnIndex = columnPositions.length; if (enabledForColumns) { let hasRowWithAutoHeight = false; const [firstRowToRender, lastRowToRender] = getRenderableIndexes({ firstIndex: firstRowIndex, lastIndex: lastRowIndex, minFirstIndex: 0, maxLastIndex: currentPage.rows.length, buffer: rootProps.rowBuffer }); for (let i = firstRowToRender; i < lastRowToRender && !hasRowWithAutoHeight; i += 1) { const row = currentPage.rows[i]; hasRowWithAutoHeight = apiRef.current.rowHasAutoHeight(row.id); } if (!hasRowWithAutoHeight) { firstColumnIndex = binarySearch(Math.abs(left), columnPositions); lastColumnIndex = binarySearch(Math.abs(left) + containerDimensions.width, columnPositions); } } return { firstRowIndex, lastRowIndex, firstColumnIndex, lastColumnIndex }; }, [enabled, enabledForColumns, getNearestIndexToRender, rowsMeta.positions.length, rootProps.autoHeight, rootProps.rowBuffer, currentPage.rows, columnPositions, visibleColumns.length, apiRef, containerDimensions]); useEnhancedEffect(() => { if (enabled) { // TODO a scroll reset should not be necessary rootRef.current.scrollLeft = 0; rootRef.current.scrollTop = 0; } else { renderZoneRef.current.style.transform = `translate3d(0px, 0px, 0px)`; } }, [enabled]); useEnhancedEffect(() => { setContainerDimensions({ width: rootRef.current.clientWidth, height: rootRef.current.clientHeight }); }, [rowsMeta.currentPageTotalHeight]); const handleResize = React.useCallback(() => { if (rootRef.current) { setContainerDimensions({ width: rootRef.current.clientWidth, height: rootRef.current.clientHeight }); } }, []); useGridApiEventHandler(apiRef, 'debouncedResize', handleResize); const updateRenderZonePosition = React.useCallback(nextRenderContext => { const [firstRowToRender, lastRowToRender] = getRenderableIndexes({ firstIndex: nextRenderContext.firstRowIndex, lastIndex: nextRenderContext.lastRowIndex, minFirstIndex: 0, maxLastIndex: currentPage.rows.length, buffer: rootProps.rowBuffer }); const [initialFirstColumnToRender] = getRenderableIndexes({ firstIndex: nextRenderContext.firstColumnIndex, lastIndex: nextRenderContext.lastColumnIndex, minFirstIndex: renderZoneMinColumnIndex, maxLastIndex: renderZoneMaxColumnIndex, buffer: rootProps.columnBuffer }); const firstColumnToRender = getFirstNonSpannedColumnToRender({ firstColumnToRender: initialFirstColumnToRender, apiRef, firstRowToRender, lastRowToRender, visibleRows: currentPage.rows }); const direction = theme.direction === 'ltr' ? 1 : -1; const top = gridRowsMetaSelector(apiRef.current.state).positions[firstRowToRender]; const left = direction * gridColumnPositionsSelector(apiRef)[firstColumnToRender]; // Call directly the selector because it might be outdated when this method is called renderZoneRef.current.style.transform = `translate3d(${left}px, ${top}px, 0px)`; if (typeof onRenderZonePositioning === 'function') { onRenderZonePositioning({ top, left }); } }, [apiRef, currentPage.rows, onRenderZonePositioning, renderZoneMinColumnIndex, renderZoneMaxColumnIndex, rootProps.columnBuffer, rootProps.rowBuffer, theme.direction]); const getRenderContext = React.useCallback(() => prevRenderContext.current, []); const setRenderContext = React.useCallback(nextRenderContext => { if (prevRenderContext.current && areRenderContextsEqual(nextRenderContext, prevRenderContext.current)) { updateRenderZonePosition(nextRenderContext); return; } setRenderContextState(nextRenderContext); updateRenderZonePosition(nextRenderContext); const [firstRowToRender, lastRowToRender] = getRenderableIndexes({ firstIndex: nextRenderContext.firstRowIndex, lastIndex: nextRenderContext.lastRowIndex, minFirstIndex: 0, maxLastIndex: currentPage.rows.length, buffer: rootProps.rowBuffer }); apiRef.current.publishEvent('renderedRowsIntervalChange', { firstRowToRender, lastRowToRender }); prevRenderContext.current = nextRenderContext; }, [apiRef, setRenderContextState, prevRenderContext, currentPage.rows.length, rootProps.rowBuffer, updateRenderZonePosition]); useEnhancedEffect(() => { if (containerDimensions.width == null) { return; } const initialRenderContext = computeRenderContext(); setRenderContext(initialRenderContext); const { top, left } = scrollPosition.current; const params = { top, left, renderContext: initialRenderContext }; apiRef.current.publishEvent('scrollPositionChange', params); }, [apiRef, computeRenderContext, containerDimensions.width, setRenderContext]); const handleScroll = useEventCallback(event => { const { scrollTop, scrollLeft } = event.currentTarget; scrollPosition.current.top = scrollTop; scrollPosition.current.left = scrollLeft; // On iOS and macOS, negative offsets are possible when swiping past the start if (!prevRenderContext.current || scrollTop < 0) { return; } if (theme.direction === 'ltr') { if (scrollLeft < 0) { return; } } if (theme.direction === 'rtl') { if (scrollLeft > 0) { return; } } // When virtualization is disabled, the context never changes during scroll const nextRenderContext = enabled ? computeRenderContext() : prevRenderContext.current; const topRowsScrolledSincePreviousRender = Math.abs(nextRenderContext.firstRowIndex - prevRenderContext.current.firstRowIndex); const bottomRowsScrolledSincePreviousRender = Math.abs(nextRenderContext.lastRowIndex - prevRenderContext.current.lastRowIndex); const topColumnsScrolledSincePreviousRender = Math.abs(nextRenderContext.firstColumnIndex - prevRenderContext.current.firstColumnIndex); const bottomColumnsScrolledSincePreviousRender = Math.abs(nextRenderContext.lastColumnIndex - prevRenderContext.current.lastColumnIndex); const shouldSetState = topRowsScrolledSincePreviousRender >= rootProps.rowThreshold || bottomRowsScrolledSincePreviousRender >= rootProps.rowThreshold || topColumnsScrolledSincePreviousRender >= rootProps.columnThreshold || bottomColumnsScrolledSincePreviousRender >= rootProps.columnThreshold || prevTotalWidth.current !== columnsTotalWidth; apiRef.current.publishEvent('scrollPositionChange', { top: scrollTop, left: scrollLeft, renderContext: shouldSetState ? nextRenderContext : prevRenderContext.current }, event); if (shouldSetState) { // Prevents batching render context changes ReactDOM.flushSync(() => { setRenderContext(nextRenderContext); }); prevTotalWidth.current = columnsTotalWidth; } }); const handleWheel = useEventCallback(event => { apiRef.current.publishEvent('virtualScrollerWheel', {}, event); }); const handleTouchMove = useEventCallback(event => { apiRef.current.publishEvent('virtualScrollerTouchMove', {}, event); }); const indexOfRowWithFocusedCell = React.useMemo(() => { if (cellFocus !== null) { return currentPage.rows.findIndex(row => row.id === cellFocus.id); } return -1; }, [cellFocus, currentPage.rows]); useGridApiEventHandler(apiRef, 'rowMouseOver', (params, event) => { if (event.currentTarget.contains(event.relatedTarget)) { return; } setHoveredRowId(params.id ?? null); }); useGridApiEventHandler(apiRef, 'rowMouseOut', (params, event) => { if (event.currentTarget.contains(event.relatedTarget)) { return; } setHoveredRowId(null); }); const getRows = (params = { renderContext }) => { const { onRowRender, renderContext: nextRenderContext, minFirstColumn = renderZoneMinColumnIndex, maxLastColumn = renderZoneMaxColumnIndex, availableSpace = containerDimensions.width, rowIndexOffset = 0, position = 'center' } = params; if (!nextRenderContext || availableSpace == null) { return null; } const rowBuffer = enabled ? rootProps.rowBuffer : 0; const columnBuffer = enabled ? rootProps.columnBuffer : 0; const [firstRowToRender, lastRowToRender] = getRenderableIndexes({ firstIndex: nextRenderContext.firstRowIndex, lastIndex: nextRenderContext.lastRowIndex, minFirstIndex: 0, maxLastIndex: currentPage.rows.length, buffer: rowBuffer }); const renderedRows = []; if (params.rows) { params.rows.forEach(row => { renderedRows.push(row); apiRef.current.calculateColSpan({ rowId: row.id, minFirstColumn, maxLastColumn, columns: visibleColumns }); }); } else { if (!currentPage.range) { return null; } for (let i = firstRowToRender; i < lastRowToRender; i += 1) { const row = currentPage.rows[i]; renderedRows.push(row); apiRef.current.calculateColSpan({ rowId: row.id, minFirstColumn, maxLastColumn, columns: visibleColumns }); } } // If the selected row is not within the current range of rows being displayed, // we need to render it at either the top or bottom of the rows, // depending on whether it is above or below the range. let isRowWithFocusedCellNotInRange = false; if (indexOfRowWithFocusedCell > -1) { const rowWithFocusedCell = currentPage.rows[indexOfRowWithFocusedCell]; if (firstRowToRender > indexOfRowWithFocusedCell || lastRowToRender < indexOfRowWithFocusedCell) { isRowWithFocusedCellNotInRange = true; if (indexOfRowWithFocusedCell > firstRowToRender) { renderedRows.push(rowWithFocusedCell); } else { renderedRows.unshift(rowWithFocusedCell); } apiRef.current.calculateColSpan({ rowId: rowWithFocusedCell.id, minFirstColumn, maxLastColumn, columns: visibleColumns }); } } const [initialFirstColumnToRender, lastColumnToRender] = getRenderableIndexes({ firstIndex: nextRenderContext.firstColumnIndex, lastIndex: nextRenderContext.lastColumnIndex, minFirstIndex: minFirstColumn, maxLastIndex: maxLastColumn, buffer: columnBuffer }); const firstColumnToRender = getFirstNonSpannedColumnToRender({ firstColumnToRender: initialFirstColumnToRender, apiRef, firstRowToRender, lastRowToRender, visibleRows: currentPage.rows }); let isColumnWihFocusedCellNotInRange = false; if (firstColumnToRender > indexOfColumnWithFocusedCell || lastColumnToRender < indexOfColumnWithFocusedCell) { isColumnWihFocusedCellNotInRange = true; } const { focusedCellColumnIndexNotInRange, renderedColumns } = getRenderedColumnsRef.current(visibleColumns, firstColumnToRender, lastColumnToRender, minFirstColumn, maxLastColumn, isColumnWihFocusedCellNotInRange ? indexOfColumnWithFocusedCell : -1); const _ref = rootProps.slotProps?.row || {}, { style: rootRowStyle } = _ref, rootRowProps = _objectWithoutPropertiesLoose(_ref, _excluded); const invalidatesCachedRowStyle = prevGetRowProps.current !== getRowProps || prevRootRowStyle.current !== rootRowStyle; if (invalidatesCachedRowStyle) { rowStyleCache.current = Object.create(null); } const rows = []; let isRowWithFocusedCellRendered = false; for (let i = 0; i < renderedRows.length; i += 1) { const { id, model } = renderedRows[i]; const isRowNotVisible = isRowWithFocusedCellNotInRange && cellFocus.id === id; const lastVisibleRowIndex = isRowWithFocusedCellNotInRange ? firstRowToRender + i === currentPage.rows.length : firstRowToRender + i === currentPage.rows.length - 1; const baseRowHeight = !apiRef.current.rowHasAutoHeight(id) ? apiRef.current.unstable_getRowHeight(id) : 'auto'; let isSelected; if (selectedRowsLookup[id] == null) { isSelected = false; } else { isSelected = apiRef.current.isRowSelectable(id); } if (onRowRender) { onRowRender(id); } const focusedCell = cellFocus !== null && cellFocus.id === id ? cellFocus.field : null; const columnWithFocusedCellNotInRange = focusedCellColumnIndexNotInRange !== undefined && visibleColumns[focusedCellColumnIndexNotInRange]; const renderedColumnsWithFocusedCell = columnWithFocusedCellNotInRange && focusedCell ? [columnWithFocusedCellNotInRange, ...renderedColumns] : renderedColumns; let tabbableCell = null; if (cellTabIndex !== null && cellTabIndex.id === id) { const cellParams = apiRef.current.getCellParams(id, cellTabIndex.field); tabbableCell = cellParams.cellMode === 'view' ? cellTabIndex.field : null; } const _ref2 = typeof getRowProps === 'function' && getRowProps(id, model) || {}, { style: rowStyle } = _ref2, rowProps = _objectWithoutPropertiesLoose(_ref2, _excluded2); if (!rowStyleCache.current[id]) { const style = _extends({}, rowStyle, rootRowStyle); rowStyleCache.current[id] = style; } let index = rowIndexOffset + (currentPage?.range?.firstRowIndex || 0) + firstRowToRender + i; if (isRowWithFocusedCellNotInRange && cellFocus?.id === id) { index = indexOfRowWithFocusedCell; isRowWithFocusedCellRendered = true; } else if (isRowWithFocusedCellRendered) { index -= 1; } rows.push( /*#__PURE__*/_jsx(rootProps.slots.row, _extends({ row: model, rowId: id, focusedCellColumnIndexNotInRange: focusedCellColumnIndexNotInRange, isNotVisible: isRowNotVisible, rowHeight: baseRowHeight, focusedCell: focusedCell, tabbableCell: tabbableCell, renderedColumns: renderedColumnsWithFocusedCell, visibleColumns: visibleColumns, firstColumnToRender: firstColumnToRender, lastColumnToRender: lastColumnToRender, selected: isSelected, index: index, containerWidth: availableSpace, isLastVisible: lastVisibleRowIndex, position: position }, rowProps, rootRowProps, { hovered: hoveredRowId === id, style: rowStyleCache.current[id] }), id)); } prevGetRowProps.current = getRowProps; prevRootRowStyle.current = rootRowStyle; return rows; }; const needsHorizontalScrollbar = containerDimensions.width && columnsTotalWidth >= containerDimensions.width; const contentSize = React.useMemo(() => { // In cases where the columns exceed the available width, // the horizontal scrollbar should be shown even when there're no rows. // Keeping 1px as minimum height ensures that the scrollbar will visible if necessary. const height = Math.max(rowsMeta.currentPageTotalHeight, 1); let shouldExtendContent = false; if (rootRef?.current && height <= rootRef?.current.clientHeight) { shouldExtendContent = true; } const size = { width: needsHorizontalScrollbar ? columnsTotalWidth : 'auto', height, minHeight: shouldExtendContent ? '100%' : 'auto' }; if (rootProps.autoHeight && currentPage.rows.length === 0) { size.height = getMinimalContentHeight(apiRef, rootProps.rowHeight); // Give room to show the overlay when there no rows. } return size; }, [apiRef, rootRef, columnsTotalWidth, rowsMeta.currentPageTotalHeight, needsHorizontalScrollbar, rootProps.autoHeight, rootProps.rowHeight, currentPage.rows.length]); React.useEffect(() => { apiRef.current.publishEvent('virtualScrollerContentSizeChange'); }, [apiRef, contentSize]); const rootStyle = React.useMemo(() => { const style = {}; if (!needsHorizontalScrollbar) { style.overflowX = 'hidden'; } if (rootProps.autoHeight) { style.overflowY = 'hidden'; } return style; }, [needsHorizontalScrollbar, rootProps.autoHeight]); apiRef.current.register('private', { getRenderContext }); return { renderContext, updateRenderZonePosition, getRows, getRootProps: (inputProps = {}) => _extends({ ref: handleRef, onScroll: handleScroll, onWheel: handleWheel, onTouchMove: handleTouchMove }, inputProps, { style: inputProps.style ? _extends({}, inputProps.style, rootStyle) : rootStyle, role: 'presentation' }), getContentProps: ({ style } = {}) => ({ style: style ? _extends({}, style, contentSize) : contentSize, role: 'presentation' }), getRenderZoneProps: () => ({ ref: renderZoneRef, role: 'rowgroup' }) }; };