From 5d31a72e1905d8379f9a0076bb8a5410e9d66f55 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 6 Aug 2024 16:12:47 +0500 Subject: [PATCH 01/47] [Data Grid] Row spanning POC --- .../data-grid/row-spanning/RowSpanning.js | 67 +++++++++++++++ .../data-grid/row-spanning/RowSpanning.tsx | 67 +++++++++++++++ .../data-grid/row-spanning/row-spanning.md | 12 +-- .../src/DataGrid/useDataGridComponent.tsx | 6 ++ .../src/DataGrid/useDataGridProps.ts | 1 + .../src/components/cell/GridCell.tsx | 28 ++++++- .../features/rows/gridRowSpanningSelectors.ts | 14 ++++ .../hooks/features/rows/useGridRowSpanning.ts | 84 +++++++++++++++++++ .../src/models/gridStateCommunity.ts | 2 + .../src/models/props/DataGridProps.ts | 5 ++ 10 files changed, 277 insertions(+), 9 deletions(-) create mode 100644 docs/data/data-grid/row-spanning/RowSpanning.js create mode 100644 docs/data/data-grid/row-spanning/RowSpanning.tsx create mode 100644 packages/x-data-grid/src/hooks/features/rows/gridRowSpanningSelectors.ts create mode 100644 packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts diff --git a/docs/data/data-grid/row-spanning/RowSpanning.js b/docs/data/data-grid/row-spanning/RowSpanning.js new file mode 100644 index 000000000000..b20f3ca5c7e2 --- /dev/null +++ b/docs/data/data-grid/row-spanning/RowSpanning.js @@ -0,0 +1,67 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { DataGrid } from '@mui/x-data-grid'; + +const columns = [ + { + field: 'event', + headerName: 'Event', + width: 200, + editable: true, + }, + { + field: 'indicator', + headerName: 'Indicator', + width: 150, + editable: true, + }, + { + field: 'action', + headerName: 'Action', + width: 150, + editable: true, + }, +]; + +const rows = [ + { id: 1, event: 'Event 1', indicator: 'Indicator 1', action: 'Actions 1' }, + { id: 2, event: 'Event 1', indicator: 'Indicator 1', action: 'Actions 2' }, + { id: 3, event: 'Event 1', indicator: 'Indicator 1', action: 'Actions 3' }, + { id: 4, event: 'Event 1', indicator: 'Indicator 2', action: 'Actions 1' }, + { id: 5, event: 'Event 1', indicator: 'Indicator 2', action: 'Actions 2' }, + { id: 6, event: 'Event 2', indicator: 'Indicator 1', action: 'Actions 1' }, + { id: 7, event: 'Event 2', indicator: 'Indicator 1', action: 'Actions 2' }, + { id: 8, event: 'Event 2', indicator: 'Indicator 1', action: 'Actions 3' }, + { id: 9, event: 'Event 2', indicator: 'Indicator 2', action: 'Actions 1' }, + { id: 10, event: 'Event 2', indicator: 'Indicator 2', action: 'Actions 2' }, +]; + +export default function RowSpanning() { + return ( + + + + ); +} diff --git a/docs/data/data-grid/row-spanning/RowSpanning.tsx b/docs/data/data-grid/row-spanning/RowSpanning.tsx new file mode 100644 index 000000000000..449ce6482d75 --- /dev/null +++ b/docs/data/data-grid/row-spanning/RowSpanning.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; + +const columns: GridColDef<(typeof rows)[number]>[] = [ + { + field: 'event', + headerName: 'Event', + width: 200, + editable: true, + }, + { + field: 'indicator', + headerName: 'Indicator', + width: 150, + editable: true, + }, + { + field: 'action', + headerName: 'Action', + width: 150, + editable: true, + }, +]; + +const rows = [ + { id: 1, event: 'Event 1', indicator: 'Indicator 1', action: 'Actions 1' }, + { id: 2, event: 'Event 1', indicator: 'Indicator 1', action: 'Actions 2' }, + { id: 3, event: 'Event 1', indicator: 'Indicator 1', action: 'Actions 3' }, + { id: 4, event: 'Event 1', indicator: 'Indicator 2', action: 'Actions 1' }, + { id: 5, event: 'Event 1', indicator: 'Indicator 2', action: 'Actions 2' }, + { id: 6, event: 'Event 2', indicator: 'Indicator 1', action: 'Actions 1' }, + { id: 7, event: 'Event 2', indicator: 'Indicator 1', action: 'Actions 2' }, + { id: 8, event: 'Event 2', indicator: 'Indicator 1', action: 'Actions 3' }, + { id: 9, event: 'Event 2', indicator: 'Indicator 2', action: 'Actions 1' }, + { id: 10, event: 'Event 2', indicator: 'Indicator 2', action: 'Actions 2' }, +]; + +export default function RowSpanning() { + return ( + + + + ); +} diff --git a/docs/data/data-grid/row-spanning/row-spanning.md b/docs/data/data-grid/row-spanning/row-spanning.md index 7640695d86d6..19a41594c151 100644 --- a/docs/data/data-grid/row-spanning/row-spanning.md +++ b/docs/data/data-grid/row-spanning/row-spanning.md @@ -2,18 +2,14 @@

Span cells across several columns.

-:::warning -This feature isn't implemented yet. It's coming. - -πŸ‘ Upvote [issue #207](https://github.com/mui/mui-x/issues/207) if you want to see it land faster. - -Don't hesitate to leave a comment on the same issue to influence what gets built. Especially if you already have a use case for this component, or if you are facing a pain point with your current solution. -::: - Each cell takes up the width of one row. Row spanning lets you change this default behavior, so cells can span multiple rows. This is very close to the "row spanning" in an HTML ``. +To enable, pass the `unstable_rowSpanning` prop to the Data Grid. + +{{"demo": "RowSpanning.js", "bg": "inline", "defaultCodeOpen": false}} + ## API - [DataGrid](/x/api/data-grid/data-grid/) diff --git a/packages/x-data-grid/src/DataGrid/useDataGridComponent.tsx b/packages/x-data-grid/src/DataGrid/useDataGridComponent.tsx index 42b9d2dde827..858525d2d19e 100644 --- a/packages/x-data-grid/src/DataGrid/useDataGridComponent.tsx +++ b/packages/x-data-grid/src/DataGrid/useDataGridComponent.tsx @@ -53,6 +53,10 @@ import { columnResizeStateInitializer, useGridColumnResize, } from '../hooks/features/columnResize/useGridColumnResize'; +import { + rowSpanningStateInitializer, + useGridRowSpanning, +} from '../hooks/features/rows/useGridRowSpanning'; export const useDataGridComponent = ( inputApiRef: React.MutableRefObject | undefined, @@ -76,6 +80,7 @@ export const useDataGridComponent = ( useGridInitializeState(rowSelectionStateInitializer, apiRef, props); useGridInitializeState(columnsStateInitializer, apiRef, props); useGridInitializeState(rowsStateInitializer, apiRef, props); + useGridInitializeState(rowSpanningStateInitializer, apiRef, props); useGridInitializeState(editingStateInitializer, apiRef, props); useGridInitializeState(focusStateInitializer, apiRef, props); useGridInitializeState(sortingStateInitializer, apiRef, props); @@ -93,6 +98,7 @@ export const useDataGridComponent = ( useGridRowSelection(apiRef, props); useGridColumns(apiRef, props); useGridRows(apiRef, props); + useGridRowSpanning(apiRef, props); useGridParamsApi(apiRef); useGridColumnSpanning(apiRef); useGridColumnGrouping(apiRef, props); diff --git a/packages/x-data-grid/src/DataGrid/useDataGridProps.ts b/packages/x-data-grid/src/DataGrid/useDataGridProps.ts index 4e29d17e4f7b..a5a59d8a7796 100644 --- a/packages/x-data-grid/src/DataGrid/useDataGridProps.ts +++ b/packages/x-data-grid/src/DataGrid/useDataGridProps.ts @@ -78,6 +78,7 @@ export const DATA_GRID_PROPS_DEFAULT_VALUES: DataGridPropsWithDefaultValues = { sortingMode: 'client', sortingOrder: ['asc' as const, 'desc' as const, null], throttleRowsMs: 0, + unstable_rowSpanning: false, }; const defaultSlots = DATA_GRID_DEFAULT_SLOTS_COMPONENTS; diff --git a/packages/x-data-grid/src/components/cell/GridCell.tsx b/packages/x-data-grid/src/components/cell/GridCell.tsx index 2a196dccb5a7..17008e80fcf2 100644 --- a/packages/x-data-grid/src/components/cell/GridCell.tsx +++ b/packages/x-data-grid/src/components/cell/GridCell.tsx @@ -34,6 +34,10 @@ import { MissingRowIdError } from '../../hooks/features/rows/useGridParamsApi'; import type { DataGridProcessedProps } from '../../models/props/DataGridProps'; import { shouldCellShowLeftBorder, shouldCellShowRightBorder } from '../../utils/cellBorderUtils'; import { GridPinnedColumnPosition } from '../../hooks/features/columns/gridColumnsInterfaces'; +import { + gridRowSpanningHiddenCellsSelector, + gridRowSpanningSpannedCellsSelector, +} from '../../hooks/features/rows/gridRowSpanningSelectors'; export enum PinnedPosition { NONE, @@ -373,6 +377,9 @@ const GridCell = React.forwardRef(function GridCe } }, [hasFocus, cellMode, apiRef]); + const hiddenCells = useGridSelector(apiRef, gridRowSpanningHiddenCellsSelector); + const spannedCells = useGridSelector(apiRef, gridRowSpanningSpannedCellsSelector); + if (cellParamsWithAPI === EMPTY_CELL_PARAMS) { return null; } @@ -453,6 +460,12 @@ const GridCell = React.forwardRef(function GridCe onDragOver: publish('cellDragOver', onDragOver), }; + const isHidden = hiddenCells[rowId]?.[field] ?? false; + if (isHidden) { + return
; + } + const rowSpan = spannedCells[rowId]?.[field] ?? 1; + return (
(function GridCe data-colindex={colIndex} aria-colindex={colIndex + 1} aria-colspan={colSpan} - style={style} + aria-rowspan={rowSpan} + style={ + rowSpan === 1 + ? style + : { + ...style, + height: `calc(var(--height) * ${rowSpan})`, + background: 'white', + zIndex: 5, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + } + } title={title} tabIndex={tabIndex} onClick={publish('cellClick', onClick)} diff --git a/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningSelectors.ts b/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningSelectors.ts new file mode 100644 index 000000000000..67ac552b26e8 --- /dev/null +++ b/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningSelectors.ts @@ -0,0 +1,14 @@ +import { createSelector } from '../../../utils/createSelector'; +import { GridStateCommunity } from '../../../models/gridStateCommunity'; + +const gridRowSpanningStateSelector = (state: GridStateCommunity) => state.rowSpanning; + +export const gridRowSpanningHiddenCellsSelector = createSelector( + gridRowSpanningStateSelector, + (rowSpanning) => rowSpanning.hiddenCells, +); + +export const gridRowSpanningSpannedCellsSelector = createSelector( + gridRowSpanningStateSelector, + (rowSpanning) => rowSpanning.spannedCells, +); diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts new file mode 100644 index 000000000000..ab0d5919e2ba --- /dev/null +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -0,0 +1,84 @@ +import * as React from 'react'; +import { GridEventListener } from '../../../models/events'; +import { GridColDef } from '../../../models/colDef'; +import { GridRowId } from '../../../models/gridRows'; +import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; +import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; +import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; +import { GridStateInitializer } from '../../utils/useGridInitializeState'; +import { gridSortedRowIdsSelector } from '../sorting/gridSortingSelector'; +import { gridColumnDefinitionsSelector } from '../columns/gridColumnsSelector'; + +export interface GridRowSpanningState { + spannedCells: Record>; + hiddenCells: Record>; +} + +const EMPTY_STATE = { spannedCells: {}, hiddenCells: {} }; + +export const rowSpanningStateInitializer: GridStateInitializer = (state) => { + return { + ...state, + rowSpanning: EMPTY_STATE, + }; +}; + +export const useGridRowSpanning = ( + apiRef: React.MutableRefObject, + props: Pick, +): void => { + const handleSortedRowsSet = React.useCallback>(() => { + if (!props.unstable_rowSpanning) { + return; + } + const spannedCells: Record> = {}; + const hiddenCells: Record> = {}; + // only span `string` columns for POC + const filteredSortedRowIds = gridSortedRowIdsSelector(apiRef); + const colDefs = gridColumnDefinitionsSelector(apiRef); + colDefs.forEach((colDef) => { + if (colDef.type !== 'string') { + return; + } + // TODO Perf: Process rendered rows first and lazily process the rest + filteredSortedRowIds.forEach((rowId, index) => { + const cellValue = apiRef.current.getRow(rowId)[colDef.field]; + if (cellValue === undefined || hiddenCells[rowId]?.[colDef.field]) { + return; + } + // for each valid cell value, check if subsequent rows have the same value + let relativeIndex = index + 1; + let rowSpan = 0; + while ( + apiRef.current.getRow(filteredSortedRowIds[relativeIndex])?.[colDef.field] === cellValue + ) { + if (hiddenCells[filteredSortedRowIds[relativeIndex]]) { + hiddenCells[filteredSortedRowIds[relativeIndex]][colDef.field] = true; + } else { + hiddenCells[filteredSortedRowIds[relativeIndex]] = { [colDef.field]: true }; + } + relativeIndex += 1; + rowSpan += 1; + } + + if (rowSpan > 0) { + if (spannedCells[rowId]) { + spannedCells[rowId][colDef.field] = rowSpan + 1; + } else { + spannedCells[rowId] = { [colDef.field]: rowSpan + 1 }; + } + } + }); + }); + + apiRef.current.setState((state) => ({ + ...state, + rowSpanning: { + spannedCells, + hiddenCells, + }, + })); + }, [apiRef, props.unstable_rowSpanning]); + + useGridApiEventHandler(apiRef, 'sortedRowsSet', handleSortedRowsSet); +}; diff --git a/packages/x-data-grid/src/models/gridStateCommunity.ts b/packages/x-data-grid/src/models/gridStateCommunity.ts index 7e00992692bf..d5cc67d7e718 100644 --- a/packages/x-data-grid/src/models/gridStateCommunity.ts +++ b/packages/x-data-grid/src/models/gridStateCommunity.ts @@ -26,6 +26,7 @@ import { GridHeaderFilteringState } from './gridHeaderFilteringModel'; import type { GridRowSelectionModel } from './gridRowSelectionModel'; import type { GridVisibleRowsLookupState } from '../hooks/features/filter/gridFilterState'; import type { GridColumnResizeState } from '../hooks/features/columnResize'; +import type { GridRowSpanningState } from '../hooks/features/rows/useGridRowSpanning'; /** * The state of `DataGrid`. @@ -52,6 +53,7 @@ export interface GridStateCommunity { density: GridDensityState; virtualization: GridVirtualizationState; columnResize: GridColumnResizeState; + rowSpanning: GridRowSpanningState; } /** diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts index 47a56c9a742a..f555f596a1bd 100644 --- a/packages/x-data-grid/src/models/props/DataGridProps.ts +++ b/packages/x-data-grid/src/models/props/DataGridProps.ts @@ -375,6 +375,11 @@ export interface DataGridPropsWithDefaultValues Date: Tue, 6 Aug 2024 16:44:55 +0500 Subject: [PATCH 02/47] Fix a few issues --- docs/data/data-grid/row-spanning/RowSpanning.js | 1 + docs/data/data-grid/row-spanning/RowSpanning.tsx | 1 + packages/x-data-grid/src/components/cell/GridCell.tsx | 1 - .../src/hooks/features/rows/useGridRowSpanning.ts | 11 +++++++---- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/data/data-grid/row-spanning/RowSpanning.js b/docs/data/data-grid/row-spanning/RowSpanning.js index b20f3ca5c7e2..bc40acabb0c4 100644 --- a/docs/data/data-grid/row-spanning/RowSpanning.js +++ b/docs/data/data-grid/row-spanning/RowSpanning.js @@ -53,6 +53,7 @@ export default function RowSpanning() { pageSizeOptions={[10]} disableRowSelectionOnClick unstable_rowSpanning + disableVirtualization sx={{ '& .MuiDataGrid-row.Mui-hovered': { backgroundColor: 'transparent', diff --git a/docs/data/data-grid/row-spanning/RowSpanning.tsx b/docs/data/data-grid/row-spanning/RowSpanning.tsx index 449ce6482d75..19c0d6cdfb4d 100644 --- a/docs/data/data-grid/row-spanning/RowSpanning.tsx +++ b/docs/data/data-grid/row-spanning/RowSpanning.tsx @@ -53,6 +53,7 @@ export default function RowSpanning() { pageSizeOptions={[10]} disableRowSelectionOnClick unstable_rowSpanning + disableVirtualization sx={{ '& .MuiDataGrid-row.Mui-hovered': { backgroundColor: 'transparent', diff --git a/packages/x-data-grid/src/components/cell/GridCell.tsx b/packages/x-data-grid/src/components/cell/GridCell.tsx index 17008e80fcf2..cfd9679e976b 100644 --- a/packages/x-data-grid/src/components/cell/GridCell.tsx +++ b/packages/x-data-grid/src/components/cell/GridCell.tsx @@ -482,7 +482,6 @@ const GridCell = React.forwardRef(function GridCe : { ...style, height: `calc(var(--height) * ${rowSpan})`, - background: 'white', zIndex: 5, display: 'flex', alignItems: 'center', diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index ab0d5919e2ba..f4f8331e53c0 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -6,7 +6,7 @@ import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { GridStateInitializer } from '../../utils/useGridInitializeState'; -import { gridSortedRowIdsSelector } from '../sorting/gridSortingSelector'; +import { gridFilteredSortedRowIdsSelector } from '../filter/gridFilterSelector'; import { gridColumnDefinitionsSelector } from '../columns/gridColumnsSelector'; export interface GridRowSpanningState { @@ -27,14 +27,16 @@ export const useGridRowSpanning = ( apiRef: React.MutableRefObject, props: Pick, ): void => { - const handleSortedRowsSet = React.useCallback>(() => { + const updateRowSpanningState = React.useCallback< + GridEventListener<'sortedRowsSet' | 'filteredRowsSet'> + >(() => { if (!props.unstable_rowSpanning) { return; } const spannedCells: Record> = {}; const hiddenCells: Record> = {}; // only span `string` columns for POC - const filteredSortedRowIds = gridSortedRowIdsSelector(apiRef); + const filteredSortedRowIds = gridFilteredSortedRowIdsSelector(apiRef); const colDefs = gridColumnDefinitionsSelector(apiRef); colDefs.forEach((colDef) => { if (colDef.type !== 'string') { @@ -80,5 +82,6 @@ export const useGridRowSpanning = ( })); }, [apiRef, props.unstable_rowSpanning]); - useGridApiEventHandler(apiRef, 'sortedRowsSet', handleSortedRowsSet); + useGridApiEventHandler(apiRef, 'sortedRowsSet', updateRowSpanningState); + useGridApiEventHandler(apiRef, 'filteredRowsSet', updateRowSpanningState); }; From 7894afaec5e887bb8fe2d6b44d115c55c9695d60 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 7 Aug 2024 18:18:06 +0500 Subject: [PATCH 03/47] Add basic keyboard navigation --- .../useGridKeyboardNavigation.ts | 66 +++++++++++++++---- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts index 1854e8a134b0..80d72ec59b95 100644 --- a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts +++ b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts @@ -7,14 +7,17 @@ import { gridVisibleColumnDefinitionsSelector } from '../columns/gridColumnsSele import { useGridLogger } from '../../utils/useGridLogger'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; -import { gridExpandedSortedRowEntriesSelector } from '../filter/gridFilterSelector'; +import { + gridExpandedSortedRowEntriesSelector, + gridFilteredSortedRowIdsSelector, +} from '../filter/gridFilterSelector'; import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; import { GRID_CHECKBOX_SELECTION_COL_DEF } from '../../../colDef/gridCheckboxSelectionColDef'; import { gridClasses } from '../../../constants/gridClasses'; import { GridCellModes } from '../../../models/gridEditRowModel'; import { isNavigationKey } from '../../../utils/keyboardUtils'; import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../../../constants/gridDetailPanelToggleField'; -import { GridRowEntry, GridRowId } from '../../../models'; +import { GridColDef, GridRowEntry, GridRowId } from '../../../models'; import { gridPinnedRowsSelector } from '../rows/gridRowsSelector'; import { gridFocusColumnGroupHeaderSelector } from '../focus'; import { gridColumnGroupsHeaderMaxDepthSelector } from '../columnGrouping/gridColumnGroupsSelector'; @@ -24,6 +27,8 @@ import { } from '../headerFiltering/gridHeaderFilteringSelectors'; import { GridPipeProcessor, useGridRegisterPipeProcessor } from '../../core/pipeProcessing'; import { isEventTargetInPortal } from '../../../utils/domUtils'; +import { useGridSelector } from '../../utils/useGridSelector'; +import { gridRowSpanningHiddenCellsSelector } from '../rows/gridRowSpanningSelectors'; function enrichPageRowsWithPinnedRows( apiRef: React.MutableRefObject, @@ -105,6 +110,9 @@ export const useGridKeyboardNavigation = ( const initialCurrentPageRows = useGridVisibleRows(apiRef, props).rows; const theme = useTheme(); + const rowSpanHiddenCells = useGridSelector(apiRef, gridRowSpanningHiddenCellsSelector); + const filteredSortedRowIds = useGridSelector(apiRef, gridFilteredSortedRowIdsSelector); + const currentPageRows = React.useMemo( () => enrichPageRowsWithPinnedRows(apiRef, initialCurrentPageRows), [apiRef, initialCurrentPageRows], @@ -114,7 +122,7 @@ export const useGridKeyboardNavigation = ( /** * @param {number} colIndex Index of the column to focus - * @param {number} rowIndex index of the row to focus + * @param {GridRowId} rowId index of the row to focus * @param {string} closestColumnToUse Which closest column cell to use when the cell is spanned by `colSpan`. * TODO replace with apiRef.current.moveFocusToRelativeCell() */ @@ -508,6 +516,25 @@ export const useGridKeyboardNavigation = ( [apiRef, currentPageRows.length, goToHeader, goToGroupHeader, goToCell, getRowIdFromIndex], ); + const findNonRowSpannedCell = React.useCallback( + (rowId: GridRowId, field: GridColDef['field'], direction: 'up' | 'down') => { + if (!rowSpanHiddenCells[rowId]?.[field]) { + return rowId; + } + // find closest non row spanned cell in the given `direction` + let nextRowIndex = filteredSortedRowIds.indexOf(rowId) + (direction === 'down' ? 1 : -1); + while (nextRowIndex >= 0 && nextRowIndex < filteredSortedRowIds.length) { + const nextRowId = filteredSortedRowIds[nextRowIndex]; + if (!rowSpanHiddenCells[nextRowId]?.[field]) { + return nextRowId; + } + nextRowIndex += direction === 'down' ? 1 : -1; + } + return rowId; + }, + [filteredSortedRowIds, rowSpanHiddenCells], + ); + const handleCellKeyDown = React.useCallback>( (params, event) => { // Ignore portal @@ -552,14 +579,24 @@ export const useGridKeyboardNavigation = ( case 'ArrowDown': { // "Enter" is only triggered by the row / cell editing feature if (rowIndexBefore < lastRowIndexInPage) { - goToCell(colIndexBefore, getRowIdFromIndex(rowIndexBefore + 1)); + const rowId = findNonRowSpannedCell( + getRowIdFromIndex(rowIndexBefore + 1), + (params as GridCellParams).field, + 'down', + ); + goToCell(colIndexBefore, rowId); } break; } case 'ArrowUp': { if (rowIndexBefore > firstRowIndexInPage) { - goToCell(colIndexBefore, getRowIdFromIndex(rowIndexBefore - 1)); + const rowId = findNonRowSpannedCell( + getRowIdFromIndex(rowIndexBefore - 1), + (params as GridCellParams).field, + 'up', + ); + goToCell(colIndexBefore, rowId); } else if (headerFilteringEnabled) { goToHeaderFilter(colIndexBefore, event); } else { @@ -576,11 +613,13 @@ export const useGridKeyboardNavigation = ( direction, }); if (rightColIndex !== null) { - goToCell( - rightColIndex, + const rightColField = apiRef.current.getVisibleColumns()[rightColIndex].field; + const rowId = findNonRowSpannedCell( getRowIdFromIndex(rowIndexBefore), - direction === 'rtl' ? 'left' : 'right', + rightColField, + 'up', ); + goToCell(rightColIndex, rowId, direction === 'rtl' ? 'left' : 'right'); } break; } @@ -593,11 +632,13 @@ export const useGridKeyboardNavigation = ( direction, }); if (leftColIndex !== null) { - goToCell( - leftColIndex, + const leftColField = apiRef.current.getVisibleColumns()[leftColIndex].field; + const rowId = findNonRowSpannedCell( getRowIdFromIndex(rowIndexBefore), - direction === 'rtl' ? 'right' : 'left', + leftColField, + 'up', ); + goToCell(leftColIndex, rowId, direction === 'rtl' ? 'right' : 'left'); } break; } @@ -686,8 +727,9 @@ export const useGridKeyboardNavigation = ( apiRef, currentPageRows, theme.direction, - goToCell, + findNonRowSpannedCell, getRowIdFromIndex, + goToCell, headerFilteringEnabled, goToHeaderFilter, goToHeader, From b5d7add38d5a7f562bd33015e23562ad1d2bc43a Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 7 Aug 2024 18:28:41 +0500 Subject: [PATCH 04/47] Add hook and initializer to other packages --- .../src/DataGridPremium/useDataGridPremiumComponent.tsx | 4 ++++ .../src/DataGridPro/useDataGridProComponent.tsx | 4 ++++ packages/x-data-grid/src/internals/index.ts | 1 + 3 files changed, 9 insertions(+) diff --git a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx index b61e63a1f927..8a3951e8e6ee 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx @@ -68,6 +68,8 @@ import { useGridDataSourceTreeDataPreProcessors, useGridDataSource, dataSourceStateInitializer, + useGridRowSpanning, + rowSpanningStateInitializer, } from '@mui/x-data-grid-pro/internals'; import { GridApiPremium, GridPrivateApiPremium } from '../models/gridApiPremium'; import { DataGridPremiumProcessedProps } from '../models/dataGridPremiumProps'; @@ -125,6 +127,7 @@ export const useDataGridPremiumComponent = ( useGridInitializeState(columnPinningStateInitializer, apiRef, props); useGridInitializeState(columnsStateInitializer, apiRef, props); useGridInitializeState(rowPinningStateInitializer, apiRef, props); + useGridInitializeState(rowSpanningStateInitializer, apiRef, props); useGridInitializeState(rowsStateInitializer, apiRef, props); useGridInitializeState(editingStateInitializer, apiRef, props); useGridInitializeState(focusStateInitializer, apiRef, props); @@ -152,6 +155,7 @@ export const useDataGridPremiumComponent = ( useGridRowPinning(apiRef, props); useGridColumns(apiRef, props); useGridRows(apiRef, props); + useGridRowSpanning(apiRef, props); useGridParamsApi(apiRef); useGridDetailPanel(apiRef, props); useGridColumnSpanning(apiRef); diff --git a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx index d902aa413bb6..966770103edb 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx @@ -47,6 +47,8 @@ import { useGridVirtualization, useGridColumnResize, columnResizeStateInitializer, + useGridRowSpanning, + rowSpanningStateInitializer, } from '@mui/x-data-grid/internals'; import { GridApiPro, GridPrivateApiPro } from '../models/gridApiPro'; import { DataGridProProcessedProps } from '../models/dataGridProProps'; @@ -114,6 +116,7 @@ export const useDataGridProComponent = ( useGridInitializeState(columnPinningStateInitializer, apiRef, props); useGridInitializeState(columnsStateInitializer, apiRef, props); useGridInitializeState(rowPinningStateInitializer, apiRef, props); + useGridInitializeState(rowSpanningStateInitializer, apiRef, props); useGridInitializeState(rowsStateInitializer, apiRef, props); useGridInitializeState(editingStateInitializer, apiRef, props); useGridInitializeState(focusStateInitializer, apiRef, props); @@ -138,6 +141,7 @@ export const useDataGridProComponent = ( useGridRowPinning(apiRef, props); useGridColumns(apiRef, props); useGridRows(apiRef, props); + useGridRowSpanning(apiRef, props); useGridParamsApi(apiRef); useGridDetailPanel(apiRef, props); useGridColumnSpanning(apiRef); diff --git a/packages/x-data-grid/src/internals/index.ts b/packages/x-data-grid/src/internals/index.ts index 90f1ca592e48..6611d9cad637 100644 --- a/packages/x-data-grid/src/internals/index.ts +++ b/packages/x-data-grid/src/internals/index.ts @@ -74,6 +74,7 @@ export { export { useGridEditing, editingStateInitializer } from '../hooks/features/editing/useGridEditing'; export { gridEditRowsStateSelector } from '../hooks/features/editing/gridEditingSelectors'; export { useGridRows, rowsStateInitializer } from '../hooks/features/rows/useGridRows'; +export { useGridRowSpanning, rowSpanningStateInitializer } from '../hooks/features/rows/useGridRowSpanning'; export { useGridRowsPreProcessors } from '../hooks/features/rows/useGridRowsPreProcessors'; export type { GridRowTreeCreationParams, From bbfad036e6be10e3fe6162ae0437c3fed0d5a7b2 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 7 Aug 2024 18:29:21 +0500 Subject: [PATCH 05/47] Some housekeeping --- docs/pages/x/api/data-grid/data-grid-premium.json | 3 ++- docs/pages/x/api/data-grid/data-grid-pro.json | 3 ++- docs/pages/x/api/data-grid/data-grid.json | 3 ++- .../data-grid/data-grid-premium/data-grid-premium.json | 3 +++ .../api-docs/data-grid/data-grid-pro/data-grid-pro.json | 3 +++ .../translations/api-docs/data-grid/data-grid/data-grid.json | 3 +++ .../src/DataGridPremium/DataGridPremium.tsx | 5 +++++ packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx | 5 +++++ packages/x-data-grid/src/DataGrid/DataGrid.tsx | 5 +++++ packages/x-data-grid/src/internals/index.ts | 5 ++++- 10 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json index 1c79c35a04e6..a668806ae4cf 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -620,7 +620,8 @@ "additionalInfo": { "sx": true } }, "throttleRowsMs": { "type": { "name": "number" }, "default": "0" }, - "treeData": { "type": { "name": "bool" }, "default": "false" } + "treeData": { "type": { "name": "bool" }, "default": "false" }, + "unstable_rowSpanning": { "type": { "name": "bool" }, "default": "false" } }, "name": "DataGridPremium", "imports": [ diff --git a/docs/pages/x/api/data-grid/data-grid-pro.json b/docs/pages/x/api/data-grid/data-grid-pro.json index c3eb24a2ffb8..53ce625dde80 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -554,7 +554,8 @@ "additionalInfo": { "sx": true } }, "throttleRowsMs": { "type": { "name": "number" }, "default": "0" }, - "treeData": { "type": { "name": "bool" }, "default": "false" } + "treeData": { "type": { "name": "bool" }, "default": "false" }, + "unstable_rowSpanning": { "type": { "name": "bool" }, "default": "false" } }, "name": "DataGridPro", "imports": [ diff --git a/docs/pages/x/api/data-grid/data-grid.json b/docs/pages/x/api/data-grid/data-grid.json index 7543c9d9f0ab..68dc7d23a7e9 100644 --- a/docs/pages/x/api/data-grid/data-grid.json +++ b/docs/pages/x/api/data-grid/data-grid.json @@ -463,7 +463,8 @@ "description": "Array<func
| object
| bool>
| func
| object" }, "additionalInfo": { "sx": true } - } + }, + "unstable_rowSpanning": { "type": { "name": "bool" }, "default": "false" } }, "name": "DataGrid", "imports": [ diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index a8b79b49fb93..0d93f8b2c03b 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -643,6 +643,9 @@ }, "treeData": { "description": "If true, the rows will be gathered in a tree structure according to the getTreeDataPath prop." + }, + "unstable_rowSpanning": { + "description": "If true, the Data Grid will auto span the cells over the rows having the same value." } }, "classDescriptions": { diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json index 914817291c87..676053f4913e 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json @@ -581,6 +581,9 @@ }, "treeData": { "description": "If true, the rows will be gathered in a tree structure according to the getTreeDataPath prop." + }, + "unstable_rowSpanning": { + "description": "If true, the Data Grid will auto span the cells over the rows having the same value." } }, "classDescriptions": { diff --git a/docs/translations/api-docs/data-grid/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid/data-grid.json index 08fa6f50e6c4..ebf7ff6c4aba 100644 --- a/docs/translations/api-docs/data-grid/data-grid/data-grid.json +++ b/docs/translations/api-docs/data-grid/data-grid/data-grid.json @@ -470,6 +470,9 @@ "sortModel": { "description": "Set the sort model of the Data Grid." }, "sx": { "description": "The system prop that allows defining system overrides as well as additional CSS styles." + }, + "unstable_rowSpanning": { + "description": "If true, the Data Grid will auto span the cells over the rows having the same value." } }, "classDescriptions": { diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index 13c08a00b7e1..9e68c043f1ce 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -1057,6 +1057,11 @@ DataGridPremiumRaw.propTypes = { set: PropTypes.func.isRequired, }), unstable_onDataSourceError: PropTypes.func, + /** + * If `true`, the Data Grid will auto span the cells over the rows having the same value. + * @default false + */ + unstable_rowSpanning: PropTypes.bool, } as any; interface DataGridPremiumComponent { diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index 1cacac533b70..c6fa34f2c276 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -956,4 +956,9 @@ DataGridProRaw.propTypes = { set: PropTypes.func.isRequired, }), unstable_onDataSourceError: PropTypes.func, + /** + * If `true`, the Data Grid will auto span the cells over the rows having the same value. + * @default false + */ + unstable_rowSpanning: PropTypes.bool, } as any; diff --git a/packages/x-data-grid/src/DataGrid/DataGrid.tsx b/packages/x-data-grid/src/DataGrid/DataGrid.tsx index ab054cbab917..ac864d6a611d 100644 --- a/packages/x-data-grid/src/DataGrid/DataGrid.tsx +++ b/packages/x-data-grid/src/DataGrid/DataGrid.tsx @@ -782,4 +782,9 @@ DataGridRaw.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * If `true`, the Data Grid will auto span the cells over the rows having the same value. + * @default false + */ + unstable_rowSpanning: PropTypes.bool, } as any; diff --git a/packages/x-data-grid/src/internals/index.ts b/packages/x-data-grid/src/internals/index.ts index 6611d9cad637..6e12e1f3f510 100644 --- a/packages/x-data-grid/src/internals/index.ts +++ b/packages/x-data-grid/src/internals/index.ts @@ -74,7 +74,10 @@ export { export { useGridEditing, editingStateInitializer } from '../hooks/features/editing/useGridEditing'; export { gridEditRowsStateSelector } from '../hooks/features/editing/gridEditingSelectors'; export { useGridRows, rowsStateInitializer } from '../hooks/features/rows/useGridRows'; -export { useGridRowSpanning, rowSpanningStateInitializer } from '../hooks/features/rows/useGridRowSpanning'; +export { + useGridRowSpanning, + rowSpanningStateInitializer, +} from '../hooks/features/rows/useGridRowSpanning'; export { useGridRowsPreProcessors } from '../hooks/features/rows/useGridRowsPreProcessors'; export type { GridRowTreeCreationParams, From 0c86f398e048fb0dd67d2fc27b9eb6bbb9ec9a1a Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 7 Aug 2024 18:51:39 +0500 Subject: [PATCH 06/47] Remove planned flag --- docs/data/data-grid/row-spanning/row-spanning.md | 6 +++++- docs/data/pages.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/data/data-grid/row-spanning/row-spanning.md b/docs/data/data-grid/row-spanning/row-spanning.md index 19a41594c151..4522cad36290 100644 --- a/docs/data/data-grid/row-spanning/row-spanning.md +++ b/docs/data/data-grid/row-spanning/row-spanning.md @@ -1,4 +1,4 @@ -# Data Grid - Row spanning 🚧 +# Data Grid - Row spanning

Span cells across several columns.

@@ -8,6 +8,10 @@ This is very close to the "row spanning" in an HTML `
`. To enable, pass the `unstable_rowSpanning` prop to the Data Grid. +The Data Grid will automatically merge cells with the same value in a specified column. + +Additionally, you could manually provide the value used in row spanning using `colDef.valueGetter` prop. + {{"demo": "RowSpanning.js", "bg": "inline", "defaultCodeOpen": false}} ## API diff --git a/docs/data/pages.ts b/docs/data/pages.ts index 62ed986c8ca8..eba91fb7ce87 100644 --- a/docs/data/pages.ts +++ b/docs/data/pages.ts @@ -51,7 +51,7 @@ const pages: MuiPage[] = [ { pathname: '/x/react-data-grid/row-definition' }, { pathname: '/x/react-data-grid/row-updates' }, { pathname: '/x/react-data-grid/row-height' }, - { pathname: '/x/react-data-grid/row-spanning', planned: true }, + { pathname: '/x/react-data-grid/row-spanning' }, { pathname: '/x/react-data-grid/master-detail', plan: 'pro' }, { pathname: '/x/react-data-grid/row-ordering', plan: 'pro' }, { pathname: '/x/react-data-grid/row-pinning', plan: 'pro' }, From 0880c1bcfa4cfffa3053ae2f6d6a1bc2fc94aed1 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 7 Aug 2024 18:52:07 +0500 Subject: [PATCH 07/47] Add support for valueGetter --- .../hooks/features/rows/useGridRowSpanning.ts | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index f4f8331e53c0..ec5da5c347fc 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -3,7 +3,7 @@ import { GridEventListener } from '../../../models/events'; import { GridColDef } from '../../../models/colDef'; import { GridRowId } from '../../../models/gridRows'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; -import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; +import { GridApiCommunity, GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { GridStateInitializer } from '../../utils/useGridInitializeState'; import { gridFilteredSortedRowIdsSelector } from '../filter/gridFilterSelector'; @@ -23,6 +23,19 @@ export const rowSpanningStateInitializer: GridStateInitializer = (state) => { }; }; +const getCellValue = ( + rowId: GridRowId, + colDef: GridColDef, + apiRef: React.MutableRefObject, +) => { + const row = apiRef.current.getRow(rowId); + let cellValue = row?.[colDef.field]; + if (colDef.valueGetter) { + cellValue = colDef.valueGetter(cellValue as never, row, colDef, apiRef); + } + return cellValue; +}; + export const useGridRowSpanning = ( apiRef: React.MutableRefObject, props: Pick, @@ -35,25 +48,23 @@ export const useGridRowSpanning = ( } const spannedCells: Record> = {}; const hiddenCells: Record> = {}; - // only span `string` columns for POC const filteredSortedRowIds = gridFilteredSortedRowIdsSelector(apiRef); const colDefs = gridColumnDefinitionsSelector(apiRef); colDefs.forEach((colDef) => { - if (colDef.type !== 'string') { - return; - } // TODO Perf: Process rendered rows first and lazily process the rest filteredSortedRowIds.forEach((rowId, index) => { - const cellValue = apiRef.current.getRow(rowId)[colDef.field]; - if (cellValue === undefined || hiddenCells[rowId]?.[colDef.field]) { + if (hiddenCells[rowId]?.[colDef.field]) { + return; + } + const cellValue = getCellValue(rowId, colDef, apiRef); + + if (cellValue == null) { return; } // for each valid cell value, check if subsequent rows have the same value let relativeIndex = index + 1; let rowSpan = 0; - while ( - apiRef.current.getRow(filteredSortedRowIds[relativeIndex])?.[colDef.field] === cellValue - ) { + while (getCellValue( filteredSortedRowIds[relativeIndex], colDef, apiRef) === cellValue) { if (hiddenCells[filteredSortedRowIds[relativeIndex]]) { hiddenCells[filteredSortedRowIds[relativeIndex]][colDef.field] = true; } else { From c526ea6b96fcd970664fc0b45113653592415126 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 7 Aug 2024 19:17:42 +0500 Subject: [PATCH 08/47] Add rowSpanValueGetter to support exclusion from row spanning even when there are repeated values --- .../data-grid/row-spanning/RowSpanning.js | 194 +++++++++++++++--- .../data-grid/row-spanning/RowSpanning.tsx | 194 +++++++++++++++--- .../x/api/data-grid/grid-actions-col-def.json | 1 + docs/pages/x/api/data-grid/grid-col-def.json | 1 + .../data-grid/grid-single-select-col-def.json | 1 + .../data-grid/grid-actions-col-def.json | 3 + .../api-docs/data-grid/grid-col-def.json | 3 + .../data-grid/grid-single-select-col-def.json | 3 + .../src/components/cell/GridCell.tsx | 3 - .../hooks/features/rows/useGridRowSpanning.ts | 7 +- .../src/models/colDef/gridColDef.ts | 4 + 11 files changed, 340 insertions(+), 74 deletions(-) diff --git a/docs/data/data-grid/row-spanning/RowSpanning.js b/docs/data/data-grid/row-spanning/RowSpanning.js index bc40acabb0c4..d5c92e81af64 100644 --- a/docs/data/data-grid/row-spanning/RowSpanning.js +++ b/docs/data/data-grid/row-spanning/RowSpanning.js @@ -2,40 +2,6 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import { DataGrid } from '@mui/x-data-grid'; -const columns = [ - { - field: 'event', - headerName: 'Event', - width: 200, - editable: true, - }, - { - field: 'indicator', - headerName: 'Indicator', - width: 150, - editable: true, - }, - { - field: 'action', - headerName: 'Action', - width: 150, - editable: true, - }, -]; - -const rows = [ - { id: 1, event: 'Event 1', indicator: 'Indicator 1', action: 'Actions 1' }, - { id: 2, event: 'Event 1', indicator: 'Indicator 1', action: 'Actions 2' }, - { id: 3, event: 'Event 1', indicator: 'Indicator 1', action: 'Actions 3' }, - { id: 4, event: 'Event 1', indicator: 'Indicator 2', action: 'Actions 1' }, - { id: 5, event: 'Event 1', indicator: 'Indicator 2', action: 'Actions 2' }, - { id: 6, event: 'Event 2', indicator: 'Indicator 1', action: 'Actions 1' }, - { id: 7, event: 'Event 2', indicator: 'Indicator 1', action: 'Actions 2' }, - { id: 8, event: 'Event 2', indicator: 'Indicator 1', action: 'Actions 3' }, - { id: 9, event: 'Event 2', indicator: 'Indicator 2', action: 'Actions 1' }, - { id: 10, event: 'Event 2', indicator: 'Indicator 2', action: 'Actions 2' }, -]; - export default function RowSpanning() { return ( @@ -66,3 +32,163 @@ export default function RowSpanning() { ); } + +const columns = [ + { + field: 'event', + headerName: 'Event', + width: 200, + editable: true, + }, + { + field: 'indicator', + headerName: 'Indicator', + width: 150, + editable: true, + }, + { + field: 'action', + headerName: 'Action', + width: 150, + editable: true, + }, + { + field: 'decision', + headerName: 'Decision', + type: 'number', + width: 100, + }, + { + field: 'location', + headerName: 'Location', + type: 'number', + width: 100, + rowSpanValueGetter: () => { + // Exclude this column from row spanning irrespective of the values + return undefined; + }, + }, +]; + +const rows = [ + { + id: 1, + event: 'Event 1', + indicator: 'Indicator 1', + action: 'Actions 1', + decision: 1, + location: 2, + }, + { + id: 2, + event: 'Event 1', + indicator: 'Indicator 1', + action: 'Actions 2', + decision: 1, + location: 3, + }, + { + id: 3, + event: 'Event 1', + indicator: 'Indicator 1', + action: 'Actions 3', + decision: 1, + location: 1, + }, + { + id: 4, + event: 'Event 1', + indicator: 'Indicator 2', + action: 'Actions 1', + decision: 4, + location: 3, + }, + { + id: 5, + event: 'Event 1', + indicator: 'Indicator 2', + action: 'Actions 2', + decision: 4, + location: 3, + }, + { + id: 6, + event: 'Event 2', + indicator: 'Indicator 1', + action: 'Actions 1', + decision: 6, + location: 1, + }, + { + id: 7, + event: 'Event 2', + indicator: 'Indicator 1', + action: 'Actions 2', + decision: 6, + location: 2, + }, + { + id: 8, + event: 'Event 2', + indicator: 'Indicator 1', + action: 'Actions 3', + decision: 6, + location: 2, + }, + { + id: 9, + event: 'Event 2', + indicator: 'Indicator 2', + action: 'Actions 1', + decision: 9, + location: 1, + }, + { + id: 10, + event: 'Event 2', + indicator: 'Indicator 2', + action: 'Actions 2', + decision: 9, + location: 4, + }, + { + id: 11, + event: 'Event 3', + indicator: 'Indicator 1', + action: 'Actions 1', + decision: 11, + location: 1, + }, + { + id: 12, + event: 'Event 3', + indicator: 'Indicator 1', + action: 'Actions 2', + decision: 11, + location: 1, + }, + { + id: 13, + event: 'Event 3', + indicator: 'Indicator 1', + action: 'Actions 3', + decision: 11, + location: 2, + }, + { + id: 14, + event: 'Event 3', + indicator: 'Indicator 2', + action: 'Actions 1', + decision: 14, + location: 4, + }, + { + id: 15, + event: 'Event 3', + indicator: 'Indicator 2', + action: 'Actions 2', + decision: 14, + location: 3, + }, +]; diff --git a/docs/data/data-grid/row-spanning/RowSpanning.tsx b/docs/data/data-grid/row-spanning/RowSpanning.tsx index 19c0d6cdfb4d..319316dd629c 100644 --- a/docs/data/data-grid/row-spanning/RowSpanning.tsx +++ b/docs/data/data-grid/row-spanning/RowSpanning.tsx @@ -2,40 +2,6 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import { DataGrid, GridColDef } from '@mui/x-data-grid'; -const columns: GridColDef<(typeof rows)[number]>[] = [ - { - field: 'event', - headerName: 'Event', - width: 200, - editable: true, - }, - { - field: 'indicator', - headerName: 'Indicator', - width: 150, - editable: true, - }, - { - field: 'action', - headerName: 'Action', - width: 150, - editable: true, - }, -]; - -const rows = [ - { id: 1, event: 'Event 1', indicator: 'Indicator 1', action: 'Actions 1' }, - { id: 2, event: 'Event 1', indicator: 'Indicator 1', action: 'Actions 2' }, - { id: 3, event: 'Event 1', indicator: 'Indicator 1', action: 'Actions 3' }, - { id: 4, event: 'Event 1', indicator: 'Indicator 2', action: 'Actions 1' }, - { id: 5, event: 'Event 1', indicator: 'Indicator 2', action: 'Actions 2' }, - { id: 6, event: 'Event 2', indicator: 'Indicator 1', action: 'Actions 1' }, - { id: 7, event: 'Event 2', indicator: 'Indicator 1', action: 'Actions 2' }, - { id: 8, event: 'Event 2', indicator: 'Indicator 1', action: 'Actions 3' }, - { id: 9, event: 'Event 2', indicator: 'Indicator 2', action: 'Actions 1' }, - { id: 10, event: 'Event 2', indicator: 'Indicator 2', action: 'Actions 2' }, -]; - export default function RowSpanning() { return ( @@ -66,3 +32,163 @@ export default function RowSpanning() { ); } + +const columns: GridColDef<(typeof rows)[number]>[] = [ + { + field: 'event', + headerName: 'Event', + width: 200, + editable: true, + }, + { + field: 'indicator', + headerName: 'Indicator', + width: 150, + editable: true, + }, + { + field: 'action', + headerName: 'Action', + width: 150, + editable: true, + }, + { + field: 'decision', + headerName: 'Decision', + type: 'number', + width: 100, + }, + { + field: 'location', + headerName: 'Location', + type: 'number', + width: 100, + rowSpanValueGetter: () => { + // Exclude this column from row spanning irrespective of the values + return undefined; + }, + }, +]; + +const rows = [ + { + id: 1, + event: 'Event 1', + indicator: 'Indicator 1', + action: 'Actions 1', + decision: 1, + location: 2, + }, + { + id: 2, + event: 'Event 1', + indicator: 'Indicator 1', + action: 'Actions 2', + decision: 1, + location: 3, + }, + { + id: 3, + event: 'Event 1', + indicator: 'Indicator 1', + action: 'Actions 3', + decision: 1, + location: 1, + }, + { + id: 4, + event: 'Event 1', + indicator: 'Indicator 2', + action: 'Actions 1', + decision: 4, + location: 3, + }, + { + id: 5, + event: 'Event 1', + indicator: 'Indicator 2', + action: 'Actions 2', + decision: 4, + location: 3, + }, + { + id: 6, + event: 'Event 2', + indicator: 'Indicator 1', + action: 'Actions 1', + decision: 6, + location: 1, + }, + { + id: 7, + event: 'Event 2', + indicator: 'Indicator 1', + action: 'Actions 2', + decision: 6, + location: 2, + }, + { + id: 8, + event: 'Event 2', + indicator: 'Indicator 1', + action: 'Actions 3', + decision: 6, + location: 2, + }, + { + id: 9, + event: 'Event 2', + indicator: 'Indicator 2', + action: 'Actions 1', + decision: 9, + location: 1, + }, + { + id: 10, + event: 'Event 2', + indicator: 'Indicator 2', + action: 'Actions 2', + decision: 9, + location: 4, + }, + { + id: 11, + event: 'Event 3', + indicator: 'Indicator 1', + action: 'Actions 1', + decision: 11, + location: 1, + }, + { + id: 12, + event: 'Event 3', + indicator: 'Indicator 1', + action: 'Actions 2', + decision: 11, + location: 1, + }, + { + id: 13, + event: 'Event 3', + indicator: 'Indicator 1', + action: 'Actions 3', + decision: 11, + location: 2, + }, + { + id: 14, + event: 'Event 3', + indicator: 'Indicator 2', + action: 'Actions 1', + decision: 14, + location: 4, + }, + { + id: 15, + event: 'Event 3', + indicator: 'Indicator 2', + action: 'Actions 2', + decision: 14, + location: 3, + }, +]; diff --git a/docs/pages/x/api/data-grid/grid-actions-col-def.json b/docs/pages/x/api/data-grid/grid-actions-col-def.json index 26d5acce67f3..eece855816ed 100644 --- a/docs/pages/x/api/data-grid/grid-actions-col-def.json +++ b/docs/pages/x/api/data-grid/grid-actions-col-def.json @@ -89,6 +89,7 @@ "isProPlan": true }, "resizable": { "type": { "description": "boolean" }, "default": "true" }, + "rowSpanValueGetter": { "type": { "description": "GridValueGetter<R, V, F>" } }, "sortable": { "type": { "description": "boolean" }, "default": "true" }, "sortComparator": { "type": { "description": "GridComparatorFn<V>" } }, "sortingOrder": { "type": { "description": "readonly GridSortDirection[]" } }, diff --git a/docs/pages/x/api/data-grid/grid-col-def.json b/docs/pages/x/api/data-grid/grid-col-def.json index bfe97f8a9704..0546471b90ea 100644 --- a/docs/pages/x/api/data-grid/grid-col-def.json +++ b/docs/pages/x/api/data-grid/grid-col-def.json @@ -82,6 +82,7 @@ "isProPlan": true }, "resizable": { "type": { "description": "boolean" }, "default": "true" }, + "rowSpanValueGetter": { "type": { "description": "GridValueGetter<R, V, F>" } }, "sortable": { "type": { "description": "boolean" }, "default": "true" }, "sortComparator": { "type": { "description": "GridComparatorFn<V>" } }, "sortingOrder": { "type": { "description": "readonly GridSortDirection[]" } }, diff --git a/docs/pages/x/api/data-grid/grid-single-select-col-def.json b/docs/pages/x/api/data-grid/grid-single-select-col-def.json index 669318bc4848..e07981f5de85 100644 --- a/docs/pages/x/api/data-grid/grid-single-select-col-def.json +++ b/docs/pages/x/api/data-grid/grid-single-select-col-def.json @@ -89,6 +89,7 @@ "isProPlan": true }, "resizable": { "type": { "description": "boolean" }, "default": "true" }, + "rowSpanValueGetter": { "type": { "description": "GridValueGetter<R, V, F>" } }, "sortable": { "type": { "description": "boolean" }, "default": "true" }, "sortComparator": { "type": { "description": "GridComparatorFn<V>" } }, "sortingOrder": { "type": { "description": "readonly GridSortDirection[]" } }, diff --git a/docs/translations/api-docs/data-grid/grid-actions-col-def.json b/docs/translations/api-docs/data-grid/grid-actions-col-def.json index 087bee3376cc..ec34f0c62093 100644 --- a/docs/translations/api-docs/data-grid/grid-actions-col-def.json +++ b/docs/translations/api-docs/data-grid/grid-actions-col-def.json @@ -75,6 +75,9 @@ "description": "Allows to render a component in the column header filter cell." }, "resizable": { "description": "If true, the column is resizable." }, + "rowSpanValueGetter": { + "description": "Function that allows to provide a specific value to be used in row spanning." + }, "sortable": { "description": "If true, the column is sortable." }, "sortComparator": { "description": "A comparator function used to sort rows." }, "sortingOrder": { "description": "The order of the sorting sequence." }, diff --git a/docs/translations/api-docs/data-grid/grid-col-def.json b/docs/translations/api-docs/data-grid/grid-col-def.json index 15b648e0809e..1e63902cd79e 100644 --- a/docs/translations/api-docs/data-grid/grid-col-def.json +++ b/docs/translations/api-docs/data-grid/grid-col-def.json @@ -73,6 +73,9 @@ "description": "Allows to render a component in the column header filter cell." }, "resizable": { "description": "If true, the column is resizable." }, + "rowSpanValueGetter": { + "description": "Function that allows to provide a specific value to be used in row spanning." + }, "sortable": { "description": "If true, the column is sortable." }, "sortComparator": { "description": "A comparator function used to sort rows." }, "sortingOrder": { "description": "The order of the sorting sequence." }, diff --git a/docs/translations/api-docs/data-grid/grid-single-select-col-def.json b/docs/translations/api-docs/data-grid/grid-single-select-col-def.json index fdd0720fd1a2..55fceed053c9 100644 --- a/docs/translations/api-docs/data-grid/grid-single-select-col-def.json +++ b/docs/translations/api-docs/data-grid/grid-single-select-col-def.json @@ -78,6 +78,9 @@ "description": "Allows to render a component in the column header filter cell." }, "resizable": { "description": "If true, the column is resizable." }, + "rowSpanValueGetter": { + "description": "Function that allows to provide a specific value to be used in row spanning." + }, "sortable": { "description": "If true, the column is sortable." }, "sortComparator": { "description": "A comparator function used to sort rows." }, "sortingOrder": { "description": "The order of the sorting sequence." }, diff --git a/packages/x-data-grid/src/components/cell/GridCell.tsx b/packages/x-data-grid/src/components/cell/GridCell.tsx index cfd9679e976b..d9c31efd46e3 100644 --- a/packages/x-data-grid/src/components/cell/GridCell.tsx +++ b/packages/x-data-grid/src/components/cell/GridCell.tsx @@ -483,9 +483,6 @@ const GridCell = React.forwardRef(function GridCe ...style, height: `calc(var(--height) * ${rowSpan})`, zIndex: 5, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', } } title={title} diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index ec5da5c347fc..1c7726f57869 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -30,8 +30,9 @@ const getCellValue = ( ) => { const row = apiRef.current.getRow(rowId); let cellValue = row?.[colDef.field]; - if (colDef.valueGetter) { - cellValue = colDef.valueGetter(cellValue as never, row, colDef, apiRef); + const valueGetter = colDef.rowSpanValueGetter ?? colDef.valueGetter; + if (valueGetter) { + cellValue = valueGetter(cellValue as never, row, colDef, apiRef); } return cellValue; }; @@ -64,7 +65,7 @@ export const useGridRowSpanning = ( // for each valid cell value, check if subsequent rows have the same value let relativeIndex = index + 1; let rowSpan = 0; - while (getCellValue( filteredSortedRowIds[relativeIndex], colDef, apiRef) === cellValue) { + while (getCellValue(filteredSortedRowIds[relativeIndex], colDef, apiRef) === cellValue) { if (hiddenCells[filteredSortedRowIds[relativeIndex]]) { hiddenCells[filteredSortedRowIds[relativeIndex]][colDef.field] = true; } else { diff --git a/packages/x-data-grid/src/models/colDef/gridColDef.ts b/packages/x-data-grid/src/models/colDef/gridColDef.ts index 4030443ff315..039fb6589f5e 100644 --- a/packages/x-data-grid/src/models/colDef/gridColDef.ts +++ b/packages/x-data-grid/src/models/colDef/gridColDef.ts @@ -184,6 +184,10 @@ export interface GridBaseColDef; + /** + * Function that allows to provide a specific value to be used in row spanning. + */ + rowSpanValueGetter?: GridValueGetter; /** * Function that allows to customize how the entered value is stored in the row. * It only works with cell/row editing. From ed97a2dafb0346cc5ffc05ecc8b33dc848e4f236 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 7 Aug 2024 19:24:47 +0500 Subject: [PATCH 09/47] Improve docs a bit --- docs/data/data-grid/row-spanning/row-spanning.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/data/data-grid/row-spanning/row-spanning.md b/docs/data/data-grid/row-spanning/row-spanning.md index 4522cad36290..a3c13b2fce2b 100644 --- a/docs/data/data-grid/row-spanning/row-spanning.md +++ b/docs/data/data-grid/row-spanning/row-spanning.md @@ -9,8 +9,7 @@ This is very close to the "row spanning" in an HTML `
`. To enable, pass the `unstable_rowSpanning` prop to the Data Grid. The Data Grid will automatically merge cells with the same value in a specified column. - -Additionally, you could manually provide the value used in row spanning using `colDef.valueGetter` prop. +Additionally, you could customize the value used in row spanning computation using `colDef.rowSpanValueGetter` prop and both the value used in row spanning computation and the value used in cell using `colDef.valueGetter` prop. {{"demo": "RowSpanning.js", "bg": "inline", "defaultCodeOpen": false}} From c458c570ca5658d1389b6746941cb5b4cb86fd04 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 19 Aug 2024 20:12:35 +0500 Subject: [PATCH 10/47] Add new feature flag --- docs/data/pages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/pages.ts b/docs/data/pages.ts index eba91fb7ce87..83cb88c2672e 100644 --- a/docs/data/pages.ts +++ b/docs/data/pages.ts @@ -51,7 +51,7 @@ const pages: MuiPage[] = [ { pathname: '/x/react-data-grid/row-definition' }, { pathname: '/x/react-data-grid/row-updates' }, { pathname: '/x/react-data-grid/row-height' }, - { pathname: '/x/react-data-grid/row-spanning' }, + { pathname: '/x/react-data-grid/row-spanning', newFeature: true }, { pathname: '/x/react-data-grid/master-detail', plan: 'pro' }, { pathname: '/x/react-data-grid/row-ordering', plan: 'pro' }, { pathname: '/x/react-data-grid/row-pinning', plan: 'pro' }, From 51bc9d41fed06d95d87f4e95b49071f30cc35c95 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 20 Aug 2024 00:12:18 +0500 Subject: [PATCH 11/47] Fix column resize --- packages/x-data-grid/src/components/cell/GridCell.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/x-data-grid/src/components/cell/GridCell.tsx b/packages/x-data-grid/src/components/cell/GridCell.tsx index 6f2fb91843d0..aaf3c0a0cb4d 100644 --- a/packages/x-data-grid/src/components/cell/GridCell.tsx +++ b/packages/x-data-grid/src/components/cell/GridCell.tsx @@ -462,7 +462,12 @@ const GridCell = React.forwardRef(function GridCe const isHidden = hiddenCells[rowId]?.[field] ?? false; if (isHidden) { - return
; + return ( +
+ ); } const rowSpan = spannedCells[rowId]?.[field] ?? 1; From e2a4605c750e0e67028fade868e4f04a3cfee1bd Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 20 Aug 2024 17:06:45 +0500 Subject: [PATCH 12/47] Add a couple of demos --- .../row-spanning/RowSpanningCalender.js | 156 ++++++++++++++++ .../row-spanning/RowSpanningCalender.tsx | 167 ++++++++++++++++++ .../row-spanning/RowSpanningCustom.js | 106 +++++++++++ .../row-spanning/RowSpanningCustom.tsx | 106 +++++++++++ .../data-grid/row-spanning/row-spanning.md | 23 ++- 5 files changed, 555 insertions(+), 3 deletions(-) create mode 100644 docs/data/data-grid/row-spanning/RowSpanningCalender.js create mode 100644 docs/data/data-grid/row-spanning/RowSpanningCalender.tsx create mode 100644 docs/data/data-grid/row-spanning/RowSpanningCustom.js create mode 100644 docs/data/data-grid/row-spanning/RowSpanningCustom.tsx diff --git a/docs/data/data-grid/row-spanning/RowSpanningCalender.js b/docs/data/data-grid/row-spanning/RowSpanningCalender.js new file mode 100644 index 000000000000..a9186afd2938 --- /dev/null +++ b/docs/data/data-grid/row-spanning/RowSpanningCalender.js @@ -0,0 +1,156 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { DataGrid } from '@mui/x-data-grid'; + +const slotTimesLookup = { + 0: '09:00 - 10:00', + 1: '10:00 - 11:00', + 2: '11:00 - 12:00', + 3: '12:00 - 13:00', + 4: '13:00 - 14:00', + 5: '14:00 - 15:00', + 6: '15:00 - 16:00', + 7: '16:00 - 17:00', +}; + +const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']; + +const rows = [ + { + id: 0, + time: slotTimesLookup[0], + slots: ['Maths', 'Chemistry', 'Physics', 'Music', 'Maths'], + }, + { + id: 1, + time: slotTimesLookup[1], + slots: ['English', 'Chemistry', 'English', 'Music', 'Dance'], + }, + { + id: 2, + time: slotTimesLookup[2], + slots: ['English', 'Chemistry', 'Maths', 'Chemistry', 'Dance'], + }, + { + id: 3, + time: slotTimesLookup[3], + slots: ['Lab', 'Physics', 'Maths', 'Chemistry', 'Physics'], + }, + { + id: 4, + time: slotTimesLookup[4], + slots: ['', '', '', '', ''], + }, + { + id: 5, + time: slotTimesLookup[5], + slots: ['Lab', 'Maths', 'Chemistry', 'Chemistry', 'English'], + }, + { + id: 6, + time: slotTimesLookup[6], + slots: ['Music', 'Lab', 'Chemistry', 'English', ''], + }, + { + id: 7, + time: slotTimesLookup[7], + slots: ['Music', 'Dance', '', 'English', ''], + }, +]; + +const slotColumnCommonFields = { + sortable: false, + filterable: false, + pinnable: false, + hideable: false, + cellClassName: (params) => params.value, +}; + +const columns = [ + { + field: 'time', + headerName: 'Time', + width: 120, + }, + { + field: '0', + headerName: days[0], + valueGetter: (value, row) => row?.slots[0], + ...slotColumnCommonFields, + }, + { + field: '1', + headerName: days[1], + valueGetter: (value, row) => row?.slots[1], + ...slotColumnCommonFields, + }, + { + field: '2', + headerName: days[2], + valueGetter: (value, row) => row?.slots[2], + ...slotColumnCommonFields, + }, + { + field: '3', + headerName: days[3], + valueGetter: (value, row) => row?.slots[3], + ...slotColumnCommonFields, + }, + { + field: '4', + headerName: days[4], + valueGetter: (value, row) => row?.slots[4], + ...slotColumnCommonFields, + }, +]; + +const rootStyles = { + width: '100%', + '& .Maths': { + backgroundColor: 'rgba(157, 255, 118, 0.49)', + }, + '& .English': { + backgroundColor: 'rgba(255, 255, 10, 0.49)', + }, + '& .Lab': { + backgroundColor: 'rgba(150, 150, 150, 0.49)', + }, + '& .Chemistry': { + backgroundColor: 'rgba(255, 150, 150, 0.49)', + }, + '& .Physics': { + backgroundColor: 'rgba(10, 150, 255, 0.49)', + }, + '& .Music': { + backgroundColor: 'rgba(224, 183, 60, 0.55)', + }, + '& .Dance': { + backgroundColor: 'rgba(200, 150, 255, 0.49)', + }, +}; + +export default function RowSpanningCalender() { + return ( + + + + ); +} diff --git a/docs/data/data-grid/row-spanning/RowSpanningCalender.tsx b/docs/data/data-grid/row-spanning/RowSpanningCalender.tsx new file mode 100644 index 000000000000..ee8db7e153b5 --- /dev/null +++ b/docs/data/data-grid/row-spanning/RowSpanningCalender.tsx @@ -0,0 +1,167 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; + +const slotTimesLookup = { + 0: '09:00 - 10:00', + 1: '10:00 - 11:00', + 2: '11:00 - 12:00', + 3: '12:00 - 13:00', + 4: '13:00 - 14:00', + 5: '14:00 - 15:00', + 6: '15:00 - 16:00', + 7: '16:00 - 17:00', +}; + +const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']; + +type Subject = + | 'Maths' + | 'English' + | 'Lab' + | 'Chemistry' + | 'Physics' + | 'Music' + | 'Dance'; + +type Row = { id: number; time: string; slots: Array }; + +const rows: Array = [ + { + id: 0, + time: slotTimesLookup[0], + slots: ['Maths', 'Chemistry', 'Physics', 'Music', 'Maths'], + }, + { + id: 1, + time: slotTimesLookup[1], + slots: ['English', 'Chemistry', 'English', 'Music', 'Dance'], + }, + { + id: 2, + time: slotTimesLookup[2], + slots: ['English', 'Chemistry', 'Maths', 'Chemistry', 'Dance'], + }, + { + id: 3, + time: slotTimesLookup[3], + slots: ['Lab', 'Physics', 'Maths', 'Chemistry', 'Physics'], + }, + { + id: 4, + time: slotTimesLookup[4], + slots: ['', '', '', '', ''], + }, + { + id: 5, + time: slotTimesLookup[5], + slots: ['Lab', 'Maths', 'Chemistry', 'Chemistry', 'English'], + }, + { + id: 6, + time: slotTimesLookup[6], + slots: ['Music', 'Lab', 'Chemistry', 'English', ''], + }, + { + id: 7, + time: slotTimesLookup[7], + slots: ['Music', 'Dance', '', 'English', ''], + }, +]; + +const slotColumnCommonFields: Partial = { + sortable: false, + filterable: false, + pinnable: false, + hideable: false, + cellClassName: (params) => params.value, +}; + +const columns: GridColDef[] = [ + { + field: 'time', + headerName: 'Time', + width: 120, + }, + { + field: '0', + headerName: days[0], + valueGetter: (value, row) => row?.slots[0], + ...slotColumnCommonFields, + }, + { + field: '1', + headerName: days[1], + valueGetter: (value, row) => row?.slots[1], + ...slotColumnCommonFields, + }, + { + field: '2', + headerName: days[2], + valueGetter: (value, row) => row?.slots[2], + ...slotColumnCommonFields, + }, + { + field: '3', + headerName: days[3], + valueGetter: (value, row) => row?.slots[3], + ...slotColumnCommonFields, + }, + { + field: '4', + headerName: days[4], + valueGetter: (value, row) => row?.slots[4], + ...slotColumnCommonFields, + }, +]; + +const rootStyles = { + width: '100%', + '& .Maths': { + backgroundColor: 'rgba(157, 255, 118, 0.49)', + }, + '& .English': { + backgroundColor: 'rgba(255, 255, 10, 0.49)', + }, + '& .Lab': { + backgroundColor: 'rgba(150, 150, 150, 0.49)', + }, + '& .Chemistry': { + backgroundColor: 'rgba(255, 150, 150, 0.49)', + }, + '& .Physics': { + backgroundColor: 'rgba(10, 150, 255, 0.49)', + }, + '& .Music': { + backgroundColor: 'rgba(224, 183, 60, 0.55)', + }, + '& .Dance': { + backgroundColor: 'rgba(200, 150, 255, 0.49)', + }, +}; + +export default function RowSpanningCalender() { + return ( + + + + ); +} diff --git a/docs/data/data-grid/row-spanning/RowSpanningCustom.js b/docs/data/data-grid/row-spanning/RowSpanningCustom.js new file mode 100644 index 000000000000..5e4f81d674b1 --- /dev/null +++ b/docs/data/data-grid/row-spanning/RowSpanningCustom.js @@ -0,0 +1,106 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { DataGrid } from '@mui/x-data-grid'; + +export default function RowSpanningCustom() { + return ( + + + + ); +} + +const columns = [ + { + field: 'name', + headerName: 'Name', + width: 200, + editable: true, + }, + { + field: 'designation', + headerName: 'Designation', + width: 200, + editable: true, + }, + { + field: 'department', + headerName: 'Department', + width: 150, + editable: true, + }, + { + field: 'age', + headerName: 'Age', + type: 'number', + width: 100, + valueGetter: (value) => { + return `${value} yo`; + }, + rowSpanValueGetter: (value, row) => { + console.log(row); + return row ? `${row.name}-${row.age}` : value; + }, + }, +]; + +const rows = [ + { + id: 1, + name: 'George Floyd', + designation: 'React Engineer', + department: 'Engineering', + age: 25, + }, + { + id: 2, + name: 'George Floyd', + designation: 'Technical Interviewer', + department: 'Human resource', + age: 25, + }, + { + id: 3, + name: 'Cynthia Duke', + designation: 'Technical Team Lead', + department: 'Engineering', + age: 25, + }, + { + id: 4, + name: 'Jordyn Black', + designation: 'React Engineer', + department: 'Engineering', + age: 31, + }, + { + id: 5, + name: 'Rene Glass', + designation: 'Ops Lead', + department: 'Operations', + age: 31, + }, +]; diff --git a/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx b/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx new file mode 100644 index 000000000000..4ba725593bde --- /dev/null +++ b/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; + +export default function RowSpanningCustom() { + return ( + + + + ); +} + +const columns: GridColDef[] = [ + { + field: 'name', + headerName: 'Name', + width: 200, + editable: true, + }, + { + field: 'designation', + headerName: 'Designation', + width: 200, + editable: true, + }, + { + field: 'department', + headerName: 'Department', + width: 150, + editable: true, + }, + { + field: 'age', + headerName: 'Age', + type: 'number', + width: 100, + valueGetter: (value) => { + return `${value} yo`; + }, + rowSpanValueGetter: (value, row) => { + console.log(row); + return row ? `${row.name}-${row.age}` : value; + }, + }, +]; + +const rows = [ + { + id: 1, + name: 'George Floyd', + designation: 'React Engineer', + department: 'Engineering', + age: 25, + }, + { + id: 2, + name: 'George Floyd', + designation: 'Technical Interviewer', + department: 'Human resource', + age: 25, + }, + { + id: 3, + name: 'Cynthia Duke', + designation: 'Technical Team Lead', + department: 'Engineering', + age: 25, + }, + { + id: 4, + name: 'Jordyn Black', + designation: 'React Engineer', + department: 'Engineering', + age: 31, + }, + { + id: 5, + name: 'Rene Glass', + designation: 'Ops Lead', + department: 'Operations', + age: 31, + }, +]; diff --git a/docs/data/data-grid/row-spanning/row-spanning.md b/docs/data/data-grid/row-spanning/row-spanning.md index a3c13b2fce2b..f9c79dfa0de7 100644 --- a/docs/data/data-grid/row-spanning/row-spanning.md +++ b/docs/data/data-grid/row-spanning/row-spanning.md @@ -1,18 +1,35 @@ # Data Grid - Row spanning -

Span cells across several columns.

+

Span cells across several rows.

-Each cell takes up the width of one row. +Each cell takes up the height of one row. Row spanning lets you change this default behavior, so cells can span multiple rows. This is very close to the "row spanning" in an HTML `
`. To enable, pass the `unstable_rowSpanning` prop to the Data Grid. The Data Grid will automatically merge cells with the same value in a specified column. -Additionally, you could customize the value used in row spanning computation using `colDef.rowSpanValueGetter` prop and both the value used in row spanning computation and the value used in cell using `colDef.valueGetter` prop. + +In the following example, the row spanning causes the cells with the same values in a column to be merged. {{"demo": "RowSpanning.js", "bg": "inline", "defaultCodeOpen": false}} +## Customizing row spanned cells + +You could customize the value used in row spanning computation using `colDef.rowSpanValueGetter` prop and both the value used in row spanning computation and the value used in cell using `colDef.valueGetter` prop. + +This could be useful when there _are_ some repeating values but they belong to different groups. + +In the following example, `rowSpanValueGetter` is used to avoid merging `age` cells that do not belong to the same person. + +{{"demo": "RowSpanningCustom.js", "bg": "inline", "defaultCodeOpen": false}} + +## Demo + +Here's the calender demo that you can see in the column spanning [documentation](/x/react-data-grid/column-spanning/#function-signature), but implemented with row spanning. + +{{"demo": "RowSpanningCalender.js", "bg": "inline", "defaultCodeOpen": false}} + ## API - [DataGrid](/x/api/data-grid/data-grid/) From bf15c9524b7da39f297a767a4a93cc33730fd8f5 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 20 Aug 2024 17:34:11 +0500 Subject: [PATCH 13/47] Some changes on the demos --- docs/data/data-grid/row-spanning/RowSpanning.js | 3 --- docs/data/data-grid/row-spanning/RowSpanning.tsx | 3 --- .../data-grid/row-spanning/RowSpanningCalender.js | 4 ---- .../row-spanning/RowSpanningCalender.tsx | 4 ---- .../row-spanning/RowSpanningCalender.tsx.preview | 15 +++++++++++++++ .../data-grid/row-spanning/RowSpanningCustom.js | 3 --- .../data-grid/row-spanning/RowSpanningCustom.tsx | 3 --- 7 files changed, 15 insertions(+), 20 deletions(-) create mode 100644 docs/data/data-grid/row-spanning/RowSpanningCalender.tsx.preview diff --git a/docs/data/data-grid/row-spanning/RowSpanning.js b/docs/data/data-grid/row-spanning/RowSpanning.js index d5c92e81af64..457de3a427ef 100644 --- a/docs/data/data-grid/row-spanning/RowSpanning.js +++ b/docs/data/data-grid/row-spanning/RowSpanning.js @@ -21,9 +21,6 @@ export default function RowSpanning() { unstable_rowSpanning disableVirtualization sx={{ - '& .MuiDataGrid-row.Mui-hovered': { - backgroundColor: 'transparent', - }, '& .MuiDataGrid-row:hover': { backgroundColor: 'transparent', }, diff --git a/docs/data/data-grid/row-spanning/RowSpanning.tsx b/docs/data/data-grid/row-spanning/RowSpanning.tsx index 319316dd629c..79b70da9d61a 100644 --- a/docs/data/data-grid/row-spanning/RowSpanning.tsx +++ b/docs/data/data-grid/row-spanning/RowSpanning.tsx @@ -21,9 +21,6 @@ export default function RowSpanning() { unstable_rowSpanning disableVirtualization sx={{ - '& .MuiDataGrid-row.Mui-hovered': { - backgroundColor: 'transparent', - }, '& .MuiDataGrid-row:hover': { backgroundColor: 'transparent', }, diff --git a/docs/data/data-grid/row-spanning/RowSpanningCalender.js b/docs/data/data-grid/row-spanning/RowSpanningCalender.js index a9186afd2938..30aef71a55d1 100644 --- a/docs/data/data-grid/row-spanning/RowSpanningCalender.js +++ b/docs/data/data-grid/row-spanning/RowSpanningCalender.js @@ -141,11 +141,7 @@ export default function RowSpanningCalender() { hideFooter showCellVerticalBorder showColumnVerticalBorder - disableColumnReorder sx={{ - '& .MuiDataGrid-row.Mui-hovered': { - backgroundColor: 'transparent', - }, '& .MuiDataGrid-row:hover': { backgroundColor: 'transparent', }, diff --git a/docs/data/data-grid/row-spanning/RowSpanningCalender.tsx b/docs/data/data-grid/row-spanning/RowSpanningCalender.tsx index ee8db7e153b5..368f4c4de9b9 100644 --- a/docs/data/data-grid/row-spanning/RowSpanningCalender.tsx +++ b/docs/data/data-grid/row-spanning/RowSpanningCalender.tsx @@ -152,11 +152,7 @@ export default function RowSpanningCalender() { hideFooter showCellVerticalBorder showColumnVerticalBorder - disableColumnReorder sx={{ - '& .MuiDataGrid-row.Mui-hovered': { - backgroundColor: 'transparent', - }, '& .MuiDataGrid-row:hover': { backgroundColor: 'transparent', }, diff --git a/docs/data/data-grid/row-spanning/RowSpanningCalender.tsx.preview b/docs/data/data-grid/row-spanning/RowSpanningCalender.tsx.preview new file mode 100644 index 000000000000..bba8d8d1ef97 --- /dev/null +++ b/docs/data/data-grid/row-spanning/RowSpanningCalender.tsx.preview @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/docs/data/data-grid/row-spanning/RowSpanningCustom.js b/docs/data/data-grid/row-spanning/RowSpanningCustom.js index 5e4f81d674b1..28fd09fd379c 100644 --- a/docs/data/data-grid/row-spanning/RowSpanningCustom.js +++ b/docs/data/data-grid/row-spanning/RowSpanningCustom.js @@ -21,9 +21,6 @@ export default function RowSpanningCustom() { unstable_rowSpanning disableVirtualization sx={{ - '& .MuiDataGrid-row.Mui-hovered': { - backgroundColor: 'transparent', - }, '& .MuiDataGrid-row:hover': { backgroundColor: 'transparent', }, diff --git a/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx b/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx index 4ba725593bde..f35866d67923 100644 --- a/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx +++ b/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx @@ -21,9 +21,6 @@ export default function RowSpanningCustom() { unstable_rowSpanning disableVirtualization sx={{ - '& .MuiDataGrid-row.Mui-hovered': { - backgroundColor: 'transparent', - }, '& .MuiDataGrid-row:hover': { backgroundColor: 'transparent', }, From 427e61da4f571bb0875fb2705b635353f5fbf025 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 20 Aug 2024 19:03:00 +0500 Subject: [PATCH 14/47] Update docs and add a new demo --- .../row-spanning/RowSpanningClassSchedule.js | 159 +++++++++++++++++ .../row-spanning/RowSpanningClassSchedule.tsx | 161 ++++++++++++++++++ .../data-grid/row-spanning/row-spanning.md | 18 +- 3 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 docs/data/data-grid/row-spanning/RowSpanningClassSchedule.js create mode 100644 docs/data/data-grid/row-spanning/RowSpanningClassSchedule.tsx diff --git a/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.js b/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.js new file mode 100644 index 000000000000..a480fc16c4b8 --- /dev/null +++ b/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.js @@ -0,0 +1,159 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { DataGrid } from '@mui/x-data-grid'; + +const rows = [ + { + id: 0, + day: 'Monday', + time: '9:00 AM - 10:30 AM', + course: 'Advanced Mathematics', + instructor: 'Dr. Smith', + room: 'Room 101', + notes: 'Midterm exam', + }, + { + id: 1, + day: 'Monday', + time: '10:30 AM - 12:00 PM', + course: 'Advanced Mathematics', + instructor: 'Dr. Smith', + room: 'Room 101', + notes: 'Midterm exam', + }, + { + id: 2, + day: 'Tuesday', + time: '9:00 AM - 10:30 AM', + course: 'Advanced Mathematics', + instructor: 'Dr. Smith', + room: 'Room 101', + notes: 'Practical and lab work', + }, + { + id: 3, + day: 'Tuesday', + time: '10:30 AM - 12:00 PM', + course: 'Introduction to Biology', + instructor: 'Dr. Johnson', + room: 'Room 107', + notes: 'Lab session', + }, + { + id: 4, + day: 'Wednesday', + time: '9:00 AM - 10:30 AM', + course: 'Computer Science 101', + instructor: 'Dr. Lee', + room: 'Room 303', + notes: 'Class', + }, + { + id: 5, + day: 'Wednesday', + time: '10:30 AM - 12:00 PM', + course: 'Computer Science 101', + instructor: 'Dr. Lee', + room: 'Room 303', + notes: 'Lab session', + }, + { + id: 6, + day: 'Thursday', + time: '9:00 AM - 11:00 AM', + course: 'Physics II', + instructor: 'Dr. Carter', + room: 'Room 104', + notes: 'Project Discussion', + }, + { + id: 7, + day: 'Thursday', + time: '11:00 AM - 12:30 PM', + course: 'Physics II', + instructor: 'Dr. Carter', + room: 'Room 104', + notes: 'Project Discussion', + }, + { + id: 8, + day: 'Friday', + time: '9:00 AM - 11:00 AM', + course: 'Physics II', + instructor: 'Dr. Carter', + room: 'Room 104', + notes: 'Project Submission', + }, + { + id: 9, + day: 'Friday', + time: '11:00 AM - 12:30 PM', + course: 'Literature & Composition', + instructor: 'Prof. Adams', + room: 'Lecture Hall 1', + notes: 'Reading Assignment', + }, +]; + +const columns = [ + { + field: 'day', + headerName: 'Day', + }, + { + field: 'time', + headerName: 'Time', + minWidth: 160, + }, + { + field: 'course', + headerName: 'Course', + minWidth: 140, + colSpan: 2, + valueGetter: (_, row) => `${row?.course} (${row?.instructor})`, + cellClassName: 'course-instructor--cell', + }, + { + field: 'instructor', + headerName: 'Instructor', + minWidth: 140, + hideable: false, + }, + { + field: 'room', + headerName: 'Room', + minWidth: 120, + }, + { + field: 'notes', + headerName: 'Notes', + minWidth: 180, + }, +]; + +export default function RowSpanningClassSchedule() { + return ( + + + + ); +} diff --git a/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.tsx b/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.tsx new file mode 100644 index 000000000000..274772bb16af --- /dev/null +++ b/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.tsx @@ -0,0 +1,161 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; + +const rows = [ + { + id: 0, + day: 'Monday', + time: '9:00 AM - 10:30 AM', + course: 'Advanced Mathematics', + instructor: 'Dr. Smith', + room: 'Room 101', + notes: 'Midterm exam', + }, + { + id: 1, + day: 'Monday', + time: '10:30 AM - 12:00 PM', + course: 'Advanced Mathematics', + instructor: 'Dr. Smith', + room: 'Room 101', + notes: 'Midterm exam', + }, + { + id: 2, + day: 'Tuesday', + time: '9:00 AM - 10:30 AM', + course: 'Advanced Mathematics', + instructor: 'Dr. Smith', + room: 'Room 101', + notes: 'Practical and lab work', + }, + { + id: 3, + day: 'Tuesday', + time: '10:30 AM - 12:00 PM', + course: 'Introduction to Biology', + instructor: 'Dr. Johnson', + room: 'Room 107', + notes: 'Lab session', + }, + { + id: 4, + day: 'Wednesday', + time: '9:00 AM - 10:30 AM', + course: 'Computer Science 101', + instructor: 'Dr. Lee', + room: 'Room 303', + notes: 'Class', + }, + { + id: 5, + day: 'Wednesday', + time: '10:30 AM - 12:00 PM', + course: 'Computer Science 101', + instructor: 'Dr. Lee', + room: 'Room 303', + notes: 'Lab session', + }, + { + id: 6, + day: 'Thursday', + time: '9:00 AM - 11:00 AM', + course: 'Physics II', + instructor: 'Dr. Carter', + room: 'Room 104', + notes: 'Project Discussion', + }, + + { + id: 7, + day: 'Thursday', + time: '11:00 AM - 12:30 PM', + course: 'Physics II', + instructor: 'Dr. Carter', + room: 'Room 104', + notes: 'Project Discussion', + }, + + { + id: 8, + day: 'Friday', + time: '9:00 AM - 11:00 AM', + course: 'Physics II', + instructor: 'Dr. Carter', + room: 'Room 104', + notes: 'Project Submission', + }, + { + id: 9, + day: 'Friday', + time: '11:00 AM - 12:30 PM', + course: 'Literature & Composition', + instructor: 'Prof. Adams', + room: 'Lecture Hall 1', + notes: 'Reading Assignment', + }, +]; + +const columns: GridColDef[] = [ + { + field: 'day', + headerName: 'Day', + }, + { + field: 'time', + headerName: 'Time', + minWidth: 160, + }, + { + field: 'course', + headerName: 'Course', + minWidth: 140, + colSpan: 2, + valueGetter: (_, row) => `${row?.course} (${row?.instructor})`, + cellClassName: 'course-instructor--cell', + }, + { + field: 'instructor', + headerName: 'Instructor', + minWidth: 140, + hideable: false, + }, + { + field: 'room', + headerName: 'Room', + minWidth: 120, + }, + { + field: 'notes', + headerName: 'Notes', + minWidth: 180, + }, +]; + +export default function RowSpanningClassSchedule() { + return ( + + + + ); +} diff --git a/docs/data/data-grid/row-spanning/row-spanning.md b/docs/data/data-grid/row-spanning/row-spanning.md index f9c79dfa0de7..7a51d53aba1d 100644 --- a/docs/data/data-grid/row-spanning/row-spanning.md +++ b/docs/data/data-grid/row-spanning/row-spanning.md @@ -8,25 +8,37 @@ This is very close to the "row spanning" in an HTML `
`. To enable, pass the `unstable_rowSpanning` prop to the Data Grid. -The Data Grid will automatically merge cells with the same value in a specified column. +The Data Grid will automatically merge consecutive cells with the repeating values in the same column. In the following example, the row spanning causes the cells with the same values in a column to be merged. {{"demo": "RowSpanning.js", "bg": "inline", "defaultCodeOpen": false}} +:::warning +The row spanning generally works with features like [sorting](/x/react-data-grid/sorting/) and [filtering](/x/react-data-grid/filtering/), be sure to check if everything works as expected when using it in combination with features like [column spanning](/x/react-data-grid/column-spanning/). +::: + ## Customizing row spanned cells You could customize the value used in row spanning computation using `colDef.rowSpanValueGetter` prop and both the value used in row spanning computation and the value used in cell using `colDef.valueGetter` prop. -This could be useful when there _are_ some repeating values but they belong to different groups. +This could be useful when there _are_ some repeating values but should not be row spanned due to belonging to different entities. In the following example, `rowSpanValueGetter` is used to avoid merging `age` cells that do not belong to the same person. {{"demo": "RowSpanningCustom.js", "bg": "inline", "defaultCodeOpen": false}} +## Usage with column spanning + +Row spanning could be used in conjunction with column spanning to achieve cells that span both rows and columns. + +The following weekly university class schedule uses cells that span both rows and columns. + +{{"demo": "RowSpanningClassSchedule.js", "bg": "inline", "defaultCodeOpen": false}} + ## Demo -Here's the calender demo that you can see in the column spanning [documentation](/x/react-data-grid/column-spanning/#function-signature), but implemented with row spanning. +Here's the familiar calender demo that you might have seen in the column spanning [documentation](/x/react-data-grid/column-spanning/#function-signature), implemented with the row spanning. {{"demo": "RowSpanningCalender.js", "bg": "inline", "defaultCodeOpen": false}} From 7ddcc0a910977d41692cb52cecd2f487616a00aa Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 20 Aug 2024 20:10:07 +0500 Subject: [PATCH 15/47] Support keyboard navigation from column spanned cell to row spanned cell --- .../useGridKeyboardNavigation.ts | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts index 80d72ec59b95..d3c17d5522ed 100644 --- a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts +++ b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts @@ -19,6 +19,7 @@ import { isNavigationKey } from '../../../utils/keyboardUtils'; import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../../../constants/gridDetailPanelToggleField'; import { GridColDef, GridRowEntry, GridRowId } from '../../../models'; import { gridPinnedRowsSelector } from '../rows/gridRowsSelector'; +import { gridColumnFieldsSelector } from '../columns/gridColumnsSelector'; import { gridFocusColumnGroupHeaderSelector } from '../focus'; import { gridColumnGroupsHeaderMaxDepthSelector } from '../columnGrouping/gridColumnGroupsSelector'; import { @@ -112,6 +113,7 @@ export const useGridKeyboardNavigation = ( const rowSpanHiddenCells = useGridSelector(apiRef, gridRowSpanningHiddenCellsSelector); const filteredSortedRowIds = useGridSelector(apiRef, gridFilteredSortedRowIdsSelector); + const columnFields = useGridSelector(apiRef, gridColumnFieldsSelector); const currentPageRows = React.useMemo( () => enrichPageRowsWithPinnedRows(apiRef, initialCurrentPageRows), @@ -127,7 +129,12 @@ export const useGridKeyboardNavigation = ( * TODO replace with apiRef.current.moveFocusToRelativeCell() */ const goToCell = React.useCallback( - (colIndex: number, rowId: GridRowId, closestColumnToUse: 'left' | 'right' = 'left') => { + ( + colIndex: number, + rowId: GridRowId, + closestColumnToUse: 'left' | 'right' = 'left', + rowSpanScanDirection: 'up' | 'down' = 'up', + ) => { const visibleSortedRows = gridExpandedSortedRowEntriesSelector(apiRef); const nextCellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo(rowId, colIndex); if (nextCellColSpanInfo && nextCellColSpanInfo.spannedByColSpan) { @@ -137,16 +144,23 @@ export const useGridKeyboardNavigation = ( colIndex = nextCellColSpanInfo.rightVisibleCellIndex; } } + const nonRowSpannedRowId = findNonRowSpannedCell( + rowId, + columnFields[colIndex], + rowSpanScanDirection, + ); // `scrollToIndexes` requires a rowIndex relative to all visible rows. // Those rows do not include pinned rows, but pinned rows do not need scroll anyway. - const rowIndexRelativeToAllRows = visibleSortedRows.findIndex((row) => row.id === rowId); + const rowIndexRelativeToAllRows = visibleSortedRows.findIndex( + (row) => row.id === nonRowSpannedRowId, + ); logger.debug(`Navigating to cell row ${rowIndexRelativeToAllRows}, col ${colIndex}`); apiRef.current.scrollToIndexes({ colIndex, rowIndex: rowIndexRelativeToAllRows, }); const field = apiRef.current.getVisibleColumns()[colIndex].field; - apiRef.current.setCellFocus(rowId, field); + apiRef.current.setCellFocus(nonRowSpannedRowId, field); }, [apiRef, logger], ); @@ -517,18 +531,19 @@ export const useGridKeyboardNavigation = ( ); const findNonRowSpannedCell = React.useCallback( - (rowId: GridRowId, field: GridColDef['field'], direction: 'up' | 'down') => { + (rowId: GridRowId, field: GridColDef['field'], rowSpanScanDirection: 'up' | 'down') => { if (!rowSpanHiddenCells[rowId]?.[field]) { return rowId; } - // find closest non row spanned cell in the given `direction` - let nextRowIndex = filteredSortedRowIds.indexOf(rowId) + (direction === 'down' ? 1 : -1); + // find closest non row spanned cell in the given `rowSpanScanDirection` + let nextRowIndex = + filteredSortedRowIds.indexOf(rowId) + (rowSpanScanDirection === 'down' ? 1 : -1); while (nextRowIndex >= 0 && nextRowIndex < filteredSortedRowIds.length) { const nextRowId = filteredSortedRowIds[nextRowIndex]; if (!rowSpanHiddenCells[nextRowId]?.[field]) { return nextRowId; } - nextRowIndex += direction === 'down' ? 1 : -1; + nextRowIndex += rowSpanScanDirection === 'down' ? 1 : -1; } return rowId; }, @@ -579,24 +594,14 @@ export const useGridKeyboardNavigation = ( case 'ArrowDown': { // "Enter" is only triggered by the row / cell editing feature if (rowIndexBefore < lastRowIndexInPage) { - const rowId = findNonRowSpannedCell( - getRowIdFromIndex(rowIndexBefore + 1), - (params as GridCellParams).field, - 'down', - ); - goToCell(colIndexBefore, rowId); + goToCell(colIndexBefore, getRowIdFromIndex(rowIndexBefore + 1), 'left', 'down'); } break; } case 'ArrowUp': { if (rowIndexBefore > firstRowIndexInPage) { - const rowId = findNonRowSpannedCell( - getRowIdFromIndex(rowIndexBefore - 1), - (params as GridCellParams).field, - 'up', - ); - goToCell(colIndexBefore, rowId); + goToCell(colIndexBefore, getRowIdFromIndex(rowIndexBefore - 1)); } else if (headerFilteringEnabled) { goToHeaderFilter(colIndexBefore, event); } else { @@ -613,13 +618,11 @@ export const useGridKeyboardNavigation = ( direction, }); if (rightColIndex !== null) { - const rightColField = apiRef.current.getVisibleColumns()[rightColIndex].field; - const rowId = findNonRowSpannedCell( + goToCell( + rightColIndex, getRowIdFromIndex(rowIndexBefore), - rightColField, - 'up', + direction === 'rtl' ? 'left' : 'right', ); - goToCell(rightColIndex, rowId, direction === 'rtl' ? 'left' : 'right'); } break; } @@ -632,13 +635,11 @@ export const useGridKeyboardNavigation = ( direction, }); if (leftColIndex !== null) { - const leftColField = apiRef.current.getVisibleColumns()[leftColIndex].field; - const rowId = findNonRowSpannedCell( + goToCell( + leftColIndex, getRowIdFromIndex(rowIndexBefore), - leftColField, - 'up', + direction === 'rtl' ? 'right' : 'left', ); - goToCell(leftColIndex, rowId, direction === 'rtl' ? 'right' : 'left'); } break; } From 7c8dfbc3bfe83c5931acc525abbe83250c5bbb30 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 21 Aug 2024 18:41:35 +0500 Subject: [PATCH 16/47] Improvement --- .../useGridKeyboardNavigation.ts | 110 +++--------------- .../features/keyboardNavigation/utils.ts | 85 ++++++++++++++ 2 files changed, 101 insertions(+), 94 deletions(-) create mode 100644 packages/x-data-grid/src/hooks/features/keyboardNavigation/utils.ts diff --git a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts index d3c17d5522ed..fbcd1faa72d3 100644 --- a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts +++ b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts @@ -7,18 +7,14 @@ import { gridVisibleColumnDefinitionsSelector } from '../columns/gridColumnsSele import { useGridLogger } from '../../utils/useGridLogger'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; -import { - gridExpandedSortedRowEntriesSelector, - gridFilteredSortedRowIdsSelector, -} from '../filter/gridFilterSelector'; +import { gridExpandedSortedRowEntriesSelector } from '../filter/gridFilterSelector'; import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; import { GRID_CHECKBOX_SELECTION_COL_DEF } from '../../../colDef/gridCheckboxSelectionColDef'; import { gridClasses } from '../../../constants/gridClasses'; import { GridCellModes } from '../../../models/gridEditRowModel'; import { isNavigationKey } from '../../../utils/keyboardUtils'; import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../../../constants/gridDetailPanelToggleField'; -import { GridColDef, GridRowEntry, GridRowId } from '../../../models'; -import { gridPinnedRowsSelector } from '../rows/gridRowsSelector'; +import { GridRowEntry, GridRowId } from '../../../models'; import { gridColumnFieldsSelector } from '../columns/gridColumnsSelector'; import { gridFocusColumnGroupHeaderSelector } from '../focus'; import { gridColumnGroupsHeaderMaxDepthSelector } from '../columnGrouping/gridColumnGroupsSelector'; @@ -28,63 +24,12 @@ import { } from '../headerFiltering/gridHeaderFilteringSelectors'; import { GridPipeProcessor, useGridRegisterPipeProcessor } from '../../core/pipeProcessing'; import { isEventTargetInPortal } from '../../../utils/domUtils'; -import { useGridSelector } from '../../utils/useGridSelector'; -import { gridRowSpanningHiddenCellsSelector } from '../rows/gridRowSpanningSelectors'; - -function enrichPageRowsWithPinnedRows( - apiRef: React.MutableRefObject, - rows: GridRowEntry[], -) { - const pinnedRows = gridPinnedRowsSelector(apiRef) || {}; - - return [...(pinnedRows.top || []), ...rows, ...(pinnedRows.bottom || [])]; -} - -const getLeftColumnIndex = ({ - currentColIndex, - firstColIndex, - lastColIndex, - direction, -}: { - currentColIndex: number; - firstColIndex: number; - lastColIndex: number; - direction: 'rtl' | 'ltr'; -}) => { - if (direction === 'rtl') { - if (currentColIndex < lastColIndex) { - return currentColIndex + 1; - } - } else if (direction === 'ltr') { - if (currentColIndex > firstColIndex) { - return currentColIndex - 1; - } - } - return null; -}; - -const getRightColumnIndex = ({ - currentColIndex, - firstColIndex, - lastColIndex, - direction, -}: { - currentColIndex: number; - firstColIndex: number; - lastColIndex: number; - direction: 'rtl' | 'ltr'; -}) => { - if (direction === 'rtl') { - if (currentColIndex > firstColIndex) { - return currentColIndex - 1; - } - } else if (direction === 'ltr') { - if (currentColIndex < lastColIndex) { - return currentColIndex + 1; - } - } - return null; -}; +import { + enrichPageRowsWithPinnedRows, + getLeftColumnIndex, + getRightColumnIndex, + findNonRowSpannedCell, +} from './utils'; /** * @requires useGridSorting (method) - can be after @@ -111,10 +56,6 @@ export const useGridKeyboardNavigation = ( const initialCurrentPageRows = useGridVisibleRows(apiRef, props).rows; const theme = useTheme(); - const rowSpanHiddenCells = useGridSelector(apiRef, gridRowSpanningHiddenCellsSelector); - const filteredSortedRowIds = useGridSelector(apiRef, gridFilteredSortedRowIdsSelector); - const columnFields = useGridSelector(apiRef, gridColumnFieldsSelector); - const currentPageRows = React.useMemo( () => enrichPageRowsWithPinnedRows(apiRef, initialCurrentPageRows), [apiRef, initialCurrentPageRows], @@ -144,11 +85,8 @@ export const useGridKeyboardNavigation = ( colIndex = nextCellColSpanInfo.rightVisibleCellIndex; } } - const nonRowSpannedRowId = findNonRowSpannedCell( - rowId, - columnFields[colIndex], - rowSpanScanDirection, - ); + const field = gridColumnFieldsSelector(apiRef)[colIndex]; + const nonRowSpannedRowId = findNonRowSpannedCell(apiRef, rowId, field, rowSpanScanDirection); // `scrollToIndexes` requires a rowIndex relative to all visible rows. // Those rows do not include pinned rows, but pinned rows do not need scroll anyway. const rowIndexRelativeToAllRows = visibleSortedRows.findIndex( @@ -159,7 +97,6 @@ export const useGridKeyboardNavigation = ( colIndex, rowIndex: rowIndexRelativeToAllRows, }); - const field = apiRef.current.getVisibleColumns()[colIndex].field; apiRef.current.setCellFocus(nonRowSpannedRowId, field); }, [apiRef, logger], @@ -530,26 +467,6 @@ export const useGridKeyboardNavigation = ( [apiRef, currentPageRows.length, goToHeader, goToGroupHeader, goToCell, getRowIdFromIndex], ); - const findNonRowSpannedCell = React.useCallback( - (rowId: GridRowId, field: GridColDef['field'], rowSpanScanDirection: 'up' | 'down') => { - if (!rowSpanHiddenCells[rowId]?.[field]) { - return rowId; - } - // find closest non row spanned cell in the given `rowSpanScanDirection` - let nextRowIndex = - filteredSortedRowIds.indexOf(rowId) + (rowSpanScanDirection === 'down' ? 1 : -1); - while (nextRowIndex >= 0 && nextRowIndex < filteredSortedRowIds.length) { - const nextRowId = filteredSortedRowIds[nextRowIndex]; - if (!rowSpanHiddenCells[nextRowId]?.[field]) { - return nextRowId; - } - nextRowIndex += rowSpanScanDirection === 'down' ? 1 : -1; - } - return rowId; - }, - [filteredSortedRowIds, rowSpanHiddenCells], - ); - const handleCellKeyDown = React.useCallback>( (params, event) => { // Ignore portal @@ -594,7 +511,12 @@ export const useGridKeyboardNavigation = ( case 'ArrowDown': { // "Enter" is only triggered by the row / cell editing feature if (rowIndexBefore < lastRowIndexInPage) { - goToCell(colIndexBefore, getRowIdFromIndex(rowIndexBefore + 1), 'left', 'down'); + goToCell( + colIndexBefore, + getRowIdFromIndex(rowIndexBefore + 1), + direction === 'rtl' ? 'left' : 'right', + 'down', + ); } break; } diff --git a/packages/x-data-grid/src/hooks/features/keyboardNavigation/utils.ts b/packages/x-data-grid/src/hooks/features/keyboardNavigation/utils.ts new file mode 100644 index 000000000000..853f0b0d3470 --- /dev/null +++ b/packages/x-data-grid/src/hooks/features/keyboardNavigation/utils.ts @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { gridFilteredSortedRowIdsSelector } from '../filter/gridFilterSelector'; +import { GridColDef, GridRowEntry, GridRowId } from '../../../models'; +import { gridRowSpanningHiddenCellsSelector } from '../rows/gridRowSpanningSelectors'; +import { GridApiCommunity } from '../../../models/api/gridApiCommunity'; +import { gridPinnedRowsSelector } from '../rows/gridRowsSelector'; + +export function enrichPageRowsWithPinnedRows( + apiRef: React.MutableRefObject, + rows: GridRowEntry[], +) { + const pinnedRows = gridPinnedRowsSelector(apiRef) || {}; + + return [...(pinnedRows.top || []), ...rows, ...(pinnedRows.bottom || [])]; +} + +export const getLeftColumnIndex = ({ + currentColIndex, + firstColIndex, + lastColIndex, + direction, +}: { + currentColIndex: number; + firstColIndex: number; + lastColIndex: number; + direction: 'rtl' | 'ltr'; +}) => { + if (direction === 'rtl') { + if (currentColIndex < lastColIndex) { + return currentColIndex + 1; + } + } else if (direction === 'ltr') { + if (currentColIndex > firstColIndex) { + return currentColIndex - 1; + } + } + return null; +}; + +export const getRightColumnIndex = ({ + currentColIndex, + firstColIndex, + lastColIndex, + direction, +}: { + currentColIndex: number; + firstColIndex: number; + lastColIndex: number; + direction: 'rtl' | 'ltr'; +}) => { + if (direction === 'rtl') { + if (currentColIndex > firstColIndex) { + return currentColIndex - 1; + } + } else if (direction === 'ltr') { + if (currentColIndex < lastColIndex) { + return currentColIndex + 1; + } + } + return null; +}; + +export function findNonRowSpannedCell( + apiRef: React.MutableRefObject, + rowId: GridRowId, + field: GridColDef['field'], + rowSpanScanDirection: 'up' | 'down', +) { + const rowSpanHiddenCells = gridRowSpanningHiddenCellsSelector(apiRef); + if (!rowSpanHiddenCells[rowId]?.[field]) { + return rowId; + } + const filteredSortedRowIds = gridFilteredSortedRowIdsSelector(apiRef); + // find closest non row spanned cell in the given `rowSpanScanDirection` + let nextRowIndex = + filteredSortedRowIds.indexOf(rowId) + (rowSpanScanDirection === 'down' ? 1 : -1); + while (nextRowIndex >= 0 && nextRowIndex < filteredSortedRowIds.length) { + const nextRowId = filteredSortedRowIds[nextRowIndex]; + if (!rowSpanHiddenCells[nextRowId]?.[field]) { + return nextRowId; + } + nextRowIndex += rowSpanScanDirection === 'down' ? 1 : -1; + } + return rowId; +} From 5a121b1e293df37ba86328a5842b5132cc3a8732 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 21 Aug 2024 18:51:37 +0500 Subject: [PATCH 17/47] Lint --- .../keyboardNavigation/useGridKeyboardNavigation.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts index fbcd1faa72d3..73bd89cf060b 100644 --- a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts +++ b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts @@ -1,9 +1,12 @@ import * as React from 'react'; import { useTheme } from '@mui/material/styles'; import { GridEventListener } from '../../../models/events'; -import { GridApiCommunity, GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; +import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { GridCellParams } from '../../../models/params/gridCellParams'; -import { gridVisibleColumnDefinitionsSelector } from '../columns/gridColumnsSelector'; +import { + gridVisibleColumnDefinitionsSelector, + gridColumnFieldsSelector, +} from '../columns/gridColumnsSelector'; import { useGridLogger } from '../../utils/useGridLogger'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; @@ -14,8 +17,7 @@ import { gridClasses } from '../../../constants/gridClasses'; import { GridCellModes } from '../../../models/gridEditRowModel'; import { isNavigationKey } from '../../../utils/keyboardUtils'; import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../../../constants/gridDetailPanelToggleField'; -import { GridRowEntry, GridRowId } from '../../../models'; -import { gridColumnFieldsSelector } from '../columns/gridColumnsSelector'; +import { GridRowId } from '../../../models'; import { gridFocusColumnGroupHeaderSelector } from '../focus'; import { gridColumnGroupsHeaderMaxDepthSelector } from '../columnGrouping/gridColumnGroupsSelector'; import { @@ -650,7 +652,6 @@ export const useGridKeyboardNavigation = ( apiRef, currentPageRows, theme.direction, - findNonRowSpannedCell, getRowIdFromIndex, goToCell, headerFilteringEnabled, From c169d6caaf59d1fb3165f96e5ce14dc9861aeb0a Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 21 Aug 2024 20:05:17 +0500 Subject: [PATCH 18/47] Refactor --- .../src/components/cell/GridCell.tsx | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/x-data-grid/src/components/cell/GridCell.tsx b/packages/x-data-grid/src/components/cell/GridCell.tsx index 83b6c9238582..d3502e158e17 100644 --- a/packages/x-data-grid/src/components/cell/GridCell.tsx +++ b/packages/x-data-grid/src/components/cell/GridCell.tsx @@ -212,6 +212,9 @@ const GridCell = React.forwardRef(function GridCe }), ); + const hiddenCells = useGridSelector(apiRef, gridRowSpanningHiddenCellsSelector); + const spannedCells = useGridSelector(apiRef, gridRowSpanningSpannedCellsSelector); + const { cellMode, hasFocus, isEditable = false, value } = cellParams; const canManageOwnFocus = @@ -323,6 +326,9 @@ const GridCell = React.forwardRef(function GridCe [apiRef, field, rowId], ); + const isCellRowSpanned = hiddenCells[rowId]?.[field] ?? false; + const rowSpan = spannedCells[rowId]?.[field] ?? 1; + const style = React.useMemo(() => { if (isNotVisible) { return { @@ -346,8 +352,13 @@ const GridCell = React.forwardRef(function GridCe cellStyle.right = pinnedOffset; } + if (rowSpan > 1) { + cellStyle.height = `calc(var(--height) * ${rowSpan})`; + cellStyle.zIndex = 5; + } + return cellStyle; - }, [width, isNotVisible, styleProp, pinnedOffset, pinnedPosition]); + }, [width, isNotVisible, styleProp, pinnedOffset, pinnedPosition, rowSpan]); React.useEffect(() => { if (!hasFocus || cellMode === GridCellModes.Edit) { @@ -370,8 +381,14 @@ const GridCell = React.forwardRef(function GridCe } }, [hasFocus, cellMode, apiRef]); - const hiddenCells = useGridSelector(apiRef, gridRowSpanningHiddenCellsSelector); - const spannedCells = useGridSelector(apiRef, gridRowSpanningSpannedCellsSelector); + if (isCellRowSpanned) { + return ( +
+ ); + } if (cellParams === EMPTY_CELL_PARAMS) { return null; @@ -453,17 +470,6 @@ const GridCell = React.forwardRef(function GridCe onDragOver: publish('cellDragOver', onDragOver), }; - const isHidden = hiddenCells[rowId]?.[field] ?? false; - if (isHidden) { - return ( -
- ); - } - const rowSpan = spannedCells[rowId]?.[field] ?? 1; - return (
(function GridCe aria-colindex={colIndex + 1} aria-colspan={colSpan} aria-rowspan={rowSpan} - style={ - rowSpan === 1 - ? style - : { - ...style, - height: `calc(var(--height) * ${rowSpan})`, - zIndex: 5, - } - } + style={style} title={title} tabIndex={tabIndex} onClick={publish('cellClick', onClick)} From c85f2e74bd2b5384de3ca5e26ced526f57f08577 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 21 Aug 2024 20:07:30 +0500 Subject: [PATCH 19/47] Change an example --- .../data-grid/row-spanning/RowSpanning.js | 170 +++++------------- .../data-grid/row-spanning/RowSpanning.tsx | 170 +++++------------- 2 files changed, 92 insertions(+), 248 deletions(-) diff --git a/docs/data/data-grid/row-spanning/RowSpanning.js b/docs/data/data-grid/row-spanning/RowSpanning.js index 457de3a427ef..83d683674700 100644 --- a/docs/data/data-grid/row-spanning/RowSpanning.js +++ b/docs/data/data-grid/row-spanning/RowSpanning.js @@ -32,160 +32,82 @@ export default function RowSpanning() { const columns = [ { - field: 'event', - headerName: 'Event', - width: 200, - editable: true, + field: 'code', + headerName: 'Item Code', + width: 85, }, { - field: 'indicator', - headerName: 'Indicator', - width: 150, - editable: true, + field: 'description', + headerName: 'Description', + width: 170, }, { - field: 'action', - headerName: 'Action', - width: 150, - editable: true, + field: 'quantity', + headerName: 'Quantity', + width: 80, + // Do not span the values + rowSpanValueGetter: () => null, }, { - field: 'decision', - headerName: 'Decision', + field: 'unitPrice', + headerName: 'Unit Price', type: 'number', - width: 100, + valueFormatter: (value) => `$${value}.00`, }, { - field: 'location', - headerName: 'Location', + field: 'totalPrice', + headerName: 'Total Price', type: 'number', - width: 100, - rowSpanValueGetter: () => { - // Exclude this column from row spanning irrespective of the values - return undefined; - }, + valueGetter: (value, row) => value ?? row?.unitPrice, + valueFormatter: (value) => `$${value}.00`, }, ]; const rows = [ { id: 1, - event: 'Event 1', - indicator: 'Indicator 1', - action: 'Actions 1', - decision: 1, - location: 2, + code: 'A101', + description: 'Wireless Mouse', + quantity: 2, + unitPrice: 50, + totalPrice: 100, }, { id: 2, - event: 'Event 1', - indicator: 'Indicator 1', - action: 'Actions 2', - decision: 1, - location: 3, + code: 'A102', + description: 'Mechanical Keyboard', + quantity: 1, + unitPrice: 75, }, { id: 3, - event: 'Event 1', - indicator: 'Indicator 1', - action: 'Actions 3', - decision: 1, - location: 1, + code: 'A103', + description: 'USB Dock Station', + quantity: 1, + unitPrice: 400, }, { id: 4, - event: 'Event 1', - indicator: 'Indicator 2', - action: 'Actions 1', - decision: 4, - location: 3, + code: 'A104', + description: 'Laptop', + quantity: 1, + unitPrice: 1800, + totalPrice: 2050, }, { id: 5, - event: 'Event 1', - indicator: 'Indicator 2', - action: 'Actions 2', - decision: 4, - location: 3, + code: 'A104', + description: '- 16GB RAM Upgrade', + quantity: 1, + unitPrice: 100, + totalPrice: 2050, }, { id: 6, - event: 'Event 2', - indicator: 'Indicator 1', - action: 'Actions 1', - decision: 6, - location: 1, - }, - { - id: 7, - event: 'Event 2', - indicator: 'Indicator 1', - action: 'Actions 2', - decision: 6, - location: 2, - }, - { - id: 8, - event: 'Event 2', - indicator: 'Indicator 1', - action: 'Actions 3', - decision: 6, - location: 2, - }, - { - id: 9, - event: 'Event 2', - indicator: 'Indicator 2', - action: 'Actions 1', - decision: 9, - location: 1, - }, - { - id: 10, - event: 'Event 2', - indicator: 'Indicator 2', - action: 'Actions 2', - decision: 9, - location: 4, - }, - { - id: 11, - event: 'Event 3', - indicator: 'Indicator 1', - action: 'Actions 1', - decision: 11, - location: 1, - }, - { - id: 12, - event: 'Event 3', - indicator: 'Indicator 1', - action: 'Actions 2', - decision: 11, - location: 1, - }, - { - id: 13, - event: 'Event 3', - indicator: 'Indicator 1', - action: 'Actions 3', - decision: 11, - location: 2, - }, - { - id: 14, - event: 'Event 3', - indicator: 'Indicator 2', - action: 'Actions 1', - decision: 14, - location: 4, - }, - { - id: 15, - event: 'Event 3', - indicator: 'Indicator 2', - action: 'Actions 2', - decision: 14, - location: 3, + code: 'A104', + description: '- 512GB SSD Upgrade', + quantity: 1, + unitPrice: 150, + totalPrice: 2050, }, ]; diff --git a/docs/data/data-grid/row-spanning/RowSpanning.tsx b/docs/data/data-grid/row-spanning/RowSpanning.tsx index 79b70da9d61a..d41727f1ba11 100644 --- a/docs/data/data-grid/row-spanning/RowSpanning.tsx +++ b/docs/data/data-grid/row-spanning/RowSpanning.tsx @@ -32,160 +32,82 @@ export default function RowSpanning() { const columns: GridColDef<(typeof rows)[number]>[] = [ { - field: 'event', - headerName: 'Event', - width: 200, - editable: true, + field: 'code', + headerName: 'Item Code', + width: 85, }, { - field: 'indicator', - headerName: 'Indicator', - width: 150, - editable: true, + field: 'description', + headerName: 'Description', + width: 170, }, { - field: 'action', - headerName: 'Action', - width: 150, - editable: true, + field: 'quantity', + headerName: 'Quantity', + width: 80, + // Do not span the values + rowSpanValueGetter: () => null, }, { - field: 'decision', - headerName: 'Decision', + field: 'unitPrice', + headerName: 'Unit Price', type: 'number', - width: 100, + valueFormatter: (value) => `$${value}.00`, }, { - field: 'location', - headerName: 'Location', + field: 'totalPrice', + headerName: 'Total Price', type: 'number', - width: 100, - rowSpanValueGetter: () => { - // Exclude this column from row spanning irrespective of the values - return undefined; - }, + valueGetter: (value, row) => value ?? row?.unitPrice, + valueFormatter: (value) => `$${value}.00`, }, ]; const rows = [ { id: 1, - event: 'Event 1', - indicator: 'Indicator 1', - action: 'Actions 1', - decision: 1, - location: 2, + code: 'A101', + description: 'Wireless Mouse', + quantity: 2, + unitPrice: 50, + totalPrice: 100, }, { id: 2, - event: 'Event 1', - indicator: 'Indicator 1', - action: 'Actions 2', - decision: 1, - location: 3, + code: 'A102', + description: 'Mechanical Keyboard', + quantity: 1, + unitPrice: 75, }, { id: 3, - event: 'Event 1', - indicator: 'Indicator 1', - action: 'Actions 3', - decision: 1, - location: 1, + code: 'A103', + description: 'USB Dock Station', + quantity: 1, + unitPrice: 400, }, { id: 4, - event: 'Event 1', - indicator: 'Indicator 2', - action: 'Actions 1', - decision: 4, - location: 3, + code: 'A104', + description: 'Laptop', + quantity: 1, + unitPrice: 1800, + totalPrice: 2050, }, { id: 5, - event: 'Event 1', - indicator: 'Indicator 2', - action: 'Actions 2', - decision: 4, - location: 3, + code: 'A104', + description: '- 16GB RAM Upgrade', + quantity: 1, + unitPrice: 100, + totalPrice: 2050, }, { id: 6, - event: 'Event 2', - indicator: 'Indicator 1', - action: 'Actions 1', - decision: 6, - location: 1, - }, - { - id: 7, - event: 'Event 2', - indicator: 'Indicator 1', - action: 'Actions 2', - decision: 6, - location: 2, - }, - { - id: 8, - event: 'Event 2', - indicator: 'Indicator 1', - action: 'Actions 3', - decision: 6, - location: 2, - }, - { - id: 9, - event: 'Event 2', - indicator: 'Indicator 2', - action: 'Actions 1', - decision: 9, - location: 1, - }, - { - id: 10, - event: 'Event 2', - indicator: 'Indicator 2', - action: 'Actions 2', - decision: 9, - location: 4, - }, - { - id: 11, - event: 'Event 3', - indicator: 'Indicator 1', - action: 'Actions 1', - decision: 11, - location: 1, - }, - { - id: 12, - event: 'Event 3', - indicator: 'Indicator 1', - action: 'Actions 2', - decision: 11, - location: 1, - }, - { - id: 13, - event: 'Event 3', - indicator: 'Indicator 1', - action: 'Actions 3', - decision: 11, - location: 2, - }, - { - id: 14, - event: 'Event 3', - indicator: 'Indicator 2', - action: 'Actions 1', - decision: 14, - location: 4, - }, - { - id: 15, - event: 'Event 3', - indicator: 'Indicator 2', - action: 'Actions 2', - decision: 14, - location: 3, + code: 'A104', + description: '- 512GB SSD Upgrade', + quantity: 1, + unitPrice: 150, + totalPrice: 2050, }, ]; From 74419c468eb67bfc974cadda69c6685780243fa1 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 21 Aug 2024 20:56:21 +0500 Subject: [PATCH 20/47] Improve a demo --- .../data-grid/row-spanning/RowSpanning.js | 69 +++++++++++++------ .../data-grid/row-spanning/RowSpanning.tsx | 69 +++++++++++++------ .../data-grid/row-spanning/row-spanning.md | 8 ++- .../hooks/features/rows/useGridRowSpanning.ts | 12 ++-- 4 files changed, 109 insertions(+), 49 deletions(-) diff --git a/docs/data/data-grid/row-spanning/RowSpanning.js b/docs/data/data-grid/row-spanning/RowSpanning.js index 83d683674700..18de309f185c 100644 --- a/docs/data/data-grid/row-spanning/RowSpanning.js +++ b/docs/data/data-grid/row-spanning/RowSpanning.js @@ -1,31 +1,48 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import { DataGrid } from '@mui/x-data-grid'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Switch from '@mui/material/Switch'; export default function RowSpanning() { + const [enabled, setEnabled] = React.useState(true); + return ( - - + setEnabled(event.target.checked)} + control={} + label="Enable row spanning" /> + + + ); } @@ -35,6 +52,7 @@ const columns = [ field: 'code', headerName: 'Item Code', width: 85, + cellClassName: ({ row }) => (row.summaryRow ? 'bold' : ''), }, { field: 'description', @@ -52,7 +70,7 @@ const columns = [ field: 'unitPrice', headerName: 'Unit Price', type: 'number', - valueFormatter: (value) => `$${value}.00`, + valueFormatter: (value) => (value ? `$${value}.00` : ''), }, { field: 'totalPrice', @@ -60,6 +78,7 @@ const columns = [ type: 'number', valueGetter: (value, row) => value ?? row?.unitPrice, valueFormatter: (value) => `$${value}.00`, + cellClassName: ({ row }) => (row.summaryRow ? 'bold' : ''), }, ]; @@ -110,4 +129,10 @@ const rows = [ unitPrice: 150, totalPrice: 2050, }, + { + id: 7, + code: 'TOTAL', + totalPrice: 2625, + summaryRow: true, + }, ]; diff --git a/docs/data/data-grid/row-spanning/RowSpanning.tsx b/docs/data/data-grid/row-spanning/RowSpanning.tsx index d41727f1ba11..c17090c93514 100644 --- a/docs/data/data-grid/row-spanning/RowSpanning.tsx +++ b/docs/data/data-grid/row-spanning/RowSpanning.tsx @@ -1,31 +1,48 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Switch from '@mui/material/Switch'; export default function RowSpanning() { + const [enabled, setEnabled] = React.useState(true); + return ( - - + setEnabled((event.target as HTMLInputElement).checked)} + control={} + label="Enable row spanning" /> + + + ); } @@ -35,6 +52,7 @@ const columns: GridColDef<(typeof rows)[number]>[] = [ field: 'code', headerName: 'Item Code', width: 85, + cellClassName: ({ row }) => (row.summaryRow ? 'bold' : ''), }, { field: 'description', @@ -52,7 +70,7 @@ const columns: GridColDef<(typeof rows)[number]>[] = [ field: 'unitPrice', headerName: 'Unit Price', type: 'number', - valueFormatter: (value) => `$${value}.00`, + valueFormatter: (value) => (value ? `$${value}.00` : ''), }, { field: 'totalPrice', @@ -60,6 +78,7 @@ const columns: GridColDef<(typeof rows)[number]>[] = [ type: 'number', valueGetter: (value, row) => value ?? row?.unitPrice, valueFormatter: (value) => `$${value}.00`, + cellClassName: ({ row }) => (row.summaryRow ? 'bold' : ''), }, ]; @@ -110,4 +129,10 @@ const rows = [ unitPrice: 150, totalPrice: 2050, }, + { + id: 7, + code: 'TOTAL', + totalPrice: 2625, + summaryRow: true, + }, ]; diff --git a/docs/data/data-grid/row-spanning/row-spanning.md b/docs/data/data-grid/row-spanning/row-spanning.md index 7a51d53aba1d..7d16ff6da03d 100644 --- a/docs/data/data-grid/row-spanning/row-spanning.md +++ b/docs/data/data-grid/row-spanning/row-spanning.md @@ -7,13 +7,19 @@ Row spanning lets you change this default behavior, so cells can span multiple r This is very close to the "row spanning" in an HTML `
`. To enable, pass the `unstable_rowSpanning` prop to the Data Grid. - The Data Grid will automatically merge consecutive cells with the repeating values in the same column. In the following example, the row spanning causes the cells with the same values in a column to be merged. +Switch off the toggle button to see actual rows. {{"demo": "RowSpanning.js", "bg": "inline", "defaultCodeOpen": false}} +:::info +In the above demo, the `quantity` column has been delibrately excluded from row spanning computation by using `colDef.rowSpanValueGetter` prop. + +See the [Customizing row spanned cells](#customizing-row-spanned-cells) section for more details. +::: + :::warning The row spanning generally works with features like [sorting](/x/react-data-grid/sorting/) and [filtering](/x/react-data-grid/filtering/), be sure to check if everything works as expected when using it in combination with features like [column spanning](/x/react-data-grid/column-spanning/). ::: diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index 1c7726f57869..ed0c99f3d887 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -1,5 +1,4 @@ import * as React from 'react'; -import { GridEventListener } from '../../../models/events'; import { GridColDef } from '../../../models/colDef'; import { GridRowId } from '../../../models/gridRows'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; @@ -41,10 +40,11 @@ export const useGridRowSpanning = ( apiRef: React.MutableRefObject, props: Pick, ): void => { - const updateRowSpanningState = React.useCallback< - GridEventListener<'sortedRowsSet' | 'filteredRowsSet'> - >(() => { + const updateRowSpanningState = React.useCallback(() => { if (!props.unstable_rowSpanning) { + if (apiRef.current.state.rowSpanning !== EMPTY_STATE) { + apiRef.current.setState((state) => ({ ...state, rowSpanning: EMPTY_STATE })); + } return; } const spannedCells: Record> = {}; @@ -96,4 +96,8 @@ export const useGridRowSpanning = ( useGridApiEventHandler(apiRef, 'sortedRowsSet', updateRowSpanningState); useGridApiEventHandler(apiRef, 'filteredRowsSet', updateRowSpanningState); + + React.useEffect(() => { + updateRowSpanningState(); + }, [updateRowSpanningState]); }; From 1a4bd79db2a0afe39c60feb2d9825bb890cbf71c Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 21 Aug 2024 20:57:47 +0500 Subject: [PATCH 21/47] Remove stray prop --- docs/data/data-grid/row-spanning/RowSpanningClassSchedule.js | 1 - docs/data/data-grid/row-spanning/RowSpanningClassSchedule.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.js b/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.js index a480fc16c4b8..4d0881c9c34e 100644 --- a/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.js +++ b/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.js @@ -143,7 +143,6 @@ export default function RowSpanningClassSchedule() { hideFooter showCellVerticalBorder showColumnVerticalBorder - disableColumnReorder sx={{ '& .MuiDataGrid-row:hover': { backgroundColor: 'transparent', diff --git a/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.tsx b/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.tsx index 274772bb16af..44e3e28e7c03 100644 --- a/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.tsx +++ b/docs/data/data-grid/row-spanning/RowSpanningClassSchedule.tsx @@ -145,7 +145,6 @@ export default function RowSpanningClassSchedule() { hideFooter showCellVerticalBorder showColumnVerticalBorder - disableColumnReorder sx={{ '& .MuiDataGrid-row:hover': { backgroundColor: 'transparent', From d40f448ba46c5a3dc86258ac522cacf26ee3cf3a Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Sat, 31 Aug 2024 17:41:43 +0500 Subject: [PATCH 22/47] Fix failing of getRow API method --- .../src/hooks/features/rows/useGridRowSpanning.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index ed0c99f3d887..cb814eb37684 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -7,6 +7,7 @@ import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { GridStateInitializer } from '../../utils/useGridInitializeState'; import { gridFilteredSortedRowIdsSelector } from '../filter/gridFilterSelector'; import { gridColumnDefinitionsSelector } from '../columns/gridColumnsSelector'; +import { gridRowsLookupSelector } from './gridRowsSelector'; export interface GridRowSpanningState { spannedCells: Record>; @@ -14,6 +15,7 @@ export interface GridRowSpanningState { } const EMPTY_STATE = { spannedCells: {}, hiddenCells: {} }; +const skippedFields = new Set(['__check__']); export const rowSpanningStateInitializer: GridStateInitializer = (state) => { return { @@ -27,8 +29,11 @@ const getCellValue = ( colDef: GridColDef, apiRef: React.MutableRefObject, ) => { - const row = apiRef.current.getRow(rowId); - let cellValue = row?.[colDef.field]; + const row = gridRowsLookupSelector(apiRef)[rowId]; + if (!row) { + return null; + } + let cellValue = row[colDef.field]; const valueGetter = colDef.rowSpanValueGetter ?? colDef.valueGetter; if (valueGetter) { cellValue = valueGetter(cellValue as never, row, colDef, apiRef); @@ -52,6 +57,9 @@ export const useGridRowSpanning = ( const filteredSortedRowIds = gridFilteredSortedRowIdsSelector(apiRef); const colDefs = gridColumnDefinitionsSelector(apiRef); colDefs.forEach((colDef) => { + if (skippedFields.has(colDef.field)) { + return; + } // TODO Perf: Process rendered rows first and lazily process the rest filteredSortedRowIds.forEach((rowId, index) => { if (hiddenCells[rowId]?.[colDef.field]) { From bfc5c84e7040e5855d91d6945e1297bc39cc99b9 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Sun, 1 Sep 2024 16:55:11 +0500 Subject: [PATCH 23/47] Optimize performance - make it work on rendered subset of rows --- .../hooks/features/rows/useGridRowSpanning.ts | 256 ++++++++++++++---- 1 file changed, 201 insertions(+), 55 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index cb814eb37684..ea0339f74a2e 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -1,13 +1,15 @@ import * as React from 'react'; import { GridColDef } from '../../../models/colDef'; -import { GridRowId } from '../../../models/gridRows'; +import { GridRowId, GridValidRowModel } from '../../../models/gridRows'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import { GridApiCommunity, GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; -import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { GridStateInitializer } from '../../utils/useGridInitializeState'; -import { gridFilteredSortedRowIdsSelector } from '../filter/gridFilterSelector'; +import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { gridColumnDefinitionsSelector } from '../columns/gridColumnsSelector'; -import { gridRowsLookupSelector } from './gridRowsSelector'; +import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; +import { gridRenderContextSelector } from '../virtualization/gridVirtualizationSelectors'; +import { GridRenderContext } from '../../../models'; +import { useGridSelector } from '../../utils/useGridSelector'; export interface GridRowSpanningState { spannedCells: Record>; @@ -15,7 +17,47 @@ export interface GridRowSpanningState { } const EMPTY_STATE = { spannedCells: {}, hiddenCells: {} }; -const skippedFields = new Set(['__check__']); +const EMPTY_RANGE = { firstRowIndex: 0, lastRowIndex: 0 }; +const skippedFields = new Set(['__check__', '__reorder__']); + +function isUninitializedRowContext(renderContext: GridRenderContext) { + return renderContext.firstRowIndex === 0 && renderContext.lastRowIndex === 0; +} + +function getUnprocessedRange( + testRange: { firstRowIndex: number; lastRowIndex: number }, + processedRange: { firstRowIndex: number; lastRowIndex: number }, +) { + if ( + testRange.firstRowIndex >= processedRange.firstRowIndex && + testRange.lastRowIndex <= processedRange.lastRowIndex + ) { + return null; + } + // Overflowing at the end + // Example: testRange={ firstRowIndex: 10, lastRowIndex: 20 }, processedRange={ firstRowIndex: 0, lastRowIndex: 15 } + // Unprocessed Range={ firstRowIndex: 16, lastRowIndex: 20 } + if ( + testRange.firstRowIndex >= processedRange.firstRowIndex && + testRange.lastRowIndex > processedRange.lastRowIndex + ) { + return { firstRowIndex: processedRange.lastRowIndex, lastRowIndex: testRange.lastRowIndex }; + } + // Overflowing at the beginning + // Example: testRange={ firstRowIndex: 0, lastRowIndex: 20 }, processedRange={ firstRowIndex: 16, lastRowIndex: 30 } + // Unprocessed Range={ firstRowIndex: 0, lastRowIndex: 15 } + if ( + testRange.firstRowIndex < processedRange.firstRowIndex && + testRange.lastRowIndex <= processedRange.lastRowIndex + ) { + return { + firstRowIndex: testRange.firstRowIndex, + lastRowIndex: processedRange.firstRowIndex - 1, + }; + } + // TODO: Should return two ranges handle overflowing at both ends ? + return testRange; +} export const rowSpanningStateInitializer: GridStateInitializer = (state) => { return { @@ -25,11 +67,10 @@ export const rowSpanningStateInitializer: GridStateInitializer = (state) => { }; const getCellValue = ( - rowId: GridRowId, + row: GridValidRowModel, colDef: GridColDef, apiRef: React.MutableRefObject, ) => { - const row = gridRowsLookupSelector(apiRef)[rowId]; if (!row) { return null; } @@ -43,69 +84,174 @@ const getCellValue = ( export const useGridRowSpanning = ( apiRef: React.MutableRefObject, - props: Pick, + props: Pick, ): void => { - const updateRowSpanningState = React.useCallback(() => { - if (!props.unstable_rowSpanning) { - if (apiRef.current.state.rowSpanning !== EMPTY_STATE) { - apiRef.current.setState((state) => ({ ...state, rowSpanning: EMPTY_STATE })); + const { range, rows: visibleRows } = useGridVisibleRows(apiRef, props); + const renderContext = useGridSelector(apiRef, gridRenderContextSelector); + const processedRange = React.useRef<{ firstRowIndex: number; lastRowIndex: number }>(EMPTY_RANGE); + + const updateRowSpanningState = React.useCallback( + // A reset needs to occur when: + // - The `unstable_rowSpanning` prop is updated (feature flag) + // - The filtering is applied + // - The sorting is applied + // - The `paginationModel` is updated + // - The rows are updated + (resetState: boolean = true) => { + if (!props.unstable_rowSpanning) { + if (apiRef.current.state.rowSpanning !== EMPTY_STATE) { + apiRef.current.setState((state) => ({ ...state, rowSpanning: EMPTY_STATE })); + } + return; } - return; - } - const spannedCells: Record> = {}; - const hiddenCells: Record> = {}; - const filteredSortedRowIds = gridFilteredSortedRowIdsSelector(apiRef); - const colDefs = gridColumnDefinitionsSelector(apiRef); - colDefs.forEach((colDef) => { - if (skippedFields.has(colDef.field)) { + + if (range === null || isUninitializedRowContext(renderContext)) { return; } - // TODO Perf: Process rendered rows first and lazily process the rest - filteredSortedRowIds.forEach((rowId, index) => { - if (hiddenCells[rowId]?.[colDef.field]) { - return; - } - const cellValue = getCellValue(rowId, colDef, apiRef); - if (cellValue == null) { + const newSpannedCells = resetState + ? {} + : { ...apiRef.current.state.rowSpanning.spannedCells }; + const newHiddenCells = resetState ? {} : { ...apiRef.current.state.rowSpanning.hiddenCells }; + + const colDefs = gridColumnDefinitionsSelector(apiRef); + + if (resetState) { + processedRange.current = EMPTY_RANGE; + } + + const rangeToProcess = getUnprocessedRange( + { + firstRowIndex: renderContext.firstRowIndex, + lastRowIndex: renderContext.lastRowIndex - 1, + }, + processedRange.current, + ); + + if (rangeToProcess === null) { + return; + } + + colDefs.forEach((colDef) => { + if (skippedFields.has(colDef.field)) { return; } - // for each valid cell value, check if subsequent rows have the same value - let relativeIndex = index + 1; - let rowSpan = 0; - while (getCellValue(filteredSortedRowIds[relativeIndex], colDef, apiRef) === cellValue) { - if (hiddenCells[filteredSortedRowIds[relativeIndex]]) { - hiddenCells[filteredSortedRowIds[relativeIndex]][colDef.field] = true; - } else { - hiddenCells[filteredSortedRowIds[relativeIndex]] = { [colDef.field]: true }; + + for ( + let index = rangeToProcess.firstRowIndex; + index <= rangeToProcess.lastRowIndex; + index += 1 + ) { + const row = visibleRows[index]; + + if (newHiddenCells[row.id]?.[colDef.field]) { + continue; + } + const cellValue = getCellValue(row.model, colDef, apiRef); + + if (cellValue == null) { + continue; + } + + let rowSpanId = row.id; + let rowSpan = 0; + + // For first index, also scan in the previous rows to handle the reset state case e.g by sorting + if (index === rangeToProcess.firstRowIndex) { + let prevIndex = index - 1; + const prevRowEntry = visibleRows[prevIndex]; + while ( + prevIndex >= range.firstRowIndex && + getCellValue(prevRowEntry.model, colDef, apiRef) === cellValue + ) { + const currentRow = visibleRows[prevIndex + 1]; + if (newHiddenCells[currentRow.id]) { + newHiddenCells[currentRow.id][colDef.field] = true; + } else { + newHiddenCells[currentRow.id] = { [colDef.field]: true }; + } + rowSpan += 1; + rowSpanId = prevRowEntry.id; + prevIndex -= 1; + } + } + + let relativeIndex = index + 1; + while ( + relativeIndex <= range.lastRowIndex && + visibleRows[relativeIndex] && + getCellValue(visibleRows[relativeIndex].model, colDef, apiRef) === cellValue + ) { + const currentRow = visibleRows[relativeIndex]; + if (newHiddenCells[currentRow.id]) { + newHiddenCells[currentRow.id][colDef.field] = true; + } else { + newHiddenCells[currentRow.id] = { [colDef.field]: true }; + } + relativeIndex += 1; + rowSpan += 1; } - relativeIndex += 1; - rowSpan += 1; - } - if (rowSpan > 0) { - if (spannedCells[rowId]) { - spannedCells[rowId][colDef.field] = rowSpan + 1; - } else { - spannedCells[rowId] = { [colDef.field]: rowSpan + 1 }; + if (rowSpan > 0) { + if (newSpannedCells[rowSpanId]) { + newSpannedCells[rowSpanId][colDef.field] = rowSpan + 1; + } else { + newSpannedCells[rowSpanId] = { [colDef.field]: rowSpan + 1 }; + } } } + processedRange.current = { + firstRowIndex: Math.min( + processedRange.current.firstRowIndex, + rangeToProcess.firstRowIndex, + ), + lastRowIndex: Math.max(processedRange.current.lastRowIndex, rangeToProcess.lastRowIndex), + }; }); - }); - apiRef.current.setState((state) => ({ - ...state, - rowSpanning: { - spannedCells, - hiddenCells, - }, - })); - }, [apiRef, props.unstable_rowSpanning]); + const newSpannedCellsCount = Object.keys(newSpannedCells).length; + const newHiddenCellsCount = Object.keys(newHiddenCells).length; + const currentSpannedCellsCount = Object.keys( + apiRef.current.state.rowSpanning.spannedCells, + ).length; + const currentHiddenCellsCount = Object.keys( + apiRef.current.state.rowSpanning.hiddenCells, + ).length; - useGridApiEventHandler(apiRef, 'sortedRowsSet', updateRowSpanningState); - useGridApiEventHandler(apiRef, 'filteredRowsSet', updateRowSpanningState); + const shouldUpdateState = + resetState || + newSpannedCellsCount !== currentSpannedCellsCount || + newHiddenCellsCount !== currentHiddenCellsCount; + + if (!shouldUpdateState) { + return; + } + apiRef.current.setState((state) => { + return { + ...state, + rowSpanning: { + spannedCells: newSpannedCells, + hiddenCells: newHiddenCells, + }, + }; + }); + }, + [apiRef, props.unstable_rowSpanning, range, renderContext, visibleRows], + ); + + const prevRenderContext = React.useRef(renderContext); + const isFirstRender = React.useRef(true); React.useEffect(() => { + const firstRender = isFirstRender.current; + if (isFirstRender.current) { + isFirstRender.current = false; + } + if (!firstRender && prevRenderContext.current !== renderContext) { + prevRenderContext.current = renderContext; + updateRowSpanningState(false); + return; + } updateRowSpanningState(); - }, [updateRowSpanningState]); + }, [updateRowSpanningState, renderContext]); }; From aabb50d8e0dfdb80580ce3e935a5a112018a83e7 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Sun, 1 Sep 2024 17:00:06 +0500 Subject: [PATCH 24/47] Avoid reacting to column-only context changes --- .../src/hooks/features/rows/useGridRowSpanning.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index ea0339f74a2e..a7e92d64c3c3 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -4,7 +4,6 @@ import { GridRowId, GridValidRowModel } from '../../../models/gridRows'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import { GridApiCommunity, GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { GridStateInitializer } from '../../utils/useGridInitializeState'; -import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { gridColumnDefinitionsSelector } from '../columns/gridColumnsSelector'; import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; import { gridRenderContextSelector } from '../virtualization/gridVirtualizationSelectors'; @@ -24,6 +23,16 @@ function isUninitializedRowContext(renderContext: GridRenderContext) { return renderContext.firstRowIndex === 0 && renderContext.lastRowIndex === 0; } +function isRowRenderContextUpdated( + prevRenderContext: GridRenderContext, + renderContext: GridRenderContext, +) { + return ( + prevRenderContext.firstRowIndex !== renderContext.firstRowIndex || + prevRenderContext.lastRowIndex !== renderContext.lastRowIndex + ); +} + function getUnprocessedRange( testRange: { firstRowIndex: number; lastRowIndex: number }, processedRange: { firstRowIndex: number; lastRowIndex: number }, @@ -247,7 +256,7 @@ export const useGridRowSpanning = ( if (isFirstRender.current) { isFirstRender.current = false; } - if (!firstRender && prevRenderContext.current !== renderContext) { + if (!firstRender && isRowRenderContextUpdated(prevRenderContext.current, renderContext)) { prevRenderContext.current = renderContext; updateRowSpanningState(false); return; From 089c214f6657e58dee21ce8bb1fe35be8bb887de Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 2 Sep 2024 20:58:51 +0500 Subject: [PATCH 25/47] Virtualization: Keep the spanned cell in the viewport on scroll down --- .../features/rows/gridRowSpanningSelectors.ts | 5 ++ .../hooks/features/rows/useGridRowSpanning.ts | 66 +++++++++++++------ .../virtualization/useGridVirtualScroller.tsx | 14 +++- 3 files changed, 64 insertions(+), 21 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningSelectors.ts b/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningSelectors.ts index 67ac552b26e8..e9da213ee77a 100644 --- a/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningSelectors.ts +++ b/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningSelectors.ts @@ -12,3 +12,8 @@ export const gridRowSpanningSpannedCellsSelector = createSelector( gridRowSpanningStateSelector, (rowSpanning) => rowSpanning.spannedCells, ); + +export const gridRowSpanningHiddenCellsOriginMapSelector = createSelector( + gridRowSpanningStateSelector, + (rowSpanning) => rowSpanning.hiddenCellOriginMap, +); diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index a7e92d64c3c3..3cde556d1803 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -13,9 +13,15 @@ import { useGridSelector } from '../../utils/useGridSelector'; export interface GridRowSpanningState { spannedCells: Record>; hiddenCells: Record>; + /** + * For each hidden cell, it contains the row index corresponding to the cell that is + * the origin of the hidden cell. i.e. the cell which is spanned. + * Used by the virtualization to properly keep the spanned cells in view. + */ + hiddenCellOriginMap: Record>; } -const EMPTY_STATE = { spannedCells: {}, hiddenCells: {} }; +const EMPTY_STATE = { spannedCells: {}, hiddenCells: {}, hiddenCellOriginMap: {} }; const EMPTY_RANGE = { firstRowIndex: 0, lastRowIndex: 0 }; const skippedFields = new Set(['__check__', '__reorder__']); @@ -118,10 +124,11 @@ export const useGridRowSpanning = ( return; } - const newSpannedCells = resetState + const spannedCells = resetState ? {} : { ...apiRef.current.state.rowSpanning.spannedCells }; + const hiddenCells = resetState ? {} : { ...apiRef.current.state.rowSpanning.hiddenCells }; + const hiddenCellOriginMap = resetState ? {} - : { ...apiRef.current.state.rowSpanning.spannedCells }; - const newHiddenCells = resetState ? {} : { ...apiRef.current.state.rowSpanning.hiddenCells }; + : { ...apiRef.current.state.rowSpanning.hiddenCellOriginMap }; const colDefs = gridColumnDefinitionsSelector(apiRef); @@ -153,7 +160,7 @@ export const useGridRowSpanning = ( ) { const row = visibleRows[index]; - if (newHiddenCells[row.id]?.[colDef.field]) { + if (hiddenCells[row.id]?.[colDef.field]) { continue; } const cellValue = getCellValue(row.model, colDef, apiRef); @@ -162,10 +169,12 @@ export const useGridRowSpanning = ( continue; } - let rowSpanId = row.id; + let spannedRowId = row.id; + let spannedRowIndex = index; let rowSpan = 0; // For first index, also scan in the previous rows to handle the reset state case e.g by sorting + const backwardsHiddenCells = []; if (index === rangeToProcess.firstRowIndex) { let prevIndex = index - 1; const prevRowEntry = visibleRows[prevIndex]; @@ -174,17 +183,28 @@ export const useGridRowSpanning = ( getCellValue(prevRowEntry.model, colDef, apiRef) === cellValue ) { const currentRow = visibleRows[prevIndex + 1]; - if (newHiddenCells[currentRow.id]) { - newHiddenCells[currentRow.id][colDef.field] = true; + if (hiddenCells[currentRow.id]) { + hiddenCells[currentRow.id][colDef.field] = true; } else { - newHiddenCells[currentRow.id] = { [colDef.field]: true }; + hiddenCells[currentRow.id] = { [colDef.field]: true }; } + backwardsHiddenCells.push(index); rowSpan += 1; - rowSpanId = prevRowEntry.id; + spannedRowId = prevRowEntry.id; + spannedRowIndex = prevIndex; prevIndex -= 1; } } + backwardsHiddenCells.forEach((hiddenCellIndex) => { + if (hiddenCellOriginMap[hiddenCellIndex]) { + hiddenCellOriginMap[hiddenCellIndex][colDef.field] = spannedRowIndex; + } else { + hiddenCellOriginMap[hiddenCellIndex] = { [colDef.field]: spannedRowIndex }; + } + }); + + // Scan the next rows let relativeIndex = index + 1; while ( relativeIndex <= range.lastRowIndex && @@ -192,20 +212,25 @@ export const useGridRowSpanning = ( getCellValue(visibleRows[relativeIndex].model, colDef, apiRef) === cellValue ) { const currentRow = visibleRows[relativeIndex]; - if (newHiddenCells[currentRow.id]) { - newHiddenCells[currentRow.id][colDef.field] = true; + if (hiddenCells[currentRow.id]) { + hiddenCells[currentRow.id][colDef.field] = true; + } else { + hiddenCells[currentRow.id] = { [colDef.field]: true }; + } + if (hiddenCellOriginMap[relativeIndex]) { + hiddenCellOriginMap[relativeIndex][colDef.field] = spannedRowIndex; } else { - newHiddenCells[currentRow.id] = { [colDef.field]: true }; + hiddenCellOriginMap[relativeIndex] = { [colDef.field]: spannedRowIndex }; } relativeIndex += 1; rowSpan += 1; } if (rowSpan > 0) { - if (newSpannedCells[rowSpanId]) { - newSpannedCells[rowSpanId][colDef.field] = rowSpan + 1; + if (spannedCells[spannedRowId]) { + spannedCells[spannedRowId][colDef.field] = rowSpan + 1; } else { - newSpannedCells[rowSpanId] = { [colDef.field]: rowSpan + 1 }; + spannedCells[spannedRowId] = { [colDef.field]: rowSpan + 1 }; } } } @@ -218,8 +243,8 @@ export const useGridRowSpanning = ( }; }); - const newSpannedCellsCount = Object.keys(newSpannedCells).length; - const newHiddenCellsCount = Object.keys(newHiddenCells).length; + const newSpannedCellsCount = Object.keys(spannedCells).length; + const newHiddenCellsCount = Object.keys(hiddenCells).length; const currentSpannedCellsCount = Object.keys( apiRef.current.state.rowSpanning.spannedCells, ).length; @@ -240,8 +265,9 @@ export const useGridRowSpanning = ( return { ...state, rowSpanning: { - spannedCells: newSpannedCells, - hiddenCells: newHiddenCells, + spannedCells, + hiddenCells, + hiddenCellOriginMap, }, }; }); diff --git a/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx b/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx index d986452fabbc..c239ac0a04aa 100644 --- a/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx +++ b/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx @@ -45,6 +45,7 @@ import { gridVirtualizationColumnEnabledSelector, } from './gridVirtualizationSelectors'; import { EMPTY_RENDER_CONTEXT } from './useGridVirtualization'; +import { gridRowSpanningHiddenCellsOriginMapSelector } from '../rows/gridRowSpanningSelectors'; const MINIMUM_COLUMN_WIDTH = 50; @@ -628,6 +629,7 @@ type RenderContextInputs = { range: ReturnType['range']; pinnedColumns: ReturnType; visibleColumns: ReturnType; + hiddenCellsOriginMap: ReturnType; }; function inputsSelector( @@ -639,6 +641,7 @@ function inputsSelector( const dimensions = gridDimensionsSelector(apiRef.current.state); const currentPage = getVisibleRows(apiRef, rootProps); const visibleColumns = gridVisibleColumnDefinitionsSelector(apiRef); + const hiddenCellsOriginMap = gridRowSpanningHiddenCellsOriginMapSelector(apiRef); const lastRowId = apiRef.current.state.rows.dataRowIds.at(-1); const lastColumn = visibleColumns.at(-1); return { @@ -660,6 +663,7 @@ function inputsSelector( range: currentPage.range, pinnedColumns: gridVisiblePinnedColumnDefinitionsSelector(apiRef), visibleColumns, + hiddenCellsOriginMap, }; } @@ -681,7 +685,7 @@ function computeRenderContext( if (inputs.enabledForRows) { // 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( + let firstRowIndex = Math.min( getNearestIndexToRender(inputs, top, { atStart: true, lastPosition: @@ -690,6 +694,14 @@ function computeRenderContext( inputs.rowsMeta.positions.length - 1, ); + // If any of the cells in the `firstRowIndex` is hidden due to an extended row span, + // Make sure the row from where the rowSpan is originated is visible. + const rowSpanHiddenCellOrigin = inputs.hiddenCellsOriginMap[firstRowIndex]; + if (rowSpanHiddenCellOrigin) { + const minSpannedRowIndex = Math.min(...Object.values(rowSpanHiddenCellOrigin)); + firstRowIndex = Math.min(firstRowIndex, minSpannedRowIndex); + } + const lastRowIndex = inputs.autoHeight ? firstRowIndex + inputs.rows.length : getNearestIndexToRender(inputs, top + inputs.viewportInnerHeight); From 967cde80d643fd18e0fd6982643d1564e778774b Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 2 Sep 2024 22:04:54 +0500 Subject: [PATCH 26/47] Add some initial tests --- .../hooks/features/rows/useGridRowSpanning.ts | 2 +- .../src/tests/rowSpanning.DataGrid.test.tsx | 165 ++++++++++++++++++ 2 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index 3cde556d1803..039691490378 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -174,7 +174,7 @@ export const useGridRowSpanning = ( let rowSpan = 0; // For first index, also scan in the previous rows to handle the reset state case e.g by sorting - const backwardsHiddenCells = []; + const backwardsHiddenCells: number[] = []; if (index === rangeToProcess.firstRowIndex) { let prevIndex = index - 1; const prevRowEntry = visibleRows[prevIndex]; diff --git a/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx b/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx new file mode 100644 index 000000000000..0d2f7a01263c --- /dev/null +++ b/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx @@ -0,0 +1,165 @@ +import * as React from 'react'; +import { createRenderer } from '@mui/internal-test-utils'; +import { expect } from 'chai'; +import { DataGrid, useGridApiRef, DataGridProps, GridApi } from '@mui/x-data-grid'; +import { getCell } from 'test/utils/helperFn'; + +const isJSDOM = /jsdom/.test(window.navigator.userAgent); + +describe(' - Row spanning', () => { + const { render } = createRenderer(); + + let apiRef: React.MutableRefObject; + const baselineProps: DataGridProps = { + unstable_rowSpanning: true, + columns: [ + { + field: 'code', + headerName: 'Item Code', + width: 85, + cellClassName: ({ row }) => (row.summaryRow ? 'bold' : ''), + }, + { + field: 'description', + headerName: 'Description', + width: 170, + }, + { + field: 'quantity', + headerName: 'Quantity', + width: 80, + // Do not span the values + rowSpanValueGetter: () => null, + }, + { + field: 'unitPrice', + headerName: 'Unit Price', + type: 'number', + valueFormatter: (value) => (value ? `$${value}.00` : ''), + }, + { + field: 'totalPrice', + headerName: 'Total Price', + type: 'number', + valueGetter: (value, row) => value ?? row?.unitPrice, + valueFormatter: (value) => `$${value}.00`, + }, + ], + rows: [ + { + id: 1, + code: 'A101', + description: 'Wireless Mouse', + quantity: 2, + unitPrice: 50, + totalPrice: 100, + }, + { + id: 2, + code: 'A102', + description: 'Mechanical Keyboard', + quantity: 1, + unitPrice: 75, + }, + { + id: 3, + code: 'A103', + description: 'USB Dock Station', + quantity: 1, + unitPrice: 400, + }, + { + id: 4, + code: 'A104', + description: 'Laptop', + quantity: 1, + unitPrice: 1800, + totalPrice: 2050, + }, + { + id: 5, + code: 'A104', + description: '- 16GB RAM Upgrade', + quantity: 1, + unitPrice: 100, + totalPrice: 2050, + }, + { + id: 6, + code: 'A104', + description: '- 512GB SSD Upgrade', + quantity: 1, + unitPrice: 150, + totalPrice: 2050, + }, + { + id: 7, + code: 'TOTAL', + totalPrice: 2625, + summaryRow: true, + }, + ], + }; + + function TestDataGrid(props: Partial) { + apiRef = useGridApiRef(); + return ( +
+ +
+ ); + } + + const rowHeight = 52; + + it('should span the repeating row values', () => { + render(); + const rowsWithSpannedCells = Object.keys(apiRef.current.state.rowSpanning.spannedCells); + expect(rowsWithSpannedCells.length).to.equal(1); + const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows('4'); + expect(rowIndex).to.equal(3); + const spanValue = apiRef.current.state.rowSpanning.spannedCells['4']; + expect(spanValue).to.deep.equal({ code: 3, totalPrice: 3 }); + const spannedCell = getCell(rowIndex, 0); + expect(spannedCell).to.have.style('height', `${rowHeight * spanValue.code}px`); + }); + + it('should work with sorting', () => { + render( + , + ); + const rowsWithSpannedCells = Object.keys(apiRef.current.state.rowSpanning.spannedCells); + expect(rowsWithSpannedCells.length).to.equal(1); + const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows('4'); + expect(rowIndex).to.equal(1); + const spanValue = apiRef.current.state.rowSpanning.spannedCells['4']; + expect(spanValue).to.deep.equal({ code: 3, totalPrice: 3 }); + const spannedCell = getCell(rowIndex, 0); + expect(spannedCell).to.have.style('height', `${rowHeight * spanValue.code}px`); + }); + + it('should work with filtering', () => { + render( + , + ); + const rowsWithSpannedCells = Object.keys(apiRef.current.state.rowSpanning.spannedCells); + expect(rowsWithSpannedCells.length).to.equal(1); + const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows('5'); + expect(rowIndex).to.equal(0); + const spanValue = apiRef.current.state.rowSpanning.spannedCells['5']; + expect(spanValue).to.deep.equal({ code: 2, totalPrice: 2 }); + const spannedCell = getCell(rowIndex, 0); + expect(spannedCell).to.have.style('height', `${rowHeight * spanValue.code}px`); + }); + + // TODO: Add tests for keyboard navigation + // TODO: Add tests for column reordering +}); From dd5d95a5a1d57efbaad88bc0e5e5b9c9faa7798e Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 3 Sep 2024 00:04:52 +0500 Subject: [PATCH 27/47] Ignore column context changes --- .../src/hooks/features/rows/useGridRowSpanning.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index 039691490378..34aa73450d09 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -4,7 +4,7 @@ import { GridRowId, GridValidRowModel } from '../../../models/gridRows'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import { GridApiCommunity, GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { GridStateInitializer } from '../../utils/useGridInitializeState'; -import { gridColumnDefinitionsSelector } from '../columns/gridColumnsSelector'; +import { gridVisibleColumnDefinitionsSelector } from '../columns/gridColumnsSelector'; import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; import { gridRenderContextSelector } from '../virtualization/gridVirtualizationSelectors'; import { GridRenderContext } from '../../../models'; @@ -103,6 +103,7 @@ export const useGridRowSpanning = ( ): void => { const { range, rows: visibleRows } = useGridVisibleRows(apiRef, props); const renderContext = useGridSelector(apiRef, gridRenderContextSelector); + const colDefs = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector); const processedRange = React.useRef<{ firstRowIndex: number; lastRowIndex: number }>(EMPTY_RANGE); const updateRowSpanningState = React.useCallback( @@ -130,8 +131,6 @@ export const useGridRowSpanning = ( ? {} : { ...apiRef.current.state.rowSpanning.hiddenCellOriginMap }; - const colDefs = gridColumnDefinitionsSelector(apiRef); - if (resetState) { processedRange.current = EMPTY_RANGE; } @@ -272,7 +271,7 @@ export const useGridRowSpanning = ( }; }); }, - [apiRef, props.unstable_rowSpanning, range, renderContext, visibleRows], + [apiRef, props.unstable_rowSpanning, range, renderContext, visibleRows, colDefs], ); const prevRenderContext = React.useRef(renderContext); @@ -282,9 +281,11 @@ export const useGridRowSpanning = ( if (isFirstRender.current) { isFirstRender.current = false; } - if (!firstRender && isRowRenderContextUpdated(prevRenderContext.current, renderContext)) { + if (!firstRender && prevRenderContext.current !== renderContext) { + if (isRowRenderContextUpdated(prevRenderContext.current, renderContext)) { + updateRowSpanningState(false); + } prevRenderContext.current = renderContext; - updateRowSpanningState(false); return; } updateRowSpanningState(); From 230d04a4c7bd7b931bbb34e9897d585303c90bb8 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 3 Sep 2024 00:28:42 +0500 Subject: [PATCH 28/47] Fix keyboard navigation bug --- .../features/keyboardNavigation/useGridKeyboardNavigation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts index 3e7b0518bf61..0714ac03fd42 100644 --- a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts +++ b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts @@ -5,7 +5,7 @@ import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { GridCellParams } from '../../../models/params/gridCellParams'; import { gridVisibleColumnDefinitionsSelector, - gridColumnFieldsSelector, + gridVisibleColumnFieldsSelector, } from '../columns/gridColumnsSelector'; import { useGridLogger } from '../../utils/useGridLogger'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; @@ -87,7 +87,7 @@ export const useGridKeyboardNavigation = ( colIndex = nextCellColSpanInfo.rightVisibleCellIndex; } } - const field = gridColumnFieldsSelector(apiRef)[colIndex]; + const field = gridVisibleColumnFieldsSelector(apiRef)[colIndex]; const nonRowSpannedRowId = findNonRowSpannedCell(apiRef, rowId, field, rowSpanScanDirection); // `scrollToIndexes` requires a rowIndex relative to all visible rows. // Those rows do not include pinned rows, but pinned rows do not need scroll anyway. From ef2bcf6d45fa61b7aa6fffa9d7b3cde2023b2802 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 3 Sep 2024 16:44:12 +0500 Subject: [PATCH 29/47] Fix rtl related tests --- .../features/keyboardNavigation/useGridKeyboardNavigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts index 0714ac03fd42..915791716bad 100644 --- a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts +++ b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts @@ -515,7 +515,7 @@ export const useGridKeyboardNavigation = ( goToCell( colIndexBefore, getRowIdFromIndex(rowIndexBefore + 1), - isRtl ? 'left' : 'right', + isRtl ? 'right' : 'left', 'down', ); } From d85fe945f9190f341f0e81b32cbeadb566892255 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 3 Sep 2024 18:23:22 +0500 Subject: [PATCH 30/47] Skip tests in jsdom --- .../src/tests/rowSpanning.DataGrid.test.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx b/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx index 0d2f7a01263c..8d9668c1f660 100644 --- a/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx +++ b/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx @@ -112,7 +112,10 @@ describe(' - Row spanning', () => { const rowHeight = 52; - it('should span the repeating row values', () => { + it('should span the repeating row values', function test() { + if (isJSDOM) { + this.skip(); + } render(); const rowsWithSpannedCells = Object.keys(apiRef.current.state.rowSpanning.spannedCells); expect(rowsWithSpannedCells.length).to.equal(1); @@ -124,7 +127,10 @@ describe(' - Row spanning', () => { expect(spannedCell).to.have.style('height', `${rowHeight * spanValue.code}px`); }); - it('should work with sorting', () => { + it('should work with sorting', function test() { + if (isJSDOM) { + this.skip(); + } render( , ); @@ -138,7 +144,10 @@ describe(' - Row spanning', () => { expect(spannedCell).to.have.style('height', `${rowHeight * spanValue.code}px`); }); - it('should work with filtering', () => { + it('should work with filtering', function test() { + if (isJSDOM) { + this.skip(); + } render( Date: Tue, 3 Sep 2024 21:13:44 +0500 Subject: [PATCH 31/47] Do some updates to demos --- docs/data/data-grid/row-spanning/RowSpanning.js | 10 +--------- docs/data/data-grid/row-spanning/RowSpanning.tsx | 10 +--------- .../data-grid/row-spanning/RowSpanningCustom.js | 13 +++---------- .../data-grid/row-spanning/RowSpanningCustom.tsx | 13 +++---------- .../row-spanning/RowSpanningCustom.tsx.preview | 14 ++++++++++++++ 5 files changed, 22 insertions(+), 38 deletions(-) create mode 100644 docs/data/data-grid/row-spanning/RowSpanningCustom.tsx.preview diff --git a/docs/data/data-grid/row-spanning/RowSpanning.js b/docs/data/data-grid/row-spanning/RowSpanning.js index 18de309f185c..109566ed7227 100644 --- a/docs/data/data-grid/row-spanning/RowSpanning.js +++ b/docs/data/data-grid/row-spanning/RowSpanning.js @@ -19,19 +19,11 @@ export default function RowSpanning() { + + \ No newline at end of file From 0d74c824d9112eb052748d8407e8b535b89aa4c7 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Fri, 6 Sep 2024 00:35:38 +0500 Subject: [PATCH 32/47] Address comments --- docs/data/data-grid/row-spanning/RowSpanningCustom.js | 2 +- docs/data/data-grid/row-spanning/RowSpanningCustom.tsx | 2 +- packages/x-data-grid/src/components/cell/GridCell.tsx | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/data/data-grid/row-spanning/RowSpanningCustom.js b/docs/data/data-grid/row-spanning/RowSpanningCustom.js index 967de44756c0..df77d480b9c6 100644 --- a/docs/data/data-grid/row-spanning/RowSpanningCustom.js +++ b/docs/data/data-grid/row-spanning/RowSpanningCustom.js @@ -47,7 +47,7 @@ const columns = [ headerName: 'Age', type: 'number', width: 100, - valueGetter: (value) => { + valueFormatter: (value) => { return `${value} yo`; }, rowSpanValueGetter: (value, row) => { diff --git a/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx b/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx index 1f44e525d350..3be8ad78094c 100644 --- a/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx +++ b/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx @@ -47,7 +47,7 @@ const columns: GridColDef[] = [ headerName: 'Age', type: 'number', width: 100, - valueGetter: (value) => { + valueFormatter: (value) => { return `${value} yo`; }, rowSpanValueGetter: (value, row) => { diff --git a/packages/x-data-grid/src/components/cell/GridCell.tsx b/packages/x-data-grid/src/components/cell/GridCell.tsx index d3502e158e17..54a67f3c3908 100644 --- a/packages/x-data-grid/src/components/cell/GridCell.tsx +++ b/packages/x-data-grid/src/components/cell/GridCell.tsx @@ -385,6 +385,7 @@ const GridCell = React.forwardRef(function GridCe return (
); From cc349114a8fd67360d90b9a9fd654e07cdfc7586 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 10 Sep 2024 00:12:35 +0500 Subject: [PATCH 33/47] Compute the row spanning state in initializer --- .../features/rows/gridRowSpanningUtils.ts | 70 ++++ .../hooks/features/rows/useGridRowSpanning.ts | 337 +++++++++--------- 2 files changed, 245 insertions(+), 162 deletions(-) create mode 100644 packages/x-data-grid/src/hooks/features/rows/gridRowSpanningUtils.ts diff --git a/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningUtils.ts b/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningUtils.ts new file mode 100644 index 000000000000..032ac9209ac0 --- /dev/null +++ b/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningUtils.ts @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { GridRenderContext } from '../../../models'; +import { GridValidRowModel } from '../../../models/gridRows'; +import { GridColDef } from '../../../models/colDef'; +import { GridApiCommunity } from '../../../models/api/gridApiCommunity'; + +export function getUnprocessedRange( + testRange: { firstRowIndex: number; lastRowIndex: number }, + processedRange: { firstRowIndex: number; lastRowIndex: number }, +) { + if ( + testRange.firstRowIndex >= processedRange.firstRowIndex && + testRange.lastRowIndex <= processedRange.lastRowIndex + ) { + return null; + } + // Overflowing at the end + // Example: testRange={ firstRowIndex: 10, lastRowIndex: 20 }, processedRange={ firstRowIndex: 0, lastRowIndex: 15 } + // Unprocessed Range={ firstRowIndex: 16, lastRowIndex: 20 } + if ( + testRange.firstRowIndex >= processedRange.firstRowIndex && + testRange.lastRowIndex > processedRange.lastRowIndex + ) { + return { firstRowIndex: processedRange.lastRowIndex, lastRowIndex: testRange.lastRowIndex }; + } + // Overflowing at the beginning + // Example: testRange={ firstRowIndex: 0, lastRowIndex: 20 }, processedRange={ firstRowIndex: 16, lastRowIndex: 30 } + // Unprocessed Range={ firstRowIndex: 0, lastRowIndex: 15 } + if ( + testRange.firstRowIndex < processedRange.firstRowIndex && + testRange.lastRowIndex <= processedRange.lastRowIndex + ) { + return { + firstRowIndex: testRange.firstRowIndex, + lastRowIndex: processedRange.firstRowIndex - 1, + }; + } + // TODO: Should return two ranges handle overflowing at both ends ? + return testRange; +} + +export function isUninitializedRowContext(renderContext: GridRenderContext) { + return renderContext.firstRowIndex === 0 && renderContext.lastRowIndex === 0; +} + +export function isRowRenderContextUpdated( + prevRenderContext: GridRenderContext, + renderContext: GridRenderContext, +) { + return ( + prevRenderContext.firstRowIndex !== renderContext.firstRowIndex || + prevRenderContext.lastRowIndex !== renderContext.lastRowIndex + ); +} + +export const getCellValue = ( + row: GridValidRowModel, + colDef: GridColDef, + apiRef: React.MutableRefObject, +) => { + if (!row) { + return null; + } + let cellValue = row[colDef.field]; + const valueGetter = colDef.rowSpanValueGetter ?? colDef.valueGetter; + if (valueGetter) { + cellValue = valueGetter(cellValue as never, row, colDef, apiRef); + } + return cellValue; +}; diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index 34aa73450d09..b080caaf18a5 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import useLazyRef from '@mui/utils/useLazyRef'; import { GridColDef } from '../../../models/colDef'; import { GridRowId, GridValidRowModel } from '../../../models/gridRows'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; @@ -7,8 +8,14 @@ import { GridStateInitializer } from '../../utils/useGridInitializeState'; import { gridVisibleColumnDefinitionsSelector } from '../columns/gridColumnsSelector'; import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; import { gridRenderContextSelector } from '../virtualization/gridVirtualizationSelectors'; -import { GridRenderContext } from '../../../models'; import { useGridSelector } from '../../utils/useGridSelector'; +import { GridRowEntry } from '../../../models/gridRows'; +import { + getUnprocessedRange, + isRowRenderContextUpdated, + isUninitializedRowContext, + getCellValue, +} from './gridRowSpanningUtils'; export interface GridRowSpanningState { spannedCells: Record>; @@ -25,78 +32,164 @@ const EMPTY_STATE = { spannedCells: {}, hiddenCells: {}, hiddenCellOriginMap: {} const EMPTY_RANGE = { firstRowIndex: 0, lastRowIndex: 0 }; const skippedFields = new Set(['__check__', '__reorder__']); -function isUninitializedRowContext(renderContext: GridRenderContext) { - return renderContext.firstRowIndex === 0 && renderContext.lastRowIndex === 0; -} - -function isRowRenderContextUpdated( - prevRenderContext: GridRenderContext, - renderContext: GridRenderContext, -) { - return ( - prevRenderContext.firstRowIndex !== renderContext.firstRowIndex || - prevRenderContext.lastRowIndex !== renderContext.lastRowIndex - ); -} - -function getUnprocessedRange( - testRange: { firstRowIndex: number; lastRowIndex: number }, +const computeRowSpanningState = ( + apiRef: React.MutableRefObject, + colDefs: GridColDef[], + visibleRows: GridRowEntry[], + rangeToProcess: { firstRowIndex: number; lastRowIndex: number }, + resetState: boolean = true, processedRange: { firstRowIndex: number; lastRowIndex: number }, -) { - if ( - testRange.firstRowIndex >= processedRange.firstRowIndex && - testRange.lastRowIndex <= processedRange.lastRowIndex - ) { - return null; - } - // Overflowing at the end - // Example: testRange={ firstRowIndex: 10, lastRowIndex: 20 }, processedRange={ firstRowIndex: 0, lastRowIndex: 15 } - // Unprocessed Range={ firstRowIndex: 16, lastRowIndex: 20 } - if ( - testRange.firstRowIndex >= processedRange.firstRowIndex && - testRange.lastRowIndex > processedRange.lastRowIndex - ) { - return { firstRowIndex: processedRange.lastRowIndex, lastRowIndex: testRange.lastRowIndex }; +) => { + const spannedCells = resetState ? {} : { ...apiRef.current.state.rowSpanning.spannedCells }; + const hiddenCells = resetState ? {} : { ...apiRef.current.state.rowSpanning.hiddenCells }; + const hiddenCellOriginMap = resetState + ? {} + : { ...apiRef.current.state.rowSpanning.hiddenCellOriginMap }; + + if (resetState) { + processedRange = EMPTY_RANGE; } - // Overflowing at the beginning - // Example: testRange={ firstRowIndex: 0, lastRowIndex: 20 }, processedRange={ firstRowIndex: 16, lastRowIndex: 30 } - // Unprocessed Range={ firstRowIndex: 0, lastRowIndex: 15 } - if ( - testRange.firstRowIndex < processedRange.firstRowIndex && - testRange.lastRowIndex <= processedRange.lastRowIndex - ) { + + colDefs.forEach((colDef) => { + if (skippedFields.has(colDef.field)) { + return; + } + + for ( + let index = rangeToProcess.firstRowIndex; + index <= rangeToProcess.lastRowIndex; + index += 1 + ) { + const row = visibleRows[index]; + + if (hiddenCells[row.id]?.[colDef.field]) { + continue; + } + const cellValue = getCellValue(row.model, colDef, apiRef); + + if (cellValue == null) { + continue; + } + + let spannedRowId = row.id; + let spannedRowIndex = index; + let rowSpan = 0; + + // For first index, also scan in the previous rows to handle the reset state case e.g by sorting + const backwardsHiddenCells: number[] = []; + if (index === rangeToProcess.firstRowIndex) { + let prevIndex = index - 1; + const prevRowEntry = visibleRows[prevIndex]; + while ( + prevIndex >= rangeToProcess.firstRowIndex && + getCellValue(prevRowEntry.model, colDef, apiRef) === cellValue + ) { + const currentRow = visibleRows[prevIndex + 1]; + if (hiddenCells[currentRow.id]) { + hiddenCells[currentRow.id][colDef.field] = true; + } else { + hiddenCells[currentRow.id] = { [colDef.field]: true }; + } + backwardsHiddenCells.push(index); + rowSpan += 1; + spannedRowId = prevRowEntry.id; + spannedRowIndex = prevIndex; + prevIndex -= 1; + } + } + + backwardsHiddenCells.forEach((hiddenCellIndex) => { + if (hiddenCellOriginMap[hiddenCellIndex]) { + hiddenCellOriginMap[hiddenCellIndex][colDef.field] = spannedRowIndex; + } else { + hiddenCellOriginMap[hiddenCellIndex] = { [colDef.field]: spannedRowIndex }; + } + }); + + // Scan the next rows + let relativeIndex = index + 1; + while ( + relativeIndex <= rangeToProcess.lastRowIndex && + visibleRows[relativeIndex] && + getCellValue(visibleRows[relativeIndex].model, colDef, apiRef) === cellValue + ) { + const currentRow = visibleRows[relativeIndex]; + if (hiddenCells[currentRow.id]) { + hiddenCells[currentRow.id][colDef.field] = true; + } else { + hiddenCells[currentRow.id] = { [colDef.field]: true }; + } + if (hiddenCellOriginMap[relativeIndex]) { + hiddenCellOriginMap[relativeIndex][colDef.field] = spannedRowIndex; + } else { + hiddenCellOriginMap[relativeIndex] = { [colDef.field]: spannedRowIndex }; + } + relativeIndex += 1; + rowSpan += 1; + } + + if (rowSpan > 0) { + if (spannedCells[spannedRowId]) { + spannedCells[spannedRowId][colDef.field] = rowSpan + 1; + } else { + spannedCells[spannedRowId] = { [colDef.field]: rowSpan + 1 }; + } + } + } + processedRange = { + firstRowIndex: Math.min(processedRange.firstRowIndex, rangeToProcess.firstRowIndex), + lastRowIndex: Math.max(processedRange.lastRowIndex, rangeToProcess.lastRowIndex), + }; + }); + return { spannedCells, hiddenCells, hiddenCellOriginMap, processedRange }; +}; + +export const rowSpanningStateInitializer: GridStateInitializer = (state, props, apiRef) => { + if (props.unstable_rowSpanning) { + const rowIds = state.rows?.dataRowIds; + const orderedFields = state.columns?.orderedFields; + const dataRowIdToModelLookup = state.rows?.dataRowIdToModelLookup; + const columnsLookup = state.columns?.lookup; + + if (!rowIds?.length || !orderedFields?.length || !dataRowIdToModelLookup || !columnsLookup) { + return { + ...state, + rowSpanning: EMPTY_STATE, + }; + } + const rangeToProcess = { + firstRowIndex: 0, + lastRowIndex: Math.min(19, rowIds.length - 1), + }; + const rows = rowIds.map((id) => ({ + id, + model: dataRowIdToModelLookup[id!], + })) as GridRowEntry[]; + const colDefs = orderedFields.map((field) => columnsLookup[field!]) as GridColDef[]; + const { spannedCells, hiddenCells, hiddenCellOriginMap } = computeRowSpanningState( + apiRef, + colDefs, + rows, + rangeToProcess, + true, + EMPTY_RANGE, + ); + return { - firstRowIndex: testRange.firstRowIndex, - lastRowIndex: processedRange.firstRowIndex - 1, + ...state, + rowSpanning: { + spannedCells, + hiddenCells, + hiddenCellOriginMap, + }, }; } - // TODO: Should return two ranges handle overflowing at both ends ? - return testRange; -} - -export const rowSpanningStateInitializer: GridStateInitializer = (state) => { return { ...state, rowSpanning: EMPTY_STATE, }; }; -const getCellValue = ( - row: GridValidRowModel, - colDef: GridColDef, - apiRef: React.MutableRefObject, -) => { - if (!row) { - return null; - } - let cellValue = row[colDef.field]; - const valueGetter = colDef.rowSpanValueGetter ?? colDef.valueGetter; - if (valueGetter) { - cellValue = valueGetter(cellValue as never, row, colDef, apiRef); - } - return cellValue; -}; - export const useGridRowSpanning = ( apiRef: React.MutableRefObject, props: Pick, @@ -104,7 +197,12 @@ export const useGridRowSpanning = ( const { range, rows: visibleRows } = useGridVisibleRows(apiRef, props); const renderContext = useGridSelector(apiRef, gridRenderContextSelector); const colDefs = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector); - const processedRange = React.useRef<{ firstRowIndex: number; lastRowIndex: number }>(EMPTY_RANGE); + const processedRange = useLazyRef(() => { + return { + firstRowIndex: 0, + lastRowIndex: Math.min(19, apiRef.current.state.rows.dataRowIds.length - 1), + }; + }); const updateRowSpanningState = React.useCallback( // A reset needs to occur when: @@ -125,12 +223,6 @@ export const useGridRowSpanning = ( return; } - const spannedCells = resetState ? {} : { ...apiRef.current.state.rowSpanning.spannedCells }; - const hiddenCells = resetState ? {} : { ...apiRef.current.state.rowSpanning.hiddenCells }; - const hiddenCellOriginMap = resetState - ? {} - : { ...apiRef.current.state.rowSpanning.hiddenCellOriginMap }; - if (resetState) { processedRange.current = EMPTY_RANGE; } @@ -147,100 +239,21 @@ export const useGridRowSpanning = ( return; } - colDefs.forEach((colDef) => { - if (skippedFields.has(colDef.field)) { - return; - } - - for ( - let index = rangeToProcess.firstRowIndex; - index <= rangeToProcess.lastRowIndex; - index += 1 - ) { - const row = visibleRows[index]; - - if (hiddenCells[row.id]?.[colDef.field]) { - continue; - } - const cellValue = getCellValue(row.model, colDef, apiRef); - - if (cellValue == null) { - continue; - } - - let spannedRowId = row.id; - let spannedRowIndex = index; - let rowSpan = 0; - - // For first index, also scan in the previous rows to handle the reset state case e.g by sorting - const backwardsHiddenCells: number[] = []; - if (index === rangeToProcess.firstRowIndex) { - let prevIndex = index - 1; - const prevRowEntry = visibleRows[prevIndex]; - while ( - prevIndex >= range.firstRowIndex && - getCellValue(prevRowEntry.model, colDef, apiRef) === cellValue - ) { - const currentRow = visibleRows[prevIndex + 1]; - if (hiddenCells[currentRow.id]) { - hiddenCells[currentRow.id][colDef.field] = true; - } else { - hiddenCells[currentRow.id] = { [colDef.field]: true }; - } - backwardsHiddenCells.push(index); - rowSpan += 1; - spannedRowId = prevRowEntry.id; - spannedRowIndex = prevIndex; - prevIndex -= 1; - } - } - - backwardsHiddenCells.forEach((hiddenCellIndex) => { - if (hiddenCellOriginMap[hiddenCellIndex]) { - hiddenCellOriginMap[hiddenCellIndex][colDef.field] = spannedRowIndex; - } else { - hiddenCellOriginMap[hiddenCellIndex] = { [colDef.field]: spannedRowIndex }; - } - }); - - // Scan the next rows - let relativeIndex = index + 1; - while ( - relativeIndex <= range.lastRowIndex && - visibleRows[relativeIndex] && - getCellValue(visibleRows[relativeIndex].model, colDef, apiRef) === cellValue - ) { - const currentRow = visibleRows[relativeIndex]; - if (hiddenCells[currentRow.id]) { - hiddenCells[currentRow.id][colDef.field] = true; - } else { - hiddenCells[currentRow.id] = { [colDef.field]: true }; - } - if (hiddenCellOriginMap[relativeIndex]) { - hiddenCellOriginMap[relativeIndex][colDef.field] = spannedRowIndex; - } else { - hiddenCellOriginMap[relativeIndex] = { [colDef.field]: spannedRowIndex }; - } - relativeIndex += 1; - rowSpan += 1; - } + const { + spannedCells, + hiddenCells, + hiddenCellOriginMap, + processedRange: newProcessedRange, + } = computeRowSpanningState( + apiRef, + colDefs, + visibleRows, + rangeToProcess, + resetState, + processedRange.current, + ); - if (rowSpan > 0) { - if (spannedCells[spannedRowId]) { - spannedCells[spannedRowId][colDef.field] = rowSpan + 1; - } else { - spannedCells[spannedRowId] = { [colDef.field]: rowSpan + 1 }; - } - } - } - processedRange.current = { - firstRowIndex: Math.min( - processedRange.current.firstRowIndex, - rangeToProcess.firstRowIndex, - ), - lastRowIndex: Math.max(processedRange.current.lastRowIndex, rangeToProcess.lastRowIndex), - }; - }); + processedRange.current = newProcessedRange; const newSpannedCellsCount = Object.keys(spannedCells).length; const newHiddenCellsCount = Object.keys(hiddenCells).length; From 157a9930f863f389a34452c84e82148c6c92027d Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 10 Sep 2024 00:15:58 +0500 Subject: [PATCH 34/47] Add detail panel toggle to skipped fields --- .../x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index b080caaf18a5..5e794a58e2f9 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -30,7 +30,7 @@ export interface GridRowSpanningState { const EMPTY_STATE = { spannedCells: {}, hiddenCells: {}, hiddenCellOriginMap: {} }; const EMPTY_RANGE = { firstRowIndex: 0, lastRowIndex: 0 }; -const skippedFields = new Set(['__check__', '__reorder__']); +const skippedFields = new Set(['__check__', '__reorder__', '__detail_panel_toggle__']); const computeRowSpanningState = ( apiRef: React.MutableRefObject, From 9cb2253bed2b1ee31b147d8a37c07f89abb055d7 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 10 Sep 2024 00:18:41 +0500 Subject: [PATCH 35/47] Update docs --- docs/data/data-grid/row-spanning/row-spanning.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/data/data-grid/row-spanning/row-spanning.md b/docs/data/data-grid/row-spanning/row-spanning.md index 7d16ff6da03d..5b97740aa373 100644 --- a/docs/data/data-grid/row-spanning/row-spanning.md +++ b/docs/data/data-grid/row-spanning/row-spanning.md @@ -24,6 +24,10 @@ See the [Customizing row spanned cells](#customizing-row-spanned-cells) section The row spanning generally works with features like [sorting](/x/react-data-grid/sorting/) and [filtering](/x/react-data-grid/filtering/), be sure to check if everything works as expected when using it in combination with features like [column spanning](/x/react-data-grid/column-spanning/). ::: +:::warning +The row spanning works by increasing the height of the spanned cell by a factor `rowHeight`, it doesn't work properly with variable and dynamic row height. +::: + ## Customizing row spanned cells You could customize the value used in row spanning computation using `colDef.rowSpanValueGetter` prop and both the value used in row spanning computation and the value used in cell using `colDef.valueGetter` prop. From ba514cc5d46c99081ec5e91a0f542ae2a0c64c6c Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 10 Sep 2024 00:40:21 +0500 Subject: [PATCH 36/47] Lint + refactor --- .../hooks/features/rows/useGridRowSpanning.ts | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index 5e794a58e2f9..e18ee33d048d 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -1,15 +1,14 @@ import * as React from 'react'; import useLazyRef from '@mui/utils/useLazyRef'; import { GridColDef } from '../../../models/colDef'; -import { GridRowId, GridValidRowModel } from '../../../models/gridRows'; +import { GridRowId, GridValidRowModel, GridRowEntry } from '../../../models/gridRows'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; -import { GridApiCommunity, GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; +import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { GridStateInitializer } from '../../utils/useGridInitializeState'; import { gridVisibleColumnDefinitionsSelector } from '../columns/gridColumnsSelector'; import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; import { gridRenderContextSelector } from '../virtualization/gridVirtualizationSelectors'; import { useGridSelector } from '../../utils/useGridSelector'; -import { GridRowEntry } from '../../../models/gridRows'; import { getUnprocessedRange, isRowRenderContextUpdated, @@ -28,17 +27,19 @@ export interface GridRowSpanningState { hiddenCellOriginMap: Record>; } +type RowRange = { firstRowIndex: number; lastRowIndex: number }; + const EMPTY_STATE = { spannedCells: {}, hiddenCells: {}, hiddenCellOriginMap: {} }; -const EMPTY_RANGE = { firstRowIndex: 0, lastRowIndex: 0 }; +const EMPTY_RANGE: RowRange = { firstRowIndex: 0, lastRowIndex: 0 }; const skippedFields = new Set(['__check__', '__reorder__', '__detail_panel_toggle__']); const computeRowSpanningState = ( - apiRef: React.MutableRefObject, + apiRef: React.MutableRefObject, colDefs: GridColDef[], visibleRows: GridRowEntry[], - rangeToProcess: { firstRowIndex: number; lastRowIndex: number }, - resetState: boolean = true, - processedRange: { firstRowIndex: number; lastRowIndex: number }, + rangeToProcess: RowRange, + resetState: boolean, + processedRange: RowRange, ) => { const spannedCells = resetState ? {} : { ...apiRef.current.state.rowSpanning.spannedCells }; const hiddenCells = resetState ? {} : { ...apiRef.current.state.rowSpanning.hiddenCells }; @@ -159,7 +160,7 @@ export const rowSpanningStateInitializer: GridStateInitializer = (state, props, } const rangeToProcess = { firstRowIndex: 0, - lastRowIndex: Math.min(19, rowIds.length - 1), + lastRowIndex: Math.min(19, Math.max(rowIds.length - 1, 0)), }; const rows = rowIds.map((id) => ({ id, @@ -197,10 +198,10 @@ export const useGridRowSpanning = ( const { range, rows: visibleRows } = useGridVisibleRows(apiRef, props); const renderContext = useGridSelector(apiRef, gridRenderContextSelector); const colDefs = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector); - const processedRange = useLazyRef(() => { + const processedRange = useLazyRef(() => { return { firstRowIndex: 0, - lastRowIndex: Math.min(19, apiRef.current.state.rows.dataRowIds.length - 1), + lastRowIndex: Math.min(19, Math.max(apiRef.current.state.rows.dataRowIds.length - 1, 0)), }; }); @@ -284,7 +285,15 @@ export const useGridRowSpanning = ( }; }); }, - [apiRef, props.unstable_rowSpanning, range, renderContext, visibleRows, colDefs], + [ + apiRef, + props.unstable_rowSpanning, + range, + renderContext, + visibleRows, + colDefs, + processedRange, + ], ); const prevRenderContext = React.useRef(renderContext); From cacef0febe4f19bea2a2fc3fa3461bae509bc9bd Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 10 Sep 2024 01:18:04 +0500 Subject: [PATCH 37/47] Fix test + refactor --- .../useDataGridPremiumComponent.tsx | 2 +- .../DataGridPro/useDataGridProComponent.tsx | 2 +- .../src/DataGrid/useDataGridComponent.tsx | 2 +- .../hooks/features/rows/useGridRowSpanning.ts | 34 ++++++++++++++----- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx index 8a3951e8e6ee..12899dd06ce2 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx @@ -127,13 +127,13 @@ export const useDataGridPremiumComponent = ( useGridInitializeState(columnPinningStateInitializer, apiRef, props); useGridInitializeState(columnsStateInitializer, apiRef, props); useGridInitializeState(rowPinningStateInitializer, apiRef, props); - useGridInitializeState(rowSpanningStateInitializer, apiRef, props); useGridInitializeState(rowsStateInitializer, apiRef, props); useGridInitializeState(editingStateInitializer, apiRef, props); useGridInitializeState(focusStateInitializer, apiRef, props); useGridInitializeState(sortingStateInitializer, apiRef, props); useGridInitializeState(preferencePanelStateInitializer, apiRef, props); useGridInitializeState(filterStateInitializer, apiRef, props); + useGridInitializeState(rowSpanningStateInitializer, apiRef, props); useGridInitializeState(densityStateInitializer, apiRef, props); useGridInitializeState(columnReorderStateInitializer, apiRef, props); useGridInitializeState(columnResizeStateInitializer, apiRef, props); diff --git a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx index 966770103edb..6b8b06fc21bb 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx @@ -116,13 +116,13 @@ export const useDataGridProComponent = ( useGridInitializeState(columnPinningStateInitializer, apiRef, props); useGridInitializeState(columnsStateInitializer, apiRef, props); useGridInitializeState(rowPinningStateInitializer, apiRef, props); - useGridInitializeState(rowSpanningStateInitializer, apiRef, props); useGridInitializeState(rowsStateInitializer, apiRef, props); useGridInitializeState(editingStateInitializer, apiRef, props); useGridInitializeState(focusStateInitializer, apiRef, props); useGridInitializeState(sortingStateInitializer, apiRef, props); useGridInitializeState(preferencePanelStateInitializer, apiRef, props); useGridInitializeState(filterStateInitializer, apiRef, props); + useGridInitializeState(rowSpanningStateInitializer, apiRef, props); useGridInitializeState(densityStateInitializer, apiRef, props); useGridInitializeState(columnReorderStateInitializer, apiRef, props); useGridInitializeState(columnResizeStateInitializer, apiRef, props); diff --git a/packages/x-data-grid/src/DataGrid/useDataGridComponent.tsx b/packages/x-data-grid/src/DataGrid/useDataGridComponent.tsx index 858525d2d19e..85f9a09cb3ea 100644 --- a/packages/x-data-grid/src/DataGrid/useDataGridComponent.tsx +++ b/packages/x-data-grid/src/DataGrid/useDataGridComponent.tsx @@ -80,12 +80,12 @@ export const useDataGridComponent = ( useGridInitializeState(rowSelectionStateInitializer, apiRef, props); useGridInitializeState(columnsStateInitializer, apiRef, props); useGridInitializeState(rowsStateInitializer, apiRef, props); - useGridInitializeState(rowSpanningStateInitializer, apiRef, props); useGridInitializeState(editingStateInitializer, apiRef, props); useGridInitializeState(focusStateInitializer, apiRef, props); useGridInitializeState(sortingStateInitializer, apiRef, props); useGridInitializeState(preferencePanelStateInitializer, apiRef, props); useGridInitializeState(filterStateInitializer, apiRef, props); + useGridInitializeState(rowSpanningStateInitializer, apiRef, props); useGridInitializeState(densityStateInitializer, apiRef, props); useGridInitializeState(columnResizeStateInitializer, apiRef, props); useGridInitializeState(paginationStateInitializer, apiRef, props); diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index e18ee33d048d..1ea69b3eb78f 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -145,14 +145,28 @@ const computeRowSpanningState = ( return { spannedCells, hiddenCells, hiddenCellOriginMap, processedRange }; }; +/** + * @requires columnsStateInitializer (method) - should be initialized before + * @requires rowsStateInitializer (method) - should be initialized before + * @requires filterStateInitializer (method) - should be initialized before + */ export const rowSpanningStateInitializer: GridStateInitializer = (state, props, apiRef) => { if (props.unstable_rowSpanning) { - const rowIds = state.rows?.dataRowIds; - const orderedFields = state.columns?.orderedFields; - const dataRowIdToModelLookup = state.rows?.dataRowIdToModelLookup; - const columnsLookup = state.columns?.lookup; + const rowIds = state.rows!.dataRowIds || []; + const orderedFields = state.columns!.orderedFields || []; + const dataRowIdToModelLookup = state.rows!.dataRowIdToModelLookup; + const columnsLookup = state.columns!.lookup; + const isFilteringPending = + Boolean(state.filter!.filterModel!.items!.length) || + Boolean(state.filter!.filterModel!.quickFilterValues?.length); - if (!rowIds?.length || !orderedFields?.length || !dataRowIdToModelLookup || !columnsLookup) { + if ( + !rowIds.length || + !orderedFields.length || + !dataRowIdToModelLookup || + !columnsLookup || + isFilteringPending + ) { return { ...state, rowSpanning: EMPTY_STATE, @@ -199,10 +213,12 @@ export const useGridRowSpanning = ( const renderContext = useGridSelector(apiRef, gridRenderContextSelector); const colDefs = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector); const processedRange = useLazyRef(() => { - return { - firstRowIndex: 0, - lastRowIndex: Math.min(19, Math.max(apiRef.current.state.rows.dataRowIds.length - 1, 0)), - }; + return Object.keys(apiRef.current.state.rowSpanning.spannedCells).length > 0 + ? { + firstRowIndex: 0, + lastRowIndex: Math.min(19, Math.max(apiRef.current.state.rows.dataRowIds.length - 1, 0)), + } + : EMPTY_RANGE; }); const updateRowSpanningState = React.useCallback( From 1e6b50e2239f509c680f780ac2b93911cb2ab27c Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 11 Sep 2024 19:49:03 +0500 Subject: [PATCH 38/47] Make the behavior smooth with filtering --- .../features/rows/gridRowSpanningUtils.ts | 22 +++++------- .../hooks/features/rows/useGridRowSpanning.ts | 34 ++++++++++++------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningUtils.ts b/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningUtils.ts index 032ac9209ac0..9b5c61b88eda 100644 --- a/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningUtils.ts +++ b/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningUtils.ts @@ -1,13 +1,11 @@ import * as React from 'react'; -import { GridRenderContext } from '../../../models'; -import { GridValidRowModel } from '../../../models/gridRows'; -import { GridColDef } from '../../../models/colDef'; -import { GridApiCommunity } from '../../../models/api/gridApiCommunity'; +import type { GridRenderContext } from '../../../models'; +import type { GridValidRowModel } from '../../../models/gridRows'; +import type { GridColDef } from '../../../models/colDef'; +import type { GridApiCommunity } from '../../../models/api/gridApiCommunity'; +import type { RowRange } from './useGridRowSpanning'; -export function getUnprocessedRange( - testRange: { firstRowIndex: number; lastRowIndex: number }, - processedRange: { firstRowIndex: number; lastRowIndex: number }, -) { +export function getUnprocessedRange(testRange: RowRange, processedRange: RowRange) { if ( testRange.firstRowIndex >= processedRange.firstRowIndex && testRange.lastRowIndex <= processedRange.lastRowIndex @@ -43,13 +41,9 @@ export function isUninitializedRowContext(renderContext: GridRenderContext) { return renderContext.firstRowIndex === 0 && renderContext.lastRowIndex === 0; } -export function isRowRenderContextUpdated( - prevRenderContext: GridRenderContext, - renderContext: GridRenderContext, -) { +export function isRowRangeUpdated(range1: RowRange, range2: RowRange) { return ( - prevRenderContext.firstRowIndex !== renderContext.firstRowIndex || - prevRenderContext.lastRowIndex !== renderContext.lastRowIndex + range1.firstRowIndex !== range2.firstRowIndex || range1.lastRowIndex !== range2.lastRowIndex ); } diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index 1ea69b3eb78f..f9e9852b62de 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -1,17 +1,17 @@ import * as React from 'react'; import useLazyRef from '@mui/utils/useLazyRef'; -import { GridColDef } from '../../../models/colDef'; -import { GridRowId, GridValidRowModel, GridRowEntry } from '../../../models/gridRows'; -import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; -import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; -import { GridStateInitializer } from '../../utils/useGridInitializeState'; import { gridVisibleColumnDefinitionsSelector } from '../columns/gridColumnsSelector'; import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; import { gridRenderContextSelector } from '../virtualization/gridVirtualizationSelectors'; import { useGridSelector } from '../../utils/useGridSelector'; +import type { GridColDef } from '../../../models/colDef'; +import type { GridRowId, GridValidRowModel, GridRowEntry } from '../../../models/gridRows'; +import type { DataGridProcessedProps } from '../../../models/props/DataGridProps'; +import type { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; +import type { GridStateInitializer } from '../../utils/useGridInitializeState'; import { getUnprocessedRange, - isRowRenderContextUpdated, + isRowRangeUpdated, isUninitializedRowContext, getCellValue, } from './gridRowSpanningUtils'; @@ -27,7 +27,7 @@ export interface GridRowSpanningState { hiddenCellOriginMap: Record>; } -type RowRange = { firstRowIndex: number; lastRowIndex: number }; +export type RowRange = { firstRowIndex: number; lastRowIndex: number }; const EMPTY_STATE = { spannedCells: {}, hiddenCells: {}, hiddenCellOriginMap: {} }; const EMPTY_RANGE: RowRange = { firstRowIndex: 0, lastRowIndex: 0 }; @@ -37,6 +37,7 @@ const computeRowSpanningState = ( apiRef: React.MutableRefObject, colDefs: GridColDef[], visibleRows: GridRowEntry[], + range: RowRange, rangeToProcess: RowRange, resetState: boolean, processedRange: RowRange, @@ -82,7 +83,7 @@ const computeRowSpanningState = ( let prevIndex = index - 1; const prevRowEntry = visibleRows[prevIndex]; while ( - prevIndex >= rangeToProcess.firstRowIndex && + prevIndex >= range.firstRowIndex && getCellValue(prevRowEntry.model, colDef, apiRef) === cellValue ) { const currentRow = visibleRows[prevIndex + 1]; @@ -110,7 +111,7 @@ const computeRowSpanningState = ( // Scan the next rows let relativeIndex = index + 1; while ( - relativeIndex <= rangeToProcess.lastRowIndex && + relativeIndex <= range.lastRowIndex && visibleRows[relativeIndex] && getCellValue(visibleRows[relativeIndex].model, colDef, apiRef) === cellValue ) { @@ -186,6 +187,7 @@ export const rowSpanningStateInitializer: GridStateInitializer = (state, props, colDefs, rows, rangeToProcess, + rangeToProcess, true, EMPTY_RANGE, ); @@ -220,6 +222,7 @@ export const useGridRowSpanning = ( } : EMPTY_RANGE; }); + const lastRange = React.useRef(EMPTY_RANGE); const updateRowSpanningState = React.useCallback( // A reset needs to occur when: @@ -265,6 +268,7 @@ export const useGridRowSpanning = ( apiRef, colDefs, visibleRows, + range, rangeToProcess, resetState, processedRange.current, @@ -314,18 +318,24 @@ export const useGridRowSpanning = ( const prevRenderContext = React.useRef(renderContext); const isFirstRender = React.useRef(true); + const shouldResetState = React.useRef(false); React.useEffect(() => { const firstRender = isFirstRender.current; if (isFirstRender.current) { isFirstRender.current = false; } + if (range && lastRange.current && isRowRangeUpdated(range, lastRange.current)) { + lastRange.current = range; + shouldResetState.current = true; + } if (!firstRender && prevRenderContext.current !== renderContext) { - if (isRowRenderContextUpdated(prevRenderContext.current, renderContext)) { - updateRowSpanningState(false); + if (isRowRangeUpdated(prevRenderContext.current, renderContext)) { + updateRowSpanningState(shouldResetState.current); + shouldResetState.current = false; } prevRenderContext.current = renderContext; return; } updateRowSpanningState(); - }, [updateRowSpanningState, renderContext]); + }, [updateRowSpanningState, renderContext, range, lastRange]); }; From a3005ab02369e40fce4cb4ea50d3eeb56889a6a9 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 11 Sep 2024 19:51:28 +0500 Subject: [PATCH 39/47] Docs improvement --- docs/data/data-grid/row-spanning/RowSpanningCustom.js | 1 - docs/data/data-grid/row-spanning/RowSpanningCustom.tsx | 1 - docs/data/data-grid/row-spanning/row-spanning.md | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/data/data-grid/row-spanning/RowSpanningCustom.js b/docs/data/data-grid/row-spanning/RowSpanningCustom.js index df77d480b9c6..695063fd8165 100644 --- a/docs/data/data-grid/row-spanning/RowSpanningCustom.js +++ b/docs/data/data-grid/row-spanning/RowSpanningCustom.js @@ -51,7 +51,6 @@ const columns = [ return `${value} yo`; }, rowSpanValueGetter: (value, row) => { - console.log(row); return row ? `${row.name}-${row.age}` : value; }, }, diff --git a/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx b/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx index 3be8ad78094c..431a49aa7151 100644 --- a/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx +++ b/docs/data/data-grid/row-spanning/RowSpanningCustom.tsx @@ -51,7 +51,6 @@ const columns: GridColDef[] = [ return `${value} yo`; }, rowSpanValueGetter: (value, row) => { - console.log(row); return row ? `${row.name}-${row.age}` : value; }, }, diff --git a/docs/data/data-grid/row-spanning/row-spanning.md b/docs/data/data-grid/row-spanning/row-spanning.md index 5b97740aa373..a346fc7bec9c 100644 --- a/docs/data/data-grid/row-spanning/row-spanning.md +++ b/docs/data/data-grid/row-spanning/row-spanning.md @@ -25,7 +25,7 @@ The row spanning generally works with features like [sorting](/x/react-data-grid ::: :::warning -The row spanning works by increasing the height of the spanned cell by a factor `rowHeight`, it doesn't work properly with variable and dynamic row height. +The row spanning works by increasing the height of the spanned cell by a factor of `rowHeight`, it doesn't work properly with variable and dynamic row height. ::: ## Customizing row spanned cells From 72cccda5659ec1c3e54c1ba04e1a147271740d76 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 16 Sep 2024 17:48:44 +0500 Subject: [PATCH 40/47] Add more tests --- .../src/tests/rowSpanning.DataGrid.test.tsx | 156 +++++++++++++----- 1 file changed, 115 insertions(+), 41 deletions(-) diff --git a/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx b/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx index 8d9668c1f660..ff50468d3085 100644 --- a/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx +++ b/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import { createRenderer } from '@mui/internal-test-utils'; +import { createRenderer, waitFor, fireEvent, act } from '@mui/internal-test-utils'; import { expect } from 'chai'; import { DataGrid, useGridApiRef, DataGridProps, GridApi } from '@mui/x-data-grid'; -import { getCell } from 'test/utils/helperFn'; +import { getCell, getActiveCell } from 'test/utils/helperFn'; const isJSDOM = /jsdom/.test(window.navigator.userAgent); @@ -127,48 +127,122 @@ describe(' - Row spanning', () => { expect(spannedCell).to.have.style('height', `${rowHeight * spanValue.code}px`); }); - it('should work with sorting', function test() { - if (isJSDOM) { - this.skip(); - } - render( - , - ); - const rowsWithSpannedCells = Object.keys(apiRef.current.state.rowSpanning.spannedCells); - expect(rowsWithSpannedCells.length).to.equal(1); - const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows('4'); - expect(rowIndex).to.equal(1); - const spanValue = apiRef.current.state.rowSpanning.spannedCells['4']; - expect(spanValue).to.deep.equal({ code: 3, totalPrice: 3 }); - const spannedCell = getCell(rowIndex, 0); - expect(spannedCell).to.have.style('height', `${rowHeight * spanValue.code}px`); + describe('sorting', () => { + it('should work with sorting when initializing sorting', function test() { + if (isJSDOM) { + this.skip(); + } + render( + , + ); + const rowsWithSpannedCells = Object.keys(apiRef.current.state.rowSpanning.spannedCells); + expect(rowsWithSpannedCells.length).to.equal(1); + const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows('4'); + expect(rowIndex).to.equal(1); + const spanValue = apiRef.current.state.rowSpanning.spannedCells['4']; + expect(spanValue).to.deep.equal({ code: 3, totalPrice: 3 }); + const spannedCell = getCell(rowIndex, 0); + expect(spannedCell).to.have.style('height', `${rowHeight * spanValue.code}px`); + }); + + it('should work with sorting when controlling sorting', function test() { + if (isJSDOM) { + this.skip(); + } + render(); + const rowsWithSpannedCells = Object.keys(apiRef.current.state.rowSpanning.spannedCells); + expect(rowsWithSpannedCells.length).to.equal(1); + const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows('4'); + expect(rowIndex).to.equal(1); + const spanValue = apiRef.current.state.rowSpanning.spannedCells['4']; + expect(spanValue).to.deep.equal({ code: 3, totalPrice: 3 }); + const spannedCell = getCell(rowIndex, 0); + expect(spannedCell).to.have.style('height', `${rowHeight * spanValue.code}px`); + }); }); - it('should work with filtering', function test() { - if (isJSDOM) { - this.skip(); - } - render( - { + it('should work with filtering when initializing filter', function test() { + if (isJSDOM) { + this.skip(); + } + render( + , - ); - const rowsWithSpannedCells = Object.keys(apiRef.current.state.rowSpanning.spannedCells); - expect(rowsWithSpannedCells.length).to.equal(1); - const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows('5'); - expect(rowIndex).to.equal(0); - const spanValue = apiRef.current.state.rowSpanning.spannedCells['5']; - expect(spanValue).to.deep.equal({ code: 2, totalPrice: 2 }); - const spannedCell = getCell(rowIndex, 0); - expect(spannedCell).to.have.style('height', `${rowHeight * spanValue.code}px`); + }} + />, + ); + const rowsWithSpannedCells = Object.keys(apiRef.current.state.rowSpanning.spannedCells); + expect(rowsWithSpannedCells.length).to.equal(1); + const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows('5'); + expect(rowIndex).to.equal(0); + const spanValue = apiRef.current.state.rowSpanning.spannedCells['5']; + expect(spanValue).to.deep.equal({ code: 2, totalPrice: 2 }); + const spannedCell = getCell(rowIndex, 0); + expect(spannedCell).to.have.style('height', `${rowHeight * spanValue.code}px`); + }); + + it('should work with filtering when controlling filter', function test() { + if (isJSDOM) { + this.skip(); + } + render( + , + ); + const rowsWithSpannedCells = Object.keys(apiRef.current.state.rowSpanning.spannedCells); + expect(rowsWithSpannedCells.length).to.equal(1); + const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows('5'); + expect(rowIndex).to.equal(0); + const spanValue = apiRef.current.state.rowSpanning.spannedCells['5']; + expect(spanValue).to.deep.equal({ code: 2, totalPrice: 2 }); + const spannedCell = getCell(rowIndex, 0); + expect(spannedCell).to.have.style('height', `${rowHeight * spanValue.code}px`); + }); + }); + + describe('pagination', () => { + it('should only compute the row spanning state for current page', async () => { + render( + , + ); + expect(Object.keys(apiRef.current.state.rowSpanning.spannedCells).length).to.equal(0); + apiRef.current.setPage(1); + await waitFor(() => + expect(Object.keys(apiRef.current.state.rowSpanning.spannedCells).length).to.equal(1), + ); + expect(Object.keys(apiRef.current.state.rowSpanning.hiddenCells).length).to.equal(1); + }); + }); + + describe('keyboard navigation', () => { + it('should respect the spanned cells when navigating using keyboard', () => { + render(); + // Set focus to the cell with value `- 16GB RAM Upgrade` + act(() => apiRef.current.setCellFocus(5, 'description')); + expect(getActiveCell()).to.equal('4-1'); + const cell41 = getCell(4, 1); + fireEvent.keyDown(cell41, { key: 'ArrowLeft' }); + expect(getActiveCell()).to.equal('3-0'); + const cell30 = getCell(3, 0); + fireEvent.keyDown(cell30, { key: 'ArrowRight' }); + expect(getActiveCell()).to.equal('3-1'); + }); }); - // TODO: Add tests for keyboard navigation - // TODO: Add tests for column reordering + // TODO: Add tests for row reordering }); From e6ba91dd316c20f7a5530b6ba57b75d8525f7956 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 16 Sep 2024 17:50:34 +0500 Subject: [PATCH 41/47] Update getting started --- docs/data/data-grid/getting-started/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/data-grid/getting-started/getting-started.md b/docs/data/data-grid/getting-started/getting-started.md index f3820ef2c15e..421cc64832d5 100644 --- a/docs/data/data-grid/getting-started/getting-started.md +++ b/docs/data/data-grid/getting-started/getting-started.md @@ -188,7 +188,7 @@ The enterprise components come in two plans: Pro and Premium. | [Column pinning](/x/react-data-grid/column-pinning/) | ❌ | βœ… | βœ… | | **Row** | | | | | [Row height](/x/react-data-grid/row-height/) | βœ… | βœ… | βœ… | -| [Row spanning](/x/react-data-grid/row-spanning/) | 🚧 | 🚧 | 🚧 | +| [Row spanning](/x/react-data-grid/row-spanning/) | βœ… | βœ… | βœ… | | [Row reordering](/x/react-data-grid/row-ordering/) | ❌ | βœ… | βœ… | | [Row pinning](/x/react-data-grid/row-pinning/) | ❌ | βœ… | βœ… | | **Selection** | | | | From f68b1ed8e69ff0d54f85ce0f7a0ea253c20231ce Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 16 Sep 2024 19:13:28 +0500 Subject: [PATCH 42/47] Skip JS dom test --- packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx b/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx index ff50468d3085..c2f1e8ec9179 100644 --- a/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx +++ b/packages/x-data-grid/src/tests/rowSpanning.DataGrid.test.tsx @@ -212,7 +212,10 @@ describe(' - Row spanning', () => { }); describe('pagination', () => { - it('should only compute the row spanning state for current page', async () => { + it('should only compute the row spanning state for current page', async function test() { + if (isJSDOM) { + this.skip(); + } render( Date: Thu, 19 Sep 2024 00:11:24 +0500 Subject: [PATCH 43/47] Armin's code review comments addressed --- .../useGridKeyboardNavigation.ts | 1 + .../hooks/features/rows/gridRowSpanningUtils.ts | 4 ++-- .../hooks/features/rows/useGridRowSpanning.ts | 17 +++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts index 915791716bad..5ad04140d74b 100644 --- a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts +++ b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts @@ -69,6 +69,7 @@ export const useGridKeyboardNavigation = ( * @param {number} colIndex Index of the column to focus * @param {GridRowId} rowId index of the row to focus * @param {string} closestColumnToUse Which closest column cell to use when the cell is spanned by `colSpan`. + * @param {string} rowSpanScanDirection Which direction to search to find the next cell not hidden by `rowSpan`. * TODO replace with apiRef.current.moveFocusToRelativeCell() */ const goToCell = React.useCallback( diff --git a/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningUtils.ts b/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningUtils.ts index 9b5c61b88eda..6720ed4bd337 100644 --- a/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningUtils.ts +++ b/packages/x-data-grid/src/hooks/features/rows/gridRowSpanningUtils.ts @@ -37,8 +37,8 @@ export function getUnprocessedRange(testRange: RowRange, processedRange: RowRang return testRange; } -export function isUninitializedRowContext(renderContext: GridRenderContext) { - return renderContext.firstRowIndex === 0 && renderContext.lastRowIndex === 0; +export function isRowContextInitialized(renderContext: GridRenderContext) { + return renderContext.firstRowIndex !== 0 || renderContext.lastRowIndex !== 0; } export function isRowRangeUpdated(range1: RowRange, range2: RowRange) { diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index f9e9852b62de..0fd6b3a1c492 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -12,7 +12,7 @@ import type { GridStateInitializer } from '../../utils/useGridInitializeState'; import { getUnprocessedRange, isRowRangeUpdated, - isUninitializedRowContext, + isRowContextInitialized, getCellValue, } from './gridRowSpanningUtils'; @@ -32,6 +32,12 @@ export type RowRange = { firstRowIndex: number; lastRowIndex: number }; const EMPTY_STATE = { spannedCells: {}, hiddenCells: {}, hiddenCellOriginMap: {} }; const EMPTY_RANGE: RowRange = { firstRowIndex: 0, lastRowIndex: 0 }; const skippedFields = new Set(['__check__', '__reorder__', '__detail_panel_toggle__']); +/** + * Default number of rows to process during state initialization to avoid flickering. + * Number `20` is arbitrarily chosen to be large enough to cover most of the cases without + * compromising performance. + */ +const DEFAULT_ROWS_TO_PROCESS = 20; const computeRowSpanningState = ( apiRef: React.MutableRefObject, @@ -175,7 +181,7 @@ export const rowSpanningStateInitializer: GridStateInitializer = (state, props, } const rangeToProcess = { firstRowIndex: 0, - lastRowIndex: Math.min(19, Math.max(rowIds.length - 1, 0)), + lastRowIndex: Math.min(DEFAULT_ROWS_TO_PROCESS - 1, Math.max(rowIds.length - 1, 0)), }; const rows = rowIds.map((id) => ({ id, @@ -218,7 +224,10 @@ export const useGridRowSpanning = ( return Object.keys(apiRef.current.state.rowSpanning.spannedCells).length > 0 ? { firstRowIndex: 0, - lastRowIndex: Math.min(19, Math.max(apiRef.current.state.rows.dataRowIds.length - 1, 0)), + lastRowIndex: Math.min( + DEFAULT_ROWS_TO_PROCESS - 1, + Math.max(apiRef.current.state.rows.dataRowIds.length - 1, 0), + ), } : EMPTY_RANGE; }); @@ -239,7 +248,7 @@ export const useGridRowSpanning = ( return; } - if (range === null || isUninitializedRowContext(renderContext)) { + if (range === null || !isRowContextInitialized(renderContext)) { return; } From 085b108042ab67d338e28a37794fe110398a3724 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Fri, 20 Sep 2024 05:55:51 +0500 Subject: [PATCH 44/47] Apply suggestions from code review Co-authored-by: Sycamore <71297412+samuelsycamore@users.noreply.github.com> Signed-off-by: Bilal Shafi --- .../data-grid/row-spanning/row-spanning.md | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/data/data-grid/row-spanning/row-spanning.md b/docs/data/data-grid/row-spanning/row-spanning.md index a346fc7bec9c..0f352ced2ed7 100644 --- a/docs/data/data-grid/row-spanning/row-spanning.md +++ b/docs/data/data-grid/row-spanning/row-spanning.md @@ -2,9 +2,8 @@

Span cells across several rows.

-Each cell takes up the height of one row. -Row spanning lets you change this default behavior, so cells can span multiple rows. -This is very close to the "row spanning" in an HTML `
`. +By default, each cell in a Data Grid takes up the height of one row. +The row spanning feature makes it possible for a cell to fill multiple rows in a single column. To enable, pass the `unstable_rowSpanning` prop to the Data Grid. The Data Grid will automatically merge consecutive cells with the repeating values in the same column. @@ -15,40 +14,37 @@ Switch off the toggle button to see actual rows. {{"demo": "RowSpanning.js", "bg": "inline", "defaultCodeOpen": false}} :::info -In the above demo, the `quantity` column has been delibrately excluded from row spanning computation by using `colDef.rowSpanValueGetter` prop. +In this demo, the `quantity` column has been deliberately excluded from the row spanning computation using the `colDef.rowSpanValueGetter` prop. -See the [Customizing row spanned cells](#customizing-row-spanned-cells) section for more details. +See the [Customizing row-spanning cells](#customizing-row-spanning-cells) section for more details. ::: :::warning -The row spanning generally works with features like [sorting](/x/react-data-grid/sorting/) and [filtering](/x/react-data-grid/filtering/), be sure to check if everything works as expected when using it in combination with features like [column spanning](/x/react-data-grid/column-spanning/). +Row spanning works well with features like [sorting](/x/react-data-grid/sorting/) and [filtering](/x/react-data-grid/filtering/), but be sure to check that everything works as expected when using it with [column spanning](/x/react-data-grid/column-spanning/). ::: :::warning -The row spanning works by increasing the height of the spanned cell by a factor of `rowHeight`, it doesn't work properly with variable and dynamic row height. +Row spanning works by increasing the height of the spanned cell by a factor of `rowHeight`β€”it won't work properly with a variable or dynamic height. ::: -## Customizing row spanned cells +## Customizing row-spanning cells You could customize the value used in row spanning computation using `colDef.rowSpanValueGetter` prop and both the value used in row spanning computation and the value used in cell using `colDef.valueGetter` prop. +This can be useful when there are other repeating values present that should not span multiple rows. -This could be useful when there _are_ some repeating values but should not be row spanned due to belonging to different entities. - -In the following example, `rowSpanValueGetter` is used to avoid merging `age` cells that do not belong to the same person. +In the following example, `rowSpanValueGetter` is used to avoid merging `age` cells that don't belong to the same person. {{"demo": "RowSpanningCustom.js", "bg": "inline", "defaultCodeOpen": false}} ## Usage with column spanning -Row spanning could be used in conjunction with column spanning to achieve cells that span both rows and columns. - -The following weekly university class schedule uses cells that span both rows and columns. +Row spanning can be used in conjunction with column spanning to create cells that span multiple rows and columns simultaneously, as shown in the demo below: {{"demo": "RowSpanningClassSchedule.js", "bg": "inline", "defaultCodeOpen": false}} ## Demo -Here's the familiar calender demo that you might have seen in the column spanning [documentation](/x/react-data-grid/column-spanning/#function-signature), implemented with the row spanning. +The demo below recreates the calendar from the [column spanning documentation](/x/react-data-grid/column-spanning/#function-signature) using the row spanning feature: {{"demo": "RowSpanningCalender.js", "bg": "inline", "defaultCodeOpen": false}} From fdcf26bd79cb1cdb20d56987f82ed54ec85867ff Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Fri, 20 Sep 2024 06:02:28 +0500 Subject: [PATCH 45/47] Move warning and rephrase --- docs/data/data-grid/row-spanning/row-spanning.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/data/data-grid/row-spanning/row-spanning.md b/docs/data/data-grid/row-spanning/row-spanning.md index 0f352ced2ed7..be352ce2cb7c 100644 --- a/docs/data/data-grid/row-spanning/row-spanning.md +++ b/docs/data/data-grid/row-spanning/row-spanning.md @@ -19,18 +19,18 @@ In this demo, the `quantity` column has been deliberately excluded from the row See the [Customizing row-spanning cells](#customizing-row-spanning-cells) section for more details. ::: -:::warning -Row spanning works well with features like [sorting](/x/react-data-grid/sorting/) and [filtering](/x/react-data-grid/filtering/), but be sure to check that everything works as expected when using it with [column spanning](/x/react-data-grid/column-spanning/). -::: - :::warning Row spanning works by increasing the height of the spanned cell by a factor of `rowHeight`β€”it won't work properly with a variable or dynamic height. ::: ## Customizing row-spanning cells -You could customize the value used in row spanning computation using `colDef.rowSpanValueGetter` prop and both the value used in row spanning computation and the value used in cell using `colDef.valueGetter` prop. -This can be useful when there are other repeating values present that should not span multiple rows. +You can customize how row spanning works using two props: + +- `colDef.rowSpanValueGetter`: Controls which values are used for row spanning +- `colDef.valueGetter`: Controls both the row spanning logic and the cell value + +This lets you prevent unwanted row spanning when there are repeating values that shouldn't be merged. In the following example, `rowSpanValueGetter` is used to avoid merging `age` cells that don't belong to the same person. @@ -42,6 +42,10 @@ Row spanning can be used in conjunction with column spanning to create cells tha {{"demo": "RowSpanningClassSchedule.js", "bg": "inline", "defaultCodeOpen": false}} +:::warning +Row spanning works well with features like [sorting](/x/react-data-grid/sorting/) and [filtering](/x/react-data-grid/filtering/), but be sure to check that everything works as expected when using it with [column spanning](/x/react-data-grid/column-spanning/). +::: + ## Demo The demo below recreates the calendar from the [column spanning documentation](/x/react-data-grid/column-spanning/#function-signature) using the row spanning feature: From 31b421e0cf035228d73b3a8938a972b005788b0a Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Fri, 20 Sep 2024 06:16:24 +0500 Subject: [PATCH 46/47] CI From 9408e2f81a27f6ea68c3ffdb7d64afb313911475 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Fri, 20 Sep 2024 06:18:58 +0500 Subject: [PATCH 47/47] Update --- docs/data/data-grid/row-spanning/row-spanning.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/data/data-grid/row-spanning/row-spanning.md b/docs/data/data-grid/row-spanning/row-spanning.md index be352ce2cb7c..ab59f7680148 100644 --- a/docs/data/data-grid/row-spanning/row-spanning.md +++ b/docs/data/data-grid/row-spanning/row-spanning.md @@ -6,10 +6,7 @@ By default, each cell in a Data Grid takes up the height of one row. The row spanning feature makes it possible for a cell to fill multiple rows in a single column. To enable, pass the `unstable_rowSpanning` prop to the Data Grid. -The Data Grid will automatically merge consecutive cells with the repeating values in the same column. - -In the following example, the row spanning causes the cells with the same values in a column to be merged. -Switch off the toggle button to see actual rows. +The Data Grid will automatically merge consecutive cells with repeating values in the same column, as shown in the demo belowβ€”switch off the toggle button to see the actual rows: {{"demo": "RowSpanning.js", "bg": "inline", "defaultCodeOpen": false}}