@mui/x-data-grid 7.17.0 → 7.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +98 -5
  2. package/DataGrid/DataGrid.js +11 -1
  3. package/DataGrid/useDataGridComponent.js +3 -0
  4. package/DataGrid/useDataGridProps.js +2 -1
  5. package/components/GridRow.js +1 -0
  6. package/components/cell/GridCell.js +30 -5
  7. package/hooks/features/columnHeaders/useGridColumnHeaders.d.ts +0 -1
  8. package/hooks/features/columnHeaders/useGridColumnHeaders.js +11 -11
  9. package/hooks/features/columnResize/useGridColumnResize.js +6 -6
  10. package/hooks/features/dimensions/gridDimensionsApi.d.ts +4 -0
  11. package/hooks/features/dimensions/useGridDimensions.d.ts +1 -1
  12. package/hooks/features/dimensions/useGridDimensions.js +4 -1
  13. package/hooks/features/editing/useGridCellEditing.js +2 -18
  14. package/hooks/features/editing/useGridRowEditing.js +6 -1
  15. package/hooks/features/editing/utils.d.ts +2 -0
  16. package/hooks/features/editing/utils.js +15 -0
  17. package/hooks/features/export/useGridPrintExport.js +2 -1
  18. package/hooks/features/focus/useGridFocus.js +2 -1
  19. package/hooks/features/keyboardNavigation/useGridKeyboardNavigation.js +10 -46
  20. package/hooks/features/keyboardNavigation/utils.d.ts +17 -0
  21. package/hooks/features/keyboardNavigation/utils.js +58 -0
  22. package/hooks/features/rows/gridRowSpanningSelectors.d.ts +4 -0
  23. package/hooks/features/rows/gridRowSpanningSelectors.js +5 -0
  24. package/hooks/features/rows/gridRowSpanningUtils.d.ts +10 -0
  25. package/hooks/features/rows/gridRowSpanningUtils.js +42 -0
  26. package/hooks/features/rows/useGridRowSpanning.d.ts +27 -0
  27. package/hooks/features/rows/useGridRowSpanning.js +257 -0
  28. package/hooks/features/virtualization/useGridVirtualScroller.d.ts +1 -1
  29. package/hooks/features/virtualization/useGridVirtualScroller.js +17 -7
  30. package/hooks/utils/useGridApiEventHandler.js +0 -1
  31. package/index.js +1 -1
  32. package/internals/index.d.ts +1 -0
  33. package/internals/index.js +1 -0
  34. package/models/colDef/gridColDef.d.ts +4 -0
  35. package/models/gridStateCommunity.d.ts +2 -0
  36. package/models/props/DataGridProps.d.ts +10 -0
  37. package/modern/DataGrid/DataGrid.js +11 -1
  38. package/modern/DataGrid/useDataGridComponent.js +3 -0
  39. package/modern/DataGrid/useDataGridProps.js +2 -1
  40. package/modern/components/GridRow.js +1 -0
  41. package/modern/components/cell/GridCell.js +30 -5
  42. package/modern/hooks/features/columnHeaders/useGridColumnHeaders.js +11 -11
  43. package/modern/hooks/features/columnResize/useGridColumnResize.js +6 -6
  44. package/modern/hooks/features/dimensions/useGridDimensions.js +4 -1
  45. package/modern/hooks/features/editing/useGridCellEditing.js +2 -18
  46. package/modern/hooks/features/editing/useGridRowEditing.js +6 -1
  47. package/modern/hooks/features/editing/utils.js +15 -0
  48. package/modern/hooks/features/export/useGridPrintExport.js +2 -1
  49. package/modern/hooks/features/focus/useGridFocus.js +2 -1
  50. package/modern/hooks/features/keyboardNavigation/useGridKeyboardNavigation.js +10 -46
  51. package/modern/hooks/features/keyboardNavigation/utils.js +58 -0
  52. package/modern/hooks/features/rows/gridRowSpanningSelectors.js +5 -0
  53. package/modern/hooks/features/rows/gridRowSpanningUtils.js +42 -0
  54. package/modern/hooks/features/rows/useGridRowSpanning.js +257 -0
  55. package/modern/hooks/features/virtualization/useGridVirtualScroller.js +17 -7
  56. package/modern/hooks/utils/useGridApiEventHandler.js +0 -1
  57. package/modern/index.js +1 -1
  58. package/modern/internals/index.js +1 -0
  59. package/modern/utils/domUtils.js +12 -12
  60. package/node/DataGrid/DataGrid.js +11 -1
  61. package/node/DataGrid/useDataGridComponent.js +3 -0
  62. package/node/DataGrid/useDataGridProps.js +2 -1
  63. package/node/components/GridRow.js +1 -0
  64. package/node/components/cell/GridCell.js +30 -5
  65. package/node/hooks/features/columnHeaders/useGridColumnHeaders.js +11 -11
  66. package/node/hooks/features/columnResize/useGridColumnResize.js +6 -6
  67. package/node/hooks/features/dimensions/useGridDimensions.js +4 -1
  68. package/node/hooks/features/editing/useGridCellEditing.js +2 -18
  69. package/node/hooks/features/editing/useGridRowEditing.js +6 -1
  70. package/node/hooks/features/editing/utils.js +22 -0
  71. package/node/hooks/features/export/useGridPrintExport.js +2 -1
  72. package/node/hooks/features/focus/useGridFocus.js +2 -1
  73. package/node/hooks/features/keyboardNavigation/useGridKeyboardNavigation.js +16 -53
  74. package/node/hooks/features/keyboardNavigation/utils.js +68 -0
  75. package/node/hooks/features/rows/gridRowSpanningSelectors.js +11 -0
  76. package/node/hooks/features/rows/gridRowSpanningUtils.js +52 -0
  77. package/node/hooks/features/rows/useGridRowSpanning.js +267 -0
  78. package/node/hooks/features/virtualization/useGridVirtualScroller.js +17 -7
  79. package/node/hooks/utils/useGridApiEventHandler.js +0 -1
  80. package/node/index.js +1 -1
  81. package/node/internals/index.js +15 -0
  82. package/node/utils/domUtils.js +12 -12
  83. package/package.json +3 -3
  84. package/utils/domUtils.d.ts +4 -4
  85. package/utils/domUtils.js +12 -12
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { useRtl } from '@mui/system/RtlProvider';
3
- import { gridVisibleColumnDefinitionsSelector } from "../columns/gridColumnsSelector.js";
3
+ import { gridVisibleColumnDefinitionsSelector, gridVisibleColumnFieldsSelector } from "../columns/gridColumnsSelector.js";
4
4
  import { useGridLogger } from "../../utils/useGridLogger.js";
5
5
  import { useGridApiEventHandler } from "../../utils/useGridApiEventHandler.js";
6
6
  import { gridExpandedSortedRowEntriesSelector } from "../filter/gridFilterSelector.js";
@@ -10,50 +10,12 @@ import { gridClasses } from "../../../constants/gridClasses.js";
10
10
  import { GridCellModes } from "../../../models/gridEditRowModel.js";
11
11
  import { isNavigationKey } from "../../../utils/keyboardUtils.js";
12
12
  import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from "../../../constants/gridDetailPanelToggleField.js";
13
- import { gridPinnedRowsSelector } from "../rows/gridRowsSelector.js";
14
13
  import { gridFocusColumnGroupHeaderSelector } from "../focus/index.js";
15
14
  import { gridColumnGroupsHeaderMaxDepthSelector } from "../columnGrouping/gridColumnGroupsSelector.js";
16
15
  import { gridHeaderFilteringEditFieldSelector, gridHeaderFilteringMenuSelector } from "../headerFiltering/gridHeaderFilteringSelectors.js";
17
16
  import { useGridRegisterPipeProcessor } from "../../core/pipeProcessing/index.js";
18
17
  import { isEventTargetInPortal } from "../../../utils/domUtils.js";
19
- function enrichPageRowsWithPinnedRows(apiRef, rows) {
20
- const pinnedRows = gridPinnedRowsSelector(apiRef) || {};
21
- return [...(pinnedRows.top || []), ...rows, ...(pinnedRows.bottom || [])];
22
- }
23
- const getLeftColumnIndex = ({
24
- currentColIndex,
25
- firstColIndex,
26
- lastColIndex,
27
- isRtl
28
- }) => {
29
- if (isRtl) {
30
- if (currentColIndex < lastColIndex) {
31
- return currentColIndex + 1;
32
- }
33
- } else if (!isRtl) {
34
- if (currentColIndex > firstColIndex) {
35
- return currentColIndex - 1;
36
- }
37
- }
38
- return null;
39
- };
40
- const getRightColumnIndex = ({
41
- currentColIndex,
42
- firstColIndex,
43
- lastColIndex,
44
- isRtl
45
- }) => {
46
- if (isRtl) {
47
- if (currentColIndex > firstColIndex) {
48
- return currentColIndex - 1;
49
- }
50
- } else if (!isRtl) {
51
- if (currentColIndex < lastColIndex) {
52
- return currentColIndex + 1;
53
- }
54
- }
55
- return null;
56
- };
18
+ import { enrichPageRowsWithPinnedRows, getLeftColumnIndex, getRightColumnIndex, findNonRowSpannedCell } from "./utils.js";
57
19
 
58
20
  /**
59
21
  * @requires useGridSorting (method) - can be after
@@ -73,11 +35,12 @@ export const useGridKeyboardNavigation = (apiRef, props) => {
73
35
 
74
36
  /**
75
37
  * @param {number} colIndex Index of the column to focus
76
- * @param {number} rowIndex index of the row to focus
38
+ * @param {GridRowId} rowId index of the row to focus
77
39
  * @param {string} closestColumnToUse Which closest column cell to use when the cell is spanned by `colSpan`.
40
+ * @param {string} rowSpanScanDirection Which direction to search to find the next cell not hidden by `rowSpan`.
78
41
  * TODO replace with apiRef.current.moveFocusToRelativeCell()
79
42
  */
80
- const goToCell = React.useCallback((colIndex, rowId, closestColumnToUse = 'left') => {
43
+ const goToCell = React.useCallback((colIndex, rowId, closestColumnToUse = 'left', rowSpanScanDirection = 'up') => {
81
44
  const visibleSortedRows = gridExpandedSortedRowEntriesSelector(apiRef);
82
45
  const nextCellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo(rowId, colIndex);
83
46
  if (nextCellColSpanInfo && nextCellColSpanInfo.spannedByColSpan) {
@@ -87,16 +50,17 @@ export const useGridKeyboardNavigation = (apiRef, props) => {
87
50
  colIndex = nextCellColSpanInfo.rightVisibleCellIndex;
88
51
  }
89
52
  }
53
+ const field = gridVisibleColumnFieldsSelector(apiRef)[colIndex];
54
+ const nonRowSpannedRowId = findNonRowSpannedCell(apiRef, rowId, field, rowSpanScanDirection);
90
55
  // `scrollToIndexes` requires a rowIndex relative to all visible rows.
91
56
  // Those rows do not include pinned rows, but pinned rows do not need scroll anyway.
92
- const rowIndexRelativeToAllRows = visibleSortedRows.findIndex(row => row.id === rowId);
57
+ const rowIndexRelativeToAllRows = visibleSortedRows.findIndex(row => row.id === nonRowSpannedRowId);
93
58
  logger.debug(`Navigating to cell row ${rowIndexRelativeToAllRows}, col ${colIndex}`);
94
59
  apiRef.current.scrollToIndexes({
95
60
  colIndex,
96
61
  rowIndex: rowIndexRelativeToAllRows
97
62
  });
98
- const field = apiRef.current.getVisibleColumns()[colIndex].field;
99
- apiRef.current.setCellFocus(rowId, field);
63
+ apiRef.current.setCellFocus(nonRowSpannedRowId, field);
100
64
  }, [apiRef, logger]);
101
65
  const goToHeader = React.useCallback((colIndex, event) => {
102
66
  logger.debug(`Navigating to header col ${colIndex}`);
@@ -432,7 +396,7 @@ export const useGridKeyboardNavigation = (apiRef, props) => {
432
396
  {
433
397
  // "Enter" is only triggered by the row / cell editing feature
434
398
  if (rowIndexBefore < lastRowIndexInPage) {
435
- goToCell(colIndexBefore, getRowIdFromIndex(rowIndexBefore + 1));
399
+ goToCell(colIndexBefore, getRowIdFromIndex(rowIndexBefore + 1), isRtl ? 'right' : 'left', 'down');
436
400
  }
437
401
  break;
438
402
  }
@@ -0,0 +1,17 @@
1
+ import * as React from 'react';
2
+ import { GridColDef, GridRowEntry, GridRowId } from '../../../models';
3
+ import { GridApiCommunity } from '../../../models/api/gridApiCommunity';
4
+ export declare function enrichPageRowsWithPinnedRows(apiRef: React.MutableRefObject<GridApiCommunity>, rows: GridRowEntry[]): GridRowEntry<import("../../../models").GridValidRowModel>[];
5
+ export declare const getLeftColumnIndex: ({ currentColIndex, firstColIndex, lastColIndex, isRtl, }: {
6
+ currentColIndex: number;
7
+ firstColIndex: number;
8
+ lastColIndex: number;
9
+ isRtl: boolean;
10
+ }) => number | null;
11
+ export declare const getRightColumnIndex: ({ currentColIndex, firstColIndex, lastColIndex, isRtl, }: {
12
+ currentColIndex: number;
13
+ firstColIndex: number;
14
+ lastColIndex: number;
15
+ isRtl: boolean;
16
+ }) => number | null;
17
+ export declare function findNonRowSpannedCell(apiRef: React.MutableRefObject<GridApiCommunity>, rowId: GridRowId, field: GridColDef['field'], rowSpanScanDirection: 'up' | 'down'): GridRowId;
@@ -0,0 +1,58 @@
1
+ import { gridFilteredSortedRowIdsSelector } from "../filter/gridFilterSelector.js";
2
+ import { gridRowSpanningHiddenCellsSelector } from "../rows/gridRowSpanningSelectors.js";
3
+ import { gridPinnedRowsSelector } from "../rows/gridRowsSelector.js";
4
+ export function enrichPageRowsWithPinnedRows(apiRef, rows) {
5
+ const pinnedRows = gridPinnedRowsSelector(apiRef) || {};
6
+ return [...(pinnedRows.top || []), ...rows, ...(pinnedRows.bottom || [])];
7
+ }
8
+ export const getLeftColumnIndex = ({
9
+ currentColIndex,
10
+ firstColIndex,
11
+ lastColIndex,
12
+ isRtl
13
+ }) => {
14
+ if (isRtl) {
15
+ if (currentColIndex < lastColIndex) {
16
+ return currentColIndex + 1;
17
+ }
18
+ } else if (!isRtl) {
19
+ if (currentColIndex > firstColIndex) {
20
+ return currentColIndex - 1;
21
+ }
22
+ }
23
+ return null;
24
+ };
25
+ export const getRightColumnIndex = ({
26
+ currentColIndex,
27
+ firstColIndex,
28
+ lastColIndex,
29
+ isRtl
30
+ }) => {
31
+ if (isRtl) {
32
+ if (currentColIndex > firstColIndex) {
33
+ return currentColIndex - 1;
34
+ }
35
+ } else if (!isRtl) {
36
+ if (currentColIndex < lastColIndex) {
37
+ return currentColIndex + 1;
38
+ }
39
+ }
40
+ return null;
41
+ };
42
+ export function findNonRowSpannedCell(apiRef, rowId, field, rowSpanScanDirection) {
43
+ const rowSpanHiddenCells = gridRowSpanningHiddenCellsSelector(apiRef);
44
+ if (!rowSpanHiddenCells[rowId]?.[field]) {
45
+ return rowId;
46
+ }
47
+ const filteredSortedRowIds = gridFilteredSortedRowIdsSelector(apiRef);
48
+ // find closest non row spanned cell in the given `rowSpanScanDirection`
49
+ let nextRowIndex = filteredSortedRowIds.indexOf(rowId) + (rowSpanScanDirection === 'down' ? 1 : -1);
50
+ while (nextRowIndex >= 0 && nextRowIndex < filteredSortedRowIds.length) {
51
+ const nextRowId = filteredSortedRowIds[nextRowIndex];
52
+ if (!rowSpanHiddenCells[nextRowId]?.[field]) {
53
+ return nextRowId;
54
+ }
55
+ nextRowIndex += rowSpanScanDirection === 'down' ? 1 : -1;
56
+ }
57
+ return rowId;
58
+ }
@@ -0,0 +1,4 @@
1
+ import { GridStateCommunity } from '../../../models/gridStateCommunity';
2
+ export declare const gridRowSpanningHiddenCellsSelector: import("../../../utils/createSelector").OutputSelector<GridStateCommunity, Record<import("../../..").GridRowId, Record<string, boolean>>>;
3
+ export declare const gridRowSpanningSpannedCellsSelector: import("../../../utils/createSelector").OutputSelector<GridStateCommunity, Record<import("../../..").GridRowId, Record<string, number>>>;
4
+ export declare const gridRowSpanningHiddenCellsOriginMapSelector: import("../../../utils/createSelector").OutputSelector<GridStateCommunity, Record<number, Record<string, number>>>;
@@ -0,0 +1,5 @@
1
+ import { createSelector } from "../../../utils/createSelector.js";
2
+ const gridRowSpanningStateSelector = state => state.rowSpanning;
3
+ export const gridRowSpanningHiddenCellsSelector = createSelector(gridRowSpanningStateSelector, rowSpanning => rowSpanning.hiddenCells);
4
+ export const gridRowSpanningSpannedCellsSelector = createSelector(gridRowSpanningStateSelector, rowSpanning => rowSpanning.spannedCells);
5
+ export const gridRowSpanningHiddenCellsOriginMapSelector = createSelector(gridRowSpanningStateSelector, rowSpanning => rowSpanning.hiddenCellOriginMap);
@@ -0,0 +1,10 @@
1
+ import * as React from 'react';
2
+ import type { GridRenderContext } from '../../../models';
3
+ import type { GridValidRowModel } from '../../../models/gridRows';
4
+ import type { GridColDef } from '../../../models/colDef';
5
+ import type { GridApiCommunity } from '../../../models/api/gridApiCommunity';
6
+ import type { RowRange } from './useGridRowSpanning';
7
+ export declare function getUnprocessedRange(testRange: RowRange, processedRange: RowRange): RowRange | null;
8
+ export declare function isRowContextInitialized(renderContext: GridRenderContext): boolean;
9
+ export declare function isRowRangeUpdated(range1: RowRange, range2: RowRange): boolean;
10
+ export declare const getCellValue: (row: GridValidRowModel, colDef: GridColDef, apiRef: React.MutableRefObject<GridApiCommunity>) => any;
@@ -0,0 +1,42 @@
1
+ export function getUnprocessedRange(testRange, processedRange) {
2
+ if (testRange.firstRowIndex >= processedRange.firstRowIndex && testRange.lastRowIndex <= processedRange.lastRowIndex) {
3
+ return null;
4
+ }
5
+ // Overflowing at the end
6
+ // Example: testRange={ firstRowIndex: 10, lastRowIndex: 20 }, processedRange={ firstRowIndex: 0, lastRowIndex: 15 }
7
+ // Unprocessed Range={ firstRowIndex: 16, lastRowIndex: 20 }
8
+ if (testRange.firstRowIndex >= processedRange.firstRowIndex && testRange.lastRowIndex > processedRange.lastRowIndex) {
9
+ return {
10
+ firstRowIndex: processedRange.lastRowIndex,
11
+ lastRowIndex: testRange.lastRowIndex
12
+ };
13
+ }
14
+ // Overflowing at the beginning
15
+ // Example: testRange={ firstRowIndex: 0, lastRowIndex: 20 }, processedRange={ firstRowIndex: 16, lastRowIndex: 30 }
16
+ // Unprocessed Range={ firstRowIndex: 0, lastRowIndex: 15 }
17
+ if (testRange.firstRowIndex < processedRange.firstRowIndex && testRange.lastRowIndex <= processedRange.lastRowIndex) {
18
+ return {
19
+ firstRowIndex: testRange.firstRowIndex,
20
+ lastRowIndex: processedRange.firstRowIndex - 1
21
+ };
22
+ }
23
+ // TODO: Should return two ranges handle overflowing at both ends ?
24
+ return testRange;
25
+ }
26
+ export function isRowContextInitialized(renderContext) {
27
+ return renderContext.firstRowIndex !== 0 || renderContext.lastRowIndex !== 0;
28
+ }
29
+ export function isRowRangeUpdated(range1, range2) {
30
+ return range1.firstRowIndex !== range2.firstRowIndex || range1.lastRowIndex !== range2.lastRowIndex;
31
+ }
32
+ export const getCellValue = (row, colDef, apiRef) => {
33
+ if (!row) {
34
+ return null;
35
+ }
36
+ let cellValue = row[colDef.field];
37
+ const valueGetter = colDef.rowSpanValueGetter ?? colDef.valueGetter;
38
+ if (valueGetter) {
39
+ cellValue = valueGetter(cellValue, row, colDef, apiRef);
40
+ }
41
+ return cellValue;
42
+ };
@@ -0,0 +1,27 @@
1
+ import * as React from 'react';
2
+ import type { GridColDef } from '../../../models/colDef';
3
+ import type { GridRowId } from '../../../models/gridRows';
4
+ import type { DataGridProcessedProps } from '../../../models/props/DataGridProps';
5
+ import type { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity';
6
+ import type { GridStateInitializer } from '../../utils/useGridInitializeState';
7
+ export interface GridRowSpanningState {
8
+ spannedCells: Record<GridRowId, Record<GridColDef['field'], number>>;
9
+ hiddenCells: Record<GridRowId, Record<GridColDef['field'], boolean>>;
10
+ /**
11
+ * For each hidden cell, it contains the row index corresponding to the cell that is
12
+ * the origin of the hidden cell. i.e. the cell which is spanned.
13
+ * Used by the virtualization to properly keep the spanned cells in view.
14
+ */
15
+ hiddenCellOriginMap: Record<number, Record<GridColDef['field'], number>>;
16
+ }
17
+ export type RowRange = {
18
+ firstRowIndex: number;
19
+ lastRowIndex: number;
20
+ };
21
+ /**
22
+ * @requires columnsStateInitializer (method) - should be initialized before
23
+ * @requires rowsStateInitializer (method) - should be initialized before
24
+ * @requires filterStateInitializer (method) - should be initialized before
25
+ */
26
+ export declare const rowSpanningStateInitializer: GridStateInitializer;
27
+ export declare const useGridRowSpanning: (apiRef: React.MutableRefObject<GridPrivateApiCommunity>, props: Pick<DataGridProcessedProps, "unstable_rowSpanning" | "pagination" | "paginationMode">) => void;
@@ -0,0 +1,257 @@
1
+ import _extends from "@babel/runtime/helpers/esm/extends";
2
+ import * as React from 'react';
3
+ import useLazyRef from '@mui/utils/useLazyRef';
4
+ import { gridVisibleColumnDefinitionsSelector } from "../columns/gridColumnsSelector.js";
5
+ import { useGridVisibleRows } from "../../utils/useGridVisibleRows.js";
6
+ import { gridRenderContextSelector } from "../virtualization/gridVirtualizationSelectors.js";
7
+ import { useGridSelector } from "../../utils/useGridSelector.js";
8
+ import { getUnprocessedRange, isRowRangeUpdated, isRowContextInitialized, getCellValue } from "./gridRowSpanningUtils.js";
9
+ const EMPTY_STATE = {
10
+ spannedCells: {},
11
+ hiddenCells: {},
12
+ hiddenCellOriginMap: {}
13
+ };
14
+ const EMPTY_RANGE = {
15
+ firstRowIndex: 0,
16
+ lastRowIndex: 0
17
+ };
18
+ const skippedFields = new Set(['__check__', '__reorder__', '__detail_panel_toggle__']);
19
+ /**
20
+ * Default number of rows to process during state initialization to avoid flickering.
21
+ * Number `20` is arbitrarily chosen to be large enough to cover most of the cases without
22
+ * compromising performance.
23
+ */
24
+ const DEFAULT_ROWS_TO_PROCESS = 20;
25
+ const computeRowSpanningState = (apiRef, colDefs, visibleRows, range, rangeToProcess, resetState, processedRange) => {
26
+ const spannedCells = resetState ? {} : _extends({}, apiRef.current.state.rowSpanning.spannedCells);
27
+ const hiddenCells = resetState ? {} : _extends({}, apiRef.current.state.rowSpanning.hiddenCells);
28
+ const hiddenCellOriginMap = resetState ? {} : _extends({}, apiRef.current.state.rowSpanning.hiddenCellOriginMap);
29
+ if (resetState) {
30
+ processedRange = EMPTY_RANGE;
31
+ }
32
+ colDefs.forEach(colDef => {
33
+ if (skippedFields.has(colDef.field)) {
34
+ return;
35
+ }
36
+ for (let index = rangeToProcess.firstRowIndex; index <= rangeToProcess.lastRowIndex; index += 1) {
37
+ const row = visibleRows[index];
38
+ if (hiddenCells[row.id]?.[colDef.field]) {
39
+ continue;
40
+ }
41
+ const cellValue = getCellValue(row.model, colDef, apiRef);
42
+ if (cellValue == null) {
43
+ continue;
44
+ }
45
+ let spannedRowId = row.id;
46
+ let spannedRowIndex = index;
47
+ let rowSpan = 0;
48
+
49
+ // For first index, also scan in the previous rows to handle the reset state case e.g by sorting
50
+ const backwardsHiddenCells = [];
51
+ if (index === rangeToProcess.firstRowIndex) {
52
+ let prevIndex = index - 1;
53
+ const prevRowEntry = visibleRows[prevIndex];
54
+ while (prevIndex >= range.firstRowIndex && getCellValue(prevRowEntry.model, colDef, apiRef) === cellValue) {
55
+ const currentRow = visibleRows[prevIndex + 1];
56
+ if (hiddenCells[currentRow.id]) {
57
+ hiddenCells[currentRow.id][colDef.field] = true;
58
+ } else {
59
+ hiddenCells[currentRow.id] = {
60
+ [colDef.field]: true
61
+ };
62
+ }
63
+ backwardsHiddenCells.push(index);
64
+ rowSpan += 1;
65
+ spannedRowId = prevRowEntry.id;
66
+ spannedRowIndex = prevIndex;
67
+ prevIndex -= 1;
68
+ }
69
+ }
70
+ backwardsHiddenCells.forEach(hiddenCellIndex => {
71
+ if (hiddenCellOriginMap[hiddenCellIndex]) {
72
+ hiddenCellOriginMap[hiddenCellIndex][colDef.field] = spannedRowIndex;
73
+ } else {
74
+ hiddenCellOriginMap[hiddenCellIndex] = {
75
+ [colDef.field]: spannedRowIndex
76
+ };
77
+ }
78
+ });
79
+
80
+ // Scan the next rows
81
+ let relativeIndex = index + 1;
82
+ while (relativeIndex <= range.lastRowIndex && visibleRows[relativeIndex] && getCellValue(visibleRows[relativeIndex].model, colDef, apiRef) === cellValue) {
83
+ const currentRow = visibleRows[relativeIndex];
84
+ if (hiddenCells[currentRow.id]) {
85
+ hiddenCells[currentRow.id][colDef.field] = true;
86
+ } else {
87
+ hiddenCells[currentRow.id] = {
88
+ [colDef.field]: true
89
+ };
90
+ }
91
+ if (hiddenCellOriginMap[relativeIndex]) {
92
+ hiddenCellOriginMap[relativeIndex][colDef.field] = spannedRowIndex;
93
+ } else {
94
+ hiddenCellOriginMap[relativeIndex] = {
95
+ [colDef.field]: spannedRowIndex
96
+ };
97
+ }
98
+ relativeIndex += 1;
99
+ rowSpan += 1;
100
+ }
101
+ if (rowSpan > 0) {
102
+ if (spannedCells[spannedRowId]) {
103
+ spannedCells[spannedRowId][colDef.field] = rowSpan + 1;
104
+ } else {
105
+ spannedCells[spannedRowId] = {
106
+ [colDef.field]: rowSpan + 1
107
+ };
108
+ }
109
+ }
110
+ }
111
+ processedRange = {
112
+ firstRowIndex: Math.min(processedRange.firstRowIndex, rangeToProcess.firstRowIndex),
113
+ lastRowIndex: Math.max(processedRange.lastRowIndex, rangeToProcess.lastRowIndex)
114
+ };
115
+ });
116
+ return {
117
+ spannedCells,
118
+ hiddenCells,
119
+ hiddenCellOriginMap,
120
+ processedRange
121
+ };
122
+ };
123
+
124
+ /**
125
+ * @requires columnsStateInitializer (method) - should be initialized before
126
+ * @requires rowsStateInitializer (method) - should be initialized before
127
+ * @requires filterStateInitializer (method) - should be initialized before
128
+ */
129
+ export const rowSpanningStateInitializer = (state, props, apiRef) => {
130
+ if (props.unstable_rowSpanning) {
131
+ const rowIds = state.rows.dataRowIds || [];
132
+ const orderedFields = state.columns.orderedFields || [];
133
+ const dataRowIdToModelLookup = state.rows.dataRowIdToModelLookup;
134
+ const columnsLookup = state.columns.lookup;
135
+ const isFilteringPending = Boolean(state.filter.filterModel.items.length) || Boolean(state.filter.filterModel.quickFilterValues?.length);
136
+ if (!rowIds.length || !orderedFields.length || !dataRowIdToModelLookup || !columnsLookup || isFilteringPending) {
137
+ return _extends({}, state, {
138
+ rowSpanning: EMPTY_STATE
139
+ });
140
+ }
141
+ const rangeToProcess = {
142
+ firstRowIndex: 0,
143
+ lastRowIndex: Math.min(DEFAULT_ROWS_TO_PROCESS - 1, Math.max(rowIds.length - 1, 0))
144
+ };
145
+ const rows = rowIds.map(id => ({
146
+ id,
147
+ model: dataRowIdToModelLookup[id]
148
+ }));
149
+ const colDefs = orderedFields.map(field => columnsLookup[field]);
150
+ const {
151
+ spannedCells,
152
+ hiddenCells,
153
+ hiddenCellOriginMap
154
+ } = computeRowSpanningState(apiRef, colDefs, rows, rangeToProcess, rangeToProcess, true, EMPTY_RANGE);
155
+ return _extends({}, state, {
156
+ rowSpanning: {
157
+ spannedCells,
158
+ hiddenCells,
159
+ hiddenCellOriginMap
160
+ }
161
+ });
162
+ }
163
+ return _extends({}, state, {
164
+ rowSpanning: EMPTY_STATE
165
+ });
166
+ };
167
+ export const useGridRowSpanning = (apiRef, props) => {
168
+ const {
169
+ range,
170
+ rows: visibleRows
171
+ } = useGridVisibleRows(apiRef, props);
172
+ const renderContext = useGridSelector(apiRef, gridRenderContextSelector);
173
+ const colDefs = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector);
174
+ const processedRange = useLazyRef(() => {
175
+ return Object.keys(apiRef.current.state.rowSpanning.spannedCells).length > 0 ? {
176
+ firstRowIndex: 0,
177
+ lastRowIndex: Math.min(DEFAULT_ROWS_TO_PROCESS - 1, Math.max(apiRef.current.state.rows.dataRowIds.length - 1, 0))
178
+ } : EMPTY_RANGE;
179
+ });
180
+ const lastRange = React.useRef(EMPTY_RANGE);
181
+ const updateRowSpanningState = React.useCallback(
182
+ // A reset needs to occur when:
183
+ // - The `unstable_rowSpanning` prop is updated (feature flag)
184
+ // - The filtering is applied
185
+ // - The sorting is applied
186
+ // - The `paginationModel` is updated
187
+ // - The rows are updated
188
+ (resetState = true) => {
189
+ if (!props.unstable_rowSpanning) {
190
+ if (apiRef.current.state.rowSpanning !== EMPTY_STATE) {
191
+ apiRef.current.setState(state => _extends({}, state, {
192
+ rowSpanning: EMPTY_STATE
193
+ }));
194
+ }
195
+ return;
196
+ }
197
+ if (range === null || !isRowContextInitialized(renderContext)) {
198
+ return;
199
+ }
200
+ if (resetState) {
201
+ processedRange.current = EMPTY_RANGE;
202
+ }
203
+ const rangeToProcess = getUnprocessedRange({
204
+ firstRowIndex: renderContext.firstRowIndex,
205
+ lastRowIndex: renderContext.lastRowIndex - 1
206
+ }, processedRange.current);
207
+ if (rangeToProcess === null) {
208
+ return;
209
+ }
210
+ const {
211
+ spannedCells,
212
+ hiddenCells,
213
+ hiddenCellOriginMap,
214
+ processedRange: newProcessedRange
215
+ } = computeRowSpanningState(apiRef, colDefs, visibleRows, range, rangeToProcess, resetState, processedRange.current);
216
+ processedRange.current = newProcessedRange;
217
+ const newSpannedCellsCount = Object.keys(spannedCells).length;
218
+ const newHiddenCellsCount = Object.keys(hiddenCells).length;
219
+ const currentSpannedCellsCount = Object.keys(apiRef.current.state.rowSpanning.spannedCells).length;
220
+ const currentHiddenCellsCount = Object.keys(apiRef.current.state.rowSpanning.hiddenCells).length;
221
+ const shouldUpdateState = resetState || newSpannedCellsCount !== currentSpannedCellsCount || newHiddenCellsCount !== currentHiddenCellsCount;
222
+ if (!shouldUpdateState) {
223
+ return;
224
+ }
225
+ apiRef.current.setState(state => {
226
+ return _extends({}, state, {
227
+ rowSpanning: {
228
+ spannedCells,
229
+ hiddenCells,
230
+ hiddenCellOriginMap
231
+ }
232
+ });
233
+ });
234
+ }, [apiRef, props.unstable_rowSpanning, range, renderContext, visibleRows, colDefs, processedRange]);
235
+ const prevRenderContext = React.useRef(renderContext);
236
+ const isFirstRender = React.useRef(true);
237
+ const shouldResetState = React.useRef(false);
238
+ React.useEffect(() => {
239
+ const firstRender = isFirstRender.current;
240
+ if (isFirstRender.current) {
241
+ isFirstRender.current = false;
242
+ }
243
+ if (range && lastRange.current && isRowRangeUpdated(range, lastRange.current)) {
244
+ lastRange.current = range;
245
+ shouldResetState.current = true;
246
+ }
247
+ if (!firstRender && prevRenderContext.current !== renderContext) {
248
+ if (isRowRangeUpdated(prevRenderContext.current, renderContext)) {
249
+ updateRowSpanningState(shouldResetState.current);
250
+ shouldResetState.current = false;
251
+ }
252
+ prevRenderContext.current = renderContext;
253
+ return;
254
+ }
255
+ updateRowSpanningState();
256
+ }, [updateRowSpanningState, renderContext, range, lastRange]);
257
+ };
@@ -40,4 +40,4 @@ export declare const useGridVirtualScroller: () => {
40
40
  };
41
41
  };
42
42
  export declare function areRenderContextsEqual(context1: GridRenderContext, context2: GridRenderContext): boolean;
43
- export declare function computeOffsetLeft(columnPositions: number[], renderContext: GridColumnsRenderContext, isRtl: boolean, pinnedLeftLength: number): number;
43
+ export declare function computeOffsetLeft(columnPositions: number[], renderContext: GridColumnsRenderContext, pinnedLeftLength: number): number;
@@ -24,6 +24,7 @@ import { getFirstNonSpannedColumnToRender } from "../columns/gridColumnsUtils.js
24
24
  import { getMinimalContentHeight } from "../rows/gridRowsUtils.js";
25
25
  import { gridRenderContextSelector, gridVirtualizationRowEnabledSelector, gridVirtualizationColumnEnabledSelector } from "./gridVirtualizationSelectors.js";
26
26
  import { EMPTY_RENDER_CONTEXT } from "./useGridVirtualization.js";
27
+ import { gridRowSpanningHiddenCellsOriginMapSelector } from "../rows/gridRowSpanningSelectors.js";
27
28
  import { jsx as _jsx } from "react/jsx-runtime";
28
29
  const MINIMUM_COLUMN_WIDTH = 50;
29
30
  var ScrollDirection = /*#__PURE__*/function (ScrollDirection) {
@@ -325,7 +326,7 @@ export const useGridVirtualScroller = () => {
325
326
  if (!isPinnedSection && frozenContext.current && rowIndexInPage >= frozenContext.current.firstRowIndex && rowIndexInPage < frozenContext.current.lastRowIndex) {
326
327
  currentRenderContext = frozenContext.current;
327
328
  }
328
- const offsetLeft = computeOffsetLeft(columnPositions, currentRenderContext, isRtl, pinnedColumns.left.length);
329
+ const offsetLeft = computeOffsetLeft(columnPositions, currentRenderContext, pinnedColumns.left.length);
329
330
  const showBottomBorder = isLastVisibleInSection && params.position === 'top';
330
331
  rows.push(/*#__PURE__*/_jsx(rootProps.slots.row, _extends({
331
332
  row: model,
@@ -353,7 +354,7 @@ export const useGridVirtualScroller = () => {
353
354
  if (panel) {
354
355
  rows.push(panel);
355
356
  }
356
- if (isLastVisible) {
357
+ if (params.position === undefined && isLastVisibleInSection) {
357
358
  rows.push(apiRef.current.getInfiniteLoadingTriggerElement?.({
358
359
  lastRowId: id
359
360
  }));
@@ -447,6 +448,7 @@ function inputsSelector(apiRef, rootProps, enabledForRows, enabledForColumns) {
447
448
  const dimensions = gridDimensionsSelector(apiRef.current.state);
448
449
  const currentPage = getVisibleRows(apiRef, rootProps);
449
450
  const visibleColumns = gridVisibleColumnDefinitionsSelector(apiRef);
451
+ const hiddenCellsOriginMap = gridRowSpanningHiddenCellsOriginMapSelector(apiRef);
450
452
  const lastRowId = apiRef.current.state.rows.dataRowIds.at(-1);
451
453
  const lastColumn = visibleColumns.at(-1);
452
454
  return {
@@ -467,7 +469,8 @@ function inputsSelector(apiRef, rootProps, enabledForRows, enabledForColumns) {
467
469
  rows: currentPage.rows,
468
470
  range: currentPage.range,
469
471
  pinnedColumns: gridVisiblePinnedColumnDefinitionsSelector(apiRef),
470
- visibleColumns
472
+ visibleColumns,
473
+ hiddenCellsOriginMap
471
474
  };
472
475
  }
473
476
  function computeRenderContext(inputs, scrollPosition, scrollCache) {
@@ -485,10 +488,18 @@ function computeRenderContext(inputs, scrollPosition, scrollCache) {
485
488
  if (inputs.enabledForRows) {
486
489
  // Clamp the value because the search may return an index out of bounds.
487
490
  // In the last index, this is not needed because Array.slice doesn't include it.
488
- const firstRowIndex = Math.min(getNearestIndexToRender(inputs, top, {
491
+ let firstRowIndex = Math.min(getNearestIndexToRender(inputs, top, {
489
492
  atStart: true,
490
493
  lastPosition: inputs.rowsMeta.positions[inputs.rowsMeta.positions.length - 1] + inputs.lastRowHeight
491
494
  }), inputs.rowsMeta.positions.length - 1);
495
+
496
+ // If any of the cells in the `firstRowIndex` is hidden due to an extended row span,
497
+ // Make sure the row from where the rowSpan is originated is visible.
498
+ const rowSpanHiddenCellOrigin = inputs.hiddenCellsOriginMap[firstRowIndex];
499
+ if (rowSpanHiddenCellOrigin) {
500
+ const minSpannedRowIndex = Math.min(...Object.values(rowSpanHiddenCellOrigin));
501
+ firstRowIndex = Math.min(firstRowIndex, minSpannedRowIndex);
502
+ }
492
503
  const lastRowIndex = inputs.autoHeight ? firstRowIndex + inputs.rows.length : getNearestIndexToRender(inputs, top + inputs.viewportInnerHeight);
493
504
  renderContext.firstRowIndex = firstRowIndex;
494
505
  renderContext.lastRowIndex = lastRowIndex;
@@ -642,9 +653,8 @@ export function areRenderContextsEqual(context1, context2) {
642
653
  }
643
654
  return context1.firstRowIndex === context2.firstRowIndex && context1.lastRowIndex === context2.lastRowIndex && context1.firstColumnIndex === context2.firstColumnIndex && context1.lastColumnIndex === context2.lastColumnIndex;
644
655
  }
645
- export function computeOffsetLeft(columnPositions, renderContext, isRtl, pinnedLeftLength) {
646
- const factor = isRtl ? -1 : 1;
647
- const left = factor * (columnPositions[renderContext.firstColumnIndex] ?? 0) - (columnPositions[pinnedLeftLength] ?? 0);
656
+ export function computeOffsetLeft(columnPositions, renderContext, pinnedLeftLength) {
657
+ const left = (columnPositions[renderContext.firstColumnIndex] ?? 0) - (columnPositions[pinnedLeftLength] ?? 0);
648
658
  return Math.abs(left);
649
659
  }
650
660
  function directionForDelta(dx, dy) {
@@ -90,7 +90,6 @@ const optionsSubscriberOptions = {
90
90
  isFirst: true
91
91
  };
92
92
  export function useGridApiOptionHandler(apiRef, eventName, handler) {
93
- // Validate that only one per event name?
94
93
  useGridApiEventHandler(apiRef, eventName, handler, optionsSubscriberOptions);
95
94
  }
96
95
  export { GridSignature };
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid v7.17.0
2
+ * @mui/x-data-grid v7.18.0
3
3
  *
4
4
  * @license MIT
5
5
  * This source code is licensed under the MIT license found in the
@@ -41,6 +41,7 @@ export { useGridPreferencesPanel, preferencePanelStateInitializer, } from '../ho
41
41
  export { useGridEditing, editingStateInitializer } from '../hooks/features/editing/useGridEditing';
42
42
  export { gridEditRowsStateSelector } from '../hooks/features/editing/gridEditingSelectors';
43
43
  export { useGridRows, rowsStateInitializer } from '../hooks/features/rows/useGridRows';
44
+ export { useGridRowSpanning, rowSpanningStateInitializer, } from '../hooks/features/rows/useGridRowSpanning';
44
45
  export { useGridAriaAttributes } from '../hooks/utils/useGridAriaAttributes';
45
46
  export { useGridRowAriaAttributes } from '../hooks/features/rows/useGridRowAriaAttributes';
46
47
  export { useGridRowsPreProcessors } from '../hooks/features/rows/useGridRowsPreProcessors';
@@ -32,6 +32,7 @@ export { useGridPreferencesPanel, preferencePanelStateInitializer } from "../hoo
32
32
  export { useGridEditing, editingStateInitializer } from "../hooks/features/editing/useGridEditing.js";
33
33
  export { gridEditRowsStateSelector } from "../hooks/features/editing/gridEditingSelectors.js";
34
34
  export { useGridRows, rowsStateInitializer } from "../hooks/features/rows/useGridRows.js";
35
+ export { useGridRowSpanning, rowSpanningStateInitializer } from "../hooks/features/rows/useGridRowSpanning.js";
35
36
  export { useGridAriaAttributes } from "../hooks/utils/useGridAriaAttributes.js";
36
37
  export { useGridRowAriaAttributes } from "../hooks/features/rows/useGridRowAriaAttributes.js";
37
38
  export { useGridRowsPreProcessors } from "../hooks/features/rows/useGridRowsPreProcessors.js";
@@ -129,6 +129,10 @@ export interface GridBaseColDef<R extends GridValidRowModel = GridValidRowModel,
129
129
  * Function that allows to get a specific data instead of field to render in the cell.
130
130
  */
131
131
  valueGetter?: GridValueGetter<R, V, F>;
132
+ /**
133
+ * Function that allows to provide a specific value to be used in row spanning.
134
+ */
135
+ rowSpanValueGetter?: GridValueGetter<R, V, F>;
132
136
  /**
133
137
  * Function that allows to customize how the entered value is stored in the row.
134
138
  * It only works with cell/row editing.