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 0000000000000..b20f3ca5c7e2c --- /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 0000000000000..449ce6482d75f --- /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 7640695d86d6c..19a41594c1519 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 42b9d2dde827e..858525d2d19e1 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 4e29d17e4f7bc..a5a59d8a77969 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 2a196dccb5a73..17008e80fcf2e 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 0000000000000..67ac552b26e80 --- /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 0000000000000..ab0d5919e2ba7 --- /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 7e00992692bf8..d5cc67d7e7185 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 47a56c9a742a6..f555f596a1bdc 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 b20f3ca5c7e2c..bc40acabb0c4c 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 449ce6482d75f..19c0d6cdfb4da 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 17008e80fcf2e..cfd9679e976b2 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 ab0d5919e2ba7..f4f8331e53c04 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 1854e8a134b05..80d72ec59b957 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 b61e63a1f9277..8a3951e8e6eea 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 d902aa413bb60..966770103edbd 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 90f1ca592e487..6611d9cad6370 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 1c79c35a04e6e..a668806ae4cf5 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 c3eb24a2ffb89..53ce625dde800 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 7543c9d9f0abb..68dc7d23a7e96 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 a8b79b49fb93e..0d93f8b2c03be 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 914817291c87f..676053f4913ed 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 08fa6f50e6c46..ebf7ff6c4abaa 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 13c08a00b7e19..9e68c043f1ce3 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 1cacac533b70c..c6fa34f2c276c 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 ab054cbab9179..ac864d6a611d7 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 6611d9cad6370..6e12e1f3f5100 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 19a41594c1519..4522cad362909 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 62ed986c8ca8c..eba91fb7ce87f 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 f4f8331e53c04..ec5da5c347fcd 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 bc40acabb0c4c..d5c92e81af647 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 19c0d6cdfb4da..319316dd629c1 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 26d5acce67f3b..eece855816ed8 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 bfe97f8a97043..0546471b90ea7 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 669318bc48489..e07981f5de85a 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 087bee3376cce..ec34f0c620938 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 15b648e0809e1..1e63902cd79e5 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 fdd0720fd1a26..55fceed053c94 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 cfd9679e976b2..d9c31efd46e32 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 ec5da5c347fcd..1c7726f57869e 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 4030443ff315d..039fb6589f5ee 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 4522cad362909..a3c13b2fce2b8 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 eba91fb7ce87f..83cb88c2672e6 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 6f2fb91843d04..aaf3c0a0cb4d9 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 0000000000000..a9186afd29384 --- /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 0000000000000..ee8db7e153b58 --- /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 0000000000000..5e4f81d674b1b --- /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 0000000000000..4ba725593bde0 --- /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 a3c13b2fce2b8..f9c79dfa0de7d 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 d5c92e81af647..457de3a427ef1 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 319316dd629c1..79b70da9d61a2 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 a9186afd29384..30aef71a55d11 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 ee8db7e153b58..368f4c4de9b9d 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 0000000000000..bba8d8d1ef972 --- /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 5e4f81d674b1b..28fd09fd379c0 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 4ba725593bde0..f35866d679237 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 0000000000000..a480fc16c4b89 --- /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 0000000000000..274772bb16af6 --- /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 f9c79dfa0de7d..7a51d53aba1d0 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 80d72ec59b957..d3c17d5522edf 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 d3c17d5522edf..fbcd1faa72d3d 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 0000000000000..853f0b0d34700 --- /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 fbcd1faa72d3d..73bd89cf060b4 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 83b6c92385829..d3502e158e170 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 457de3a427ef1..83d6836747001 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 79b70da9d61a2..d41727f1ba11c 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 83d6836747001..18de309f185c3 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 d41727f1ba11c..c17090c935142 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 7a51d53aba1d0..7d16ff6da03da 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 1c7726f57869e..ed0c99f3d887b 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 a480fc16c4b89..4d0881c9c34ef 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 274772bb16af6..44e3e28e7c032 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 ed0c99f3d887b..cb814eb37684a 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 cb814eb37684a..ea0339f74a2e8 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 ea0339f74a2e8..a7e92d64c3c39 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 67ac552b26e80..e9da213ee77ab 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 a7e92d64c3c39..3cde556d18032 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 d986452fabbc3..c239ac0a04aa8 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 3cde556d18032..0396914903780 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 0000000000000..0d2f7a01263c5 --- /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 0396914903780..34aa73450d09b 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 3e7b0518bf612..0714ac03fd425 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 0714ac03fd425..915791716bad6 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 0d2f7a01263c5..8d9668c1f660d 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 18de309f185c3..109566ed72271 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 967de44756c07..df77d480b9c6d 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 1f44e525d350f..3be8ad78094c6 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 d3502e158e170..54a67f3c3908c 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 0000000000000..032ac9209ac00 --- /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 34aa73450d09b..b080caaf18a5a 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 b080caaf18a5a..5e794a58e2f94 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 7d16ff6da03da..5b97740aa3731 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 5e794a58e2f94..e18ee33d048d0 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 8a3951e8e6eea..12899dd06ce2f 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 966770103edbd..6b8b06fc21bb5 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 858525d2d19e1..85f9a09cb3eaa 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 e18ee33d048d0..1ea69b3eb78fe 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 032ac9209ac00..9b5c61b88edaf 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 1ea69b3eb78fe..f9e9852b62def 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 df77d480b9c6d..695063fd8165a 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 3be8ad78094c6..431a49aa7151e 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 5b97740aa3731..a346fc7bec9c4 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 8d9668c1f660d..ff50468d3085c 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 f3820ef2c15ec..421cc64832d59 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 ff50468d3085c..c2f1e8ec91790 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 915791716bad6..5ad04140d74b9 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 9b5c61b88edaf..6720ed4bd3374 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 f9e9852b62def..0fd6b3a1c492f 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 a346fc7bec9c4..0f352ced2ed71 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 0f352ced2ed71..be352ce2cb7c5 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 be352ce2cb7c5..ab59f7680148d 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}}