@opencorestack/opengridx 2.0.4 → 3.0.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.
- package/CHANGELOG.md +408 -3
- package/README.md +93 -72
- package/dist/index.d.ts +611 -121
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7992 -5650
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +17 -23
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +348 -161
- package/docs/README.md +6 -1
- package/docs/architecture/datagrid-orchestration.md +81 -0
- package/docs/architecture/grid-row-meta.md +102 -0
- package/docs/architecture/use-grid-columns.md +128 -0
- package/docs/architecture/use-grid-controlled-state.md +157 -0
- package/docs/architecture/use-grid-row-pipeline.md +120 -0
- package/docs/architecture/use-grid-scroll-sync.md +111 -0
- package/docs/architecture/use-grid-state-snapshot.md +90 -0
- package/docs/architecture/use-grid-virtualization.md +115 -0
- package/docs/architecture/use-grid-visible-rows.md +85 -0
- package/docs/assets/banner-alt.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/hero.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/components/aggregation-footer.md +15 -10
- package/docs/components/cell.md +16 -6
- package/docs/components/column-group-header.md +55 -28
- package/docs/components/column-resize.md +17 -13
- package/docs/components/column-visibility.md +45 -22
- package/docs/components/datagrid.md +14 -12
- package/docs/components/empty-state.md +2 -1
- package/docs/components/error-overlay.md +12 -7
- package/docs/components/filter-panel.md +28 -8
- package/docs/components/header.md +28 -9
- package/docs/components/pagination.md +14 -8
- package/docs/components/row.md +21 -5
- package/docs/components/toolbar.md +27 -9
- package/docs/components/tooltip.md +16 -7
- package/docs/customization/slots-api.md +100 -36
- package/docs/customization/theming.md +66 -18
- package/docs/features/aggregation-pivot.md +75 -9
- package/docs/features/cell-spanning.md +80 -125
- package/docs/features/clipboard.md +41 -16
- package/docs/features/custom-pagination.md +38 -18
- package/docs/features/data-source.md +36 -13
- package/docs/features/editing-reordering.md +125 -16
- package/docs/features/export-guide.md +147 -41
- package/docs/features/filtering.md +44 -6
- package/docs/features/infinite-scroll.md +54 -46
- package/docs/features/keyboard-navigation.md +83 -0
- package/docs/features/list-view.md +58 -5
- package/docs/features/loading-states.md +18 -20
- package/docs/features/master-detail.md +18 -5
- package/docs/features/pdf-export.md +56 -19
- package/docs/features/pinning.md +23 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +66 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +44 -9
- package/docs/features/virtualization.md +64 -15
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1222 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +16 -10
- package/lib/components/Cell/Cell.css +13 -1
- package/lib/components/Cell/Cell.tsx +130 -57
- package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
- package/lib/components/Cell/GridEditInputCell.tsx +114 -41
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
- package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
- package/lib/components/DataGrid/DataGrid.css +84 -12
- package/lib/components/DataGrid/DataGrid.tsx +606 -872
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +166 -23
- package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
- package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
- package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
- package/lib/components/DetailPanel/DetailPanel.css +0 -6
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
- package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
- package/lib/components/FilterPanel/FilterPanel.css +13 -2
- package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
- package/lib/components/Header/ColumnMenu.tsx +91 -39
- package/lib/components/Header/Header.css +3 -3
- package/lib/components/Header/Header.tsx +179 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +79 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +185 -75
- package/lib/components/Toolbar/GlobalSearch.css +6 -3
- package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
- package/lib/components/Toolbar/GridToolbar.css +12 -11
- package/lib/components/Toolbar/GridToolbar.tsx +133 -89
- package/lib/components/Toolbar/PivotPanel.tsx +89 -49
- package/lib/components/Tooltip/Tooltip.css +20 -0
- package/lib/components/Tooltip/Tooltip.tsx +68 -35
- package/lib/components/ui/Button.tsx +3 -0
- package/lib/components/ui/Checkbox.tsx +2 -1
- package/lib/components/ui/ExpandIcon.css +0 -9
- package/lib/components/ui/ExpandIcon.tsx +4 -1
- package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
- package/lib/hooks/core/useDataGrid.ts +115 -173
- package/lib/hooks/core/useGridAggregationApi.ts +72 -0
- package/lib/hooks/core/useGridApiMethods.ts +115 -0
- package/lib/hooks/core/useGridApiRef.ts +15 -3
- package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
- package/lib/hooks/core/useGridAriaRows.ts +38 -0
- package/lib/hooks/core/useGridClipboardApi.ts +38 -0
- package/lib/hooks/core/useGridColumnLookup.ts +30 -0
- package/lib/hooks/core/useGridColumns.tsx +284 -76
- package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
- package/lib/hooks/core/useGridControlledState.ts +99 -6
- package/lib/hooks/core/useGridDetailPanel.ts +51 -0
- package/lib/hooks/core/useGridDevWarnings.ts +123 -0
- package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
- package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
- package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
- package/lib/hooks/core/useGridHierarchy.ts +81 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
- package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
- package/lib/hooks/core/useGridPageCorrection.ts +55 -0
- package/lib/hooks/core/useGridRowIdOf.ts +32 -0
- package/lib/hooks/core/useGridRowInteractions.ts +81 -0
- package/lib/hooks/core/useGridRowPipeline.ts +69 -39
- package/lib/hooks/core/useGridRowSelection.ts +91 -0
- package/lib/hooks/core/useGridScrollSync.ts +80 -32
- package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
- package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
- package/lib/hooks/core/useGridToolbarProps.ts +97 -0
- package/lib/hooks/core/useGridViewport.ts +38 -0
- package/lib/hooks/core/useGridViewportSize.ts +26 -0
- package/lib/hooks/core/useGridVisibleRows.ts +20 -7
- package/lib/hooks/core/useLayout.ts +215 -195
- package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
- package/lib/hooks/core/useStableColumns.ts +34 -0
- package/lib/hooks/features/useAggregation.ts +86 -90
- package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
- package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -73
- package/lib/hooks/features/useGridDataSource.ts +354 -128
- package/lib/hooks/features/useGridEditing.ts +211 -51
- package/lib/hooks/features/useGridPivot.ts +92 -0
- package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
- package/lib/hooks/features/useGridSpanning.ts +410 -306
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/features/useServerTreeChildren.ts +50 -0
- package/lib/hooks/useColumnReorder.ts +38 -44
- package/lib/hooks/useRowGrouping.ts +335 -214
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +330 -262
- package/lib/index.ts +30 -2
- package/lib/state/index.ts +1 -0
- package/lib/state/types.ts +7 -1
- package/lib/state/useGridStateStorage.ts +88 -29
- package/lib/theme/DataGridThemeProvider.tsx +143 -38
- package/lib/theme/gridThemeContext.ts +5 -0
- package/lib/theme/index.ts +5 -0
- package/lib/theme/presets.ts +5 -4
- package/lib/theme/types.ts +40 -2
- package/lib/types/index.ts +311 -108
- package/lib/utils/aggregation/index.ts +157 -0
- package/lib/utils/aria/index.ts +40 -0
- package/lib/utils/columnGroups.ts +110 -0
- package/lib/utils/columnLookup.ts +33 -0
- package/lib/utils/columnWidth.ts +22 -0
- package/lib/utils/dataSource/index.ts +46 -0
- package/lib/utils/detailPanel.ts +66 -0
- package/lib/utils/dragData.ts +44 -0
- package/lib/utils/editing/index.ts +30 -0
- package/lib/utils/export/exportShared.ts +197 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
- package/lib/utils/export/exportToPdf.ts +143 -92
- package/lib/utils/export/groupLabel.ts +14 -0
- package/lib/utils/export/index.ts +278 -272
- package/lib/utils/export/pdfText.ts +50 -0
- package/lib/utils/filtering/index.ts +277 -148
- package/lib/utils/focus/index.ts +115 -0
- package/lib/utils/gridApiRows.ts +35 -0
- package/lib/utils/gridModes.ts +77 -0
- package/lib/utils/grouping/groupedExportRows.ts +91 -0
- package/lib/utils/pagination/index.ts +56 -0
- package/lib/utils/pinning/index.ts +77 -33
- package/lib/utils/pivot/index.ts +368 -0
- package/lib/utils/scroll/index.ts +78 -0
- package/lib/utils/scroll/scrollEnd.ts +18 -0
- package/lib/utils/sorting/index.ts +154 -30
- package/lib/utils/syntheticRows.ts +12 -0
- package/lib/utils/values.ts +206 -0
- package/lib/utils/viewport.ts +14 -0
- package/llms.txt +272 -166
- package/package.json +18 -6
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
- package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
- package/lib/components/Header/Header.test.tsx +0 -135
- package/lib/components/Pagination/Pagination.test.tsx +0 -64
- package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
- package/lib/hooks/core/useGridControlledState.test.ts +0 -86
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
- package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
- package/lib/hooks/features/useAggregation.test.ts +0 -175
- package/lib/hooks/features/usePivot.test.ts +0 -202
- package/lib/hooks/useRowGrouping.test.ts +0 -137
- package/lib/utils/export/exportToPdf.test.ts +0 -120
- package/lib/utils/export/groupedExport.test.ts +0 -144
- package/lib/utils/filtering/filtering.test.ts +0 -178
- package/lib/utils/sorting/sorting.test.ts +0 -88
|
@@ -1,44 +1,62 @@
|
|
|
1
1
|
import { useRef, useState, useCallback, useEffect } from 'react';
|
|
2
2
|
import type { GridRowScrollEndParams } from '../../types';
|
|
3
|
+
import { getRowsScrollEndParams } from '../../utils/scroll/scrollEnd';
|
|
3
4
|
|
|
4
5
|
export interface UseGridScrollSyncParams {
|
|
5
6
|
onRowsScrollEnd?: (params: GridRowScrollEndParams) => void;
|
|
6
7
|
/** Minimum overscan rows — the adaptive algorithm never goes below this. Defaults to 3. */
|
|
7
8
|
overscanRowCount?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Number of scrolling (center) rows. A change re-arms `onRowsScrollEnd` and re-checks whether
|
|
11
|
+
* the end of the rows is in view (e.g. the first page does not fill the viewport).
|
|
12
|
+
*/
|
|
13
|
+
rowCount?: number;
|
|
14
|
+
/** The viewport grows to its content and never scrolls itself: only scroll events can signal the end. */
|
|
15
|
+
autoHeight?: boolean;
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
export interface UseGridScrollSyncResult {
|
|
11
19
|
scrollTop: number;
|
|
12
20
|
scrollLeft: number;
|
|
13
|
-
/**
|
|
21
|
+
/** Overscan row count: the velocity-based value, never below `overscanRowCount`. */
|
|
14
22
|
overscanRows: number;
|
|
15
23
|
handleScroll: (event: React.UIEvent<HTMLDivElement>) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Callback ref for the viewport element. When the viewport remounts (list view switched off
|
|
26
|
+
* again), the last scroll position is restored onto the new element so the render window and
|
|
27
|
+
* the DOM agree.
|
|
28
|
+
*/
|
|
29
|
+
attachViewport: (element: HTMLDivElement | null) => void;
|
|
16
30
|
}
|
|
17
31
|
|
|
18
32
|
/** Maps scroll velocity (px/ms) to an overscan row count. */
|
|
19
|
-
function velocityToOverscan(velocity: number
|
|
20
|
-
|
|
21
|
-
if (velocity <
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
else base = 30;
|
|
26
|
-
return Math.max(base, minOverscan);
|
|
33
|
+
function velocityToOverscan(velocity: number): number {
|
|
34
|
+
if (velocity < 0.5) return 3;
|
|
35
|
+
if (velocity < 3) return 5;
|
|
36
|
+
if (velocity < 15) return 12;
|
|
37
|
+
if (velocity < 40) return 20;
|
|
38
|
+
return 30;
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
export function useGridScrollSync(params: UseGridScrollSyncParams): UseGridScrollSyncResult {
|
|
30
|
-
const { onRowsScrollEnd, overscanRowCount = 3 } = params;
|
|
42
|
+
const { onRowsScrollEnd, overscanRowCount = 3, rowCount = 0, autoHeight = false } = params;
|
|
31
43
|
|
|
32
44
|
const pendingRef = useRef({ scrollTop: 0, scrollLeft: 0, velocity: 0 });
|
|
33
45
|
const prevRef = useRef({ scrollTop: 0, scrollLeft: 0, time: 0 });
|
|
34
46
|
const rafRef = useRef<number | null>(null);
|
|
35
47
|
const decayRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
48
|
+
const elementRef = useRef<HTMLDivElement | null>(null);
|
|
49
|
+
/** Cleared when onRowsScrollEnd fires; set again once the viewport leaves the threshold or the rows change. */
|
|
50
|
+
const armedRef = useRef(true);
|
|
51
|
+
const onRowsScrollEndRef = useRef(onRowsScrollEnd);
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
// velocityOverscan is 0 at rest; the prop floor is applied during render, so a new
|
|
54
|
+
// overscanRowCount takes effect without waiting for a scroll.
|
|
55
|
+
const [scrollState, setScrollState] = useState({ scrollTop: 0, scrollLeft: 0, velocityOverscan: 0 });
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
onRowsScrollEndRef.current = onRowsScrollEnd;
|
|
59
|
+
}, [onRowsScrollEnd]);
|
|
42
60
|
|
|
43
61
|
useEffect(() => {
|
|
44
62
|
return () => {
|
|
@@ -47,6 +65,20 @@ export function useGridScrollSync(params: UseGridScrollSyncParams): UseGridScrol
|
|
|
47
65
|
};
|
|
48
66
|
}, []);
|
|
49
67
|
|
|
68
|
+
/** Fires onRowsScrollEnd once per arrival within the threshold of the end. */
|
|
69
|
+
const checkRowsScrollEnd = useCallback((target: HTMLElement) => {
|
|
70
|
+
const endParams = getRowsScrollEndParams(target);
|
|
71
|
+
if (!endParams) {
|
|
72
|
+
armedRef.current = true;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!armedRef.current) return;
|
|
76
|
+
const callback = onRowsScrollEndRef.current;
|
|
77
|
+
if (!callback) return;
|
|
78
|
+
armedRef.current = false;
|
|
79
|
+
callback(endParams);
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
50
82
|
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
|
|
51
83
|
const target = event.currentTarget;
|
|
52
84
|
const now = performance.now();
|
|
@@ -56,6 +88,7 @@ export function useGridScrollSync(params: UseGridScrollSyncParams): UseGridScrol
|
|
|
56
88
|
+ Math.abs(target.scrollLeft - prevRef.current.scrollLeft);
|
|
57
89
|
const deltaTime = now - prevRef.current.time;
|
|
58
90
|
const velocity = deltaTime > 0 ? deltaPos / deltaTime : 0;
|
|
91
|
+
const movedVertically = target.scrollTop !== prevRef.current.scrollTop;
|
|
59
92
|
|
|
60
93
|
prevRef.current = { scrollTop: target.scrollTop, scrollLeft: target.scrollLeft, time: now };
|
|
61
94
|
pendingRef.current = { scrollTop: target.scrollTop, scrollLeft: target.scrollLeft, velocity };
|
|
@@ -64,36 +97,51 @@ export function useGridScrollSync(params: UseGridScrollSyncParams): UseGridScrol
|
|
|
64
97
|
rafRef.current = requestAnimationFrame(() => {
|
|
65
98
|
rafRef.current = null;
|
|
66
99
|
const { scrollTop, scrollLeft, velocity: v } = pendingRef.current;
|
|
67
|
-
setScrollState({
|
|
68
|
-
scrollTop,
|
|
69
|
-
scrollLeft,
|
|
70
|
-
overscanRows: velocityToOverscan(v, overscanRowCount),
|
|
71
|
-
});
|
|
100
|
+
setScrollState({ scrollTop, scrollLeft, velocityOverscan: velocityToOverscan(v) });
|
|
72
101
|
});
|
|
73
102
|
|
|
74
103
|
// Decay back to the minimum overscan 200ms after scrolling stops
|
|
75
104
|
if (decayRef.current !== null) clearTimeout(decayRef.current);
|
|
76
105
|
decayRef.current = setTimeout(() => {
|
|
77
106
|
decayRef.current = null;
|
|
78
|
-
setScrollState(prev => ({ ...prev,
|
|
107
|
+
setScrollState(prev => (prev.velocityOverscan === 0 ? prev : { ...prev, velocityOverscan: 0 }));
|
|
79
108
|
}, 200);
|
|
80
109
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
// Horizontal-only scrolling never reaches (or leaves) the end of the rows.
|
|
111
|
+
if (movedVertically) checkRowsScrollEnd(target);
|
|
112
|
+
}, [checkRowsScrollEnd]);
|
|
113
|
+
|
|
114
|
+
// New rows re-arm the callback; if the end is still in view (the rows do not fill the
|
|
115
|
+
// viewport), no scroll event can happen, so check here.
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
armedRef.current = true;
|
|
118
|
+
const el = elementRef.current;
|
|
119
|
+
if (!el || autoHeight || rowCount <= 0 || el.clientHeight === 0) return;
|
|
120
|
+
checkRowsScrollEnd(el);
|
|
121
|
+
}, [rowCount, autoHeight, checkRowsScrollEnd]);
|
|
122
|
+
|
|
123
|
+
const attachViewport = useCallback((element: HTMLDivElement | null) => {
|
|
124
|
+
elementRef.current = element;
|
|
125
|
+
if (!element) return;
|
|
126
|
+
const { scrollTop, scrollLeft } = pendingRef.current;
|
|
127
|
+
if (scrollTop === 0 && scrollLeft === 0) return;
|
|
128
|
+
element.scrollTop = scrollTop;
|
|
129
|
+
element.scrollLeft = scrollLeft;
|
|
130
|
+
// The browser clamps to the new content. If it did, sync the state to what the element
|
|
131
|
+
// shows (no scroll event fires when the position ends up unchanged).
|
|
132
|
+
const actual = { scrollTop: element.scrollTop, scrollLeft: element.scrollLeft };
|
|
133
|
+
if (actual.scrollTop !== scrollTop || actual.scrollLeft !== scrollLeft) {
|
|
134
|
+
pendingRef.current = { ...pendingRef.current, ...actual };
|
|
135
|
+
prevRef.current = { ...prevRef.current, ...actual };
|
|
136
|
+
setScrollState(prev => ({ ...prev, ...actual }));
|
|
90
137
|
}
|
|
91
|
-
}, [
|
|
138
|
+
}, []);
|
|
92
139
|
|
|
93
140
|
return {
|
|
94
141
|
scrollTop: scrollState.scrollTop,
|
|
95
142
|
scrollLeft: scrollState.scrollLeft,
|
|
96
|
-
overscanRows: scrollState.
|
|
143
|
+
overscanRows: Math.max(scrollState.velocityOverscan, overscanRowCount),
|
|
97
144
|
handleScroll,
|
|
145
|
+
attachViewport,
|
|
98
146
|
};
|
|
99
147
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import type { MutableRefObject, RefObject } from 'react';
|
|
3
|
+
import { scrollRowIntoView, scrollColumnIntoView } from '../../utils/scroll';
|
|
4
|
+
import type { LayoutResult } from './useLayout';
|
|
5
|
+
import type { GridApi, GridRowModel } from '../../types';
|
|
6
|
+
|
|
7
|
+
export interface UseGridScrollToIndexesApiParams<R extends GridRowModel> {
|
|
8
|
+
apiRef: MutableRefObject<GridApi>;
|
|
9
|
+
viewportRef: RefObject<HTMLDivElement | null>;
|
|
10
|
+
layout: LayoutResult<R>;
|
|
11
|
+
rowHeight: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Installs `scrollToIndexes` on the API, in a layout effect reading the latest layout from a ref,
|
|
16
|
+
* so it is live in a parent's layout effect and never scrolls by a previous render's layout.
|
|
17
|
+
*/
|
|
18
|
+
export function useGridScrollToIndexesApi<R extends GridRowModel>(params: UseGridScrollToIndexesApiParams<R>): void {
|
|
19
|
+
const { apiRef } = params;
|
|
20
|
+
const latestRef = useRef(params);
|
|
21
|
+
useLayoutEffect(() => {
|
|
22
|
+
latestRef.current = params;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
useLayoutEffect(() => {
|
|
26
|
+
apiRef.current.scrollToIndexes = ({ rowIndex, colIndex }) => {
|
|
27
|
+
const { viewportRef, layout, rowHeight } = latestRef.current;
|
|
28
|
+
const el = viewportRef.current;
|
|
29
|
+
if (!el) return;
|
|
30
|
+
|
|
31
|
+
if (rowIndex !== undefined && rowIndex >= 0) {
|
|
32
|
+
scrollRowIntoView(el, rowIndex, layout.cumulativeHeights, layout.pinnedBottomHeight, rowHeight);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (colIndex !== undefined && colIndex >= 0) {
|
|
36
|
+
// colIndex is an index into all data columns: leftPinned + unpinned + rightPinned
|
|
37
|
+
const allDataCols = [...layout.leftPinnedCols, ...layout.unpinnedColsWithWidth, ...layout.rightPinnedCols];
|
|
38
|
+
const targetCol = allDataCols[colIndex];
|
|
39
|
+
if (!targetCol) return;
|
|
40
|
+
// A pinned column (index -1) is always visible: nothing to scroll.
|
|
41
|
+
scrollColumnIntoView(el, layout.unpinnedColsWithWidth.findIndex(c => c.field === targetCol.field), layout);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}, [apiRef]);
|
|
45
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useRef, useEffect, useLayoutEffect } from 'react';
|
|
2
|
-
import type { GridState } from '../../state/types';
|
|
2
|
+
import type { GridState, GridDensityState } from '../../state/types';
|
|
3
3
|
import type {
|
|
4
4
|
GridSortItem,
|
|
5
5
|
GridFilterModel,
|
|
@@ -7,6 +7,23 @@ import type {
|
|
|
7
7
|
GridColumnPinning,
|
|
8
8
|
} from '../../types';
|
|
9
9
|
|
|
10
|
+
/** Grid-owned columns that must never leak into the public state (and so into storage). */
|
|
11
|
+
const SYNTHETIC_FIELDS: ReadonlySet<string> = new Set(['__group__']);
|
|
12
|
+
const isUserField = (field: string) => !SYNTHETIC_FIELDS.has(field);
|
|
13
|
+
|
|
14
|
+
function withoutSyntheticKeys<T>(record: Record<string, T>): Record<string, T> {
|
|
15
|
+
const keys = Object.keys(record);
|
|
16
|
+
if (keys.every(isUserField)) return record;
|
|
17
|
+
return Object.fromEntries(Object.entries(record).filter(([field]) => isUserField(field)));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function userPinnedColumns(pinned: GridColumnPinning): GridColumnPinning {
|
|
21
|
+
const result: GridColumnPinning = { ...pinned };
|
|
22
|
+
if (pinned.left) result.left = pinned.left.filter(isUserField);
|
|
23
|
+
if (pinned.right) result.right = pinned.right.filter(isUserField);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
export interface UseGridStateSnapshotParams {
|
|
11
28
|
onStateChange?: (state: GridState) => void;
|
|
12
29
|
sortModel: GridSortItem[];
|
|
@@ -15,9 +32,17 @@ export interface UseGridStateSnapshotParams {
|
|
|
15
32
|
columnWidths: Record<string, number>;
|
|
16
33
|
effectiveColumnOrder: string[];
|
|
17
34
|
columnVisibilityModel: Record<string, boolean>;
|
|
35
|
+
/** The user's pins (not the grid's effective pins, which add the synthetic group column). */
|
|
18
36
|
pinnedColumns: GridColumnPinning;
|
|
37
|
+
density: GridDensityState['density'];
|
|
19
38
|
}
|
|
20
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Emits the public GridState through onStateChange whenever its *value* changes.
|
|
42
|
+
* Inline model props get a new identity on every parent render, so the comparison is
|
|
43
|
+
* structural; otherwise an onStateChange that stores state in the parent re-renders
|
|
44
|
+
* the parent, which creates new inline props, which fires onStateChange again, forever.
|
|
45
|
+
*/
|
|
21
46
|
export function useGridStateSnapshot(params: UseGridStateSnapshotParams): void {
|
|
22
47
|
const {
|
|
23
48
|
onStateChange,
|
|
@@ -28,6 +53,7 @@ export function useGridStateSnapshot(params: UseGridStateSnapshotParams): void {
|
|
|
28
53
|
effectiveColumnOrder,
|
|
29
54
|
columnVisibilityModel,
|
|
30
55
|
pinnedColumns,
|
|
56
|
+
density,
|
|
31
57
|
} = params;
|
|
32
58
|
|
|
33
59
|
const onStateChangeRef = useRef(onStateChange);
|
|
@@ -35,6 +61,8 @@ export function useGridStateSnapshot(params: UseGridStateSnapshotParams): void {
|
|
|
35
61
|
onStateChangeRef.current = onStateChange;
|
|
36
62
|
});
|
|
37
63
|
|
|
64
|
+
const lastEmittedRef = useRef<string | null>(null);
|
|
65
|
+
|
|
38
66
|
useEffect(() => {
|
|
39
67
|
if (!onStateChangeRef.current) return;
|
|
40
68
|
|
|
@@ -43,13 +71,19 @@ export function useGridStateSnapshot(params: UseGridStateSnapshotParams): void {
|
|
|
43
71
|
filter: { filterModel },
|
|
44
72
|
pagination: { paginationModel: effectivePaginationModel },
|
|
45
73
|
columns: {
|
|
46
|
-
columnWidths,
|
|
47
|
-
columnOrder: effectiveColumnOrder,
|
|
48
|
-
columnVisibilityModel,
|
|
49
|
-
pinnedColumns,
|
|
74
|
+
columnWidths: withoutSyntheticKeys(columnWidths),
|
|
75
|
+
columnOrder: effectiveColumnOrder.filter(isUserField),
|
|
76
|
+
columnVisibilityModel: withoutSyntheticKeys(columnVisibilityModel),
|
|
77
|
+
pinnedColumns: userPinnedColumns(pinnedColumns),
|
|
50
78
|
},
|
|
79
|
+
density: { density },
|
|
51
80
|
};
|
|
52
81
|
|
|
82
|
+
// The snapshot only holds plain data, so its JSON is a faithful value key.
|
|
83
|
+
const serialized = JSON.stringify(snapshot);
|
|
84
|
+
if (serialized === lastEmittedRef.current) return;
|
|
85
|
+
lastEmittedRef.current = serialized;
|
|
86
|
+
|
|
53
87
|
onStateChangeRef.current(snapshot);
|
|
54
88
|
}, [
|
|
55
89
|
sortModel,
|
|
@@ -59,5 +93,6 @@ export function useGridStateSnapshot(params: UseGridStateSnapshotParams): void {
|
|
|
59
93
|
effectiveColumnOrder,
|
|
60
94
|
columnVisibilityModel,
|
|
61
95
|
pinnedColumns,
|
|
96
|
+
density,
|
|
62
97
|
]);
|
|
63
98
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { GridThemeContext } from '../../theme/gridThemeContext';
|
|
3
|
+
import type { GridDensity } from './useGridControlledState';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_ROW_HEIGHT = 52;
|
|
6
|
+
const DEFAULT_HEADER_HEIGHT = 56;
|
|
7
|
+
const COMPACT_ROW_HEIGHT = 32;
|
|
8
|
+
const COMFORTABLE_ROW_HEIGHT = 72;
|
|
9
|
+
|
|
10
|
+
/** A theme height in pixels (`'36px'` or `'36'`); anything else (rem, calc, …) is ignored. */
|
|
11
|
+
export function parseThemePixels(value: string | undefined): number | undefined {
|
|
12
|
+
if (value === undefined) return undefined;
|
|
13
|
+
const match = /^\s*(\d+(?:\.\d+)?)\s*(px)?\s*$/.exec(value);
|
|
14
|
+
if (!match) return undefined;
|
|
15
|
+
const px = Number(match[1]);
|
|
16
|
+
return px > 0 ? px : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UseGridThemeDimensionsParams {
|
|
20
|
+
rowHeight?: number;
|
|
21
|
+
headerHeight?: number;
|
|
22
|
+
density: GridDensity;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The row and header heights the grid lays out and renders with. The grid drives virtualization
|
|
27
|
+
* from these numbers, so a DataGridThemeProvider's `grid.rowHeight*` / `grid.headerHeight` are read
|
|
28
|
+
* here rather than left to CSS (which the grid's own inline heights would override). Precedence:
|
|
29
|
+
* the `rowHeight` / `headerHeight` props, then the theme, then the defaults. `density="compact"` /
|
|
30
|
+
* `"comfortable"` use the theme's compact / comfortable height, else 32 / 72.
|
|
31
|
+
*/
|
|
32
|
+
export function useGridThemeDimensions(params: UseGridThemeDimensionsParams): { rowHeight: number; headerHeight: number } {
|
|
33
|
+
const { rowHeight, headerHeight, density } = params;
|
|
34
|
+
const grid = useContext(GridThemeContext)?.grid;
|
|
35
|
+
|
|
36
|
+
let effectiveRowHeight: number;
|
|
37
|
+
if (density === 'compact') {
|
|
38
|
+
effectiveRowHeight = parseThemePixels(grid?.rowHeightCompact) ?? COMPACT_ROW_HEIGHT;
|
|
39
|
+
} else if (density === 'comfortable') {
|
|
40
|
+
effectiveRowHeight = parseThemePixels(grid?.rowHeightComfortable) ?? COMFORTABLE_ROW_HEIGHT;
|
|
41
|
+
} else {
|
|
42
|
+
effectiveRowHeight = rowHeight ?? parseThemePixels(grid?.rowHeightStandard) ?? DEFAULT_ROW_HEIGHT;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
rowHeight: effectiveRowHeight,
|
|
47
|
+
headerHeight: headerHeight ?? parseThemePixels(grid?.headerHeight) ?? DEFAULT_HEADER_HEIGHT,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { MutableRefObject } from 'react';
|
|
3
|
+
import { canReorderWithinColumnGroups } from '../../utils/columnGroups';
|
|
4
|
+
import type {
|
|
5
|
+
DataGridProps,
|
|
6
|
+
GridAggregationModel,
|
|
7
|
+
GridApi,
|
|
8
|
+
GridColDef,
|
|
9
|
+
GridColumnGroupingModel,
|
|
10
|
+
GridColumnVisibilityModel,
|
|
11
|
+
GridFilterModel,
|
|
12
|
+
GridPivotModel,
|
|
13
|
+
GridRowModel,
|
|
14
|
+
GridToolbarSlotProps,
|
|
15
|
+
} from '../../types';
|
|
16
|
+
|
|
17
|
+
type ToolbarSlot = NonNullable<DataGridProps['slots']>['toolbar'];
|
|
18
|
+
type ToolbarSlotProps = NonNullable<DataGridProps['slotProps']>['toolbar'];
|
|
19
|
+
|
|
20
|
+
export interface UseGridToolbarPropsParams<R extends GridRowModel> {
|
|
21
|
+
/** `slots.toolbar`: nothing is built without one. */
|
|
22
|
+
toolbar: ToolbarSlot;
|
|
23
|
+
/** `slotProps.toolbar`, merged over the grid's props. */
|
|
24
|
+
toolbarSlotProps: ToolbarSlotProps;
|
|
25
|
+
apiRef: MutableRefObject<GridApi>;
|
|
26
|
+
/** Columns in display order (`useGridColumns().orderedColumns`). */
|
|
27
|
+
orderedColumns: GridColDef<R>[];
|
|
28
|
+
/** The `columns` prop. */
|
|
29
|
+
baseColumns: GridColDef<R>[];
|
|
30
|
+
aggregationModel: GridAggregationModel;
|
|
31
|
+
onAggregationModelChange: (model: GridAggregationModel) => void;
|
|
32
|
+
/** The toolbar gets the pivot model only when the consumer uses the pivot (`pivotMode`, `pivotModel` or `onPivotModelChange`). */
|
|
33
|
+
pivotMode: boolean;
|
|
34
|
+
pivotModelProp: GridPivotModel | undefined;
|
|
35
|
+
onPivotModelChangeProp: ((model: GridPivotModel) => void) | undefined;
|
|
36
|
+
pivotModel: GridPivotModel;
|
|
37
|
+
onPivotModelChange: (model: GridPivotModel) => void;
|
|
38
|
+
filterModel: GridFilterModel;
|
|
39
|
+
onFilterModelChange: (model: GridFilterModel) => void;
|
|
40
|
+
columnVisibilityModel: GridColumnVisibilityModel;
|
|
41
|
+
onColumnVisibilityModelChange: (model: GridColumnVisibilityModel) => void;
|
|
42
|
+
disableColumnReorder: boolean;
|
|
43
|
+
columnGroupingModel: GridColumnGroupingModel | undefined;
|
|
44
|
+
moveColumn: (fromField: string, toField: string) => void;
|
|
45
|
+
resetColumnOrder: () => void;
|
|
46
|
+
/** `useGridColumnsPanel().toolbarPanelRequested`. */
|
|
47
|
+
forceColumnsOpen: boolean;
|
|
48
|
+
onColumnsPanelClose: () => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The props the grid passes to `slots.toolbar`, or null when there is no toolbar slot. */
|
|
52
|
+
export function useGridToolbarProps<R extends GridRowModel>(
|
|
53
|
+
params: UseGridToolbarPropsParams<R>
|
|
54
|
+
): (GridToolbarSlotProps & Record<string, unknown>) | null {
|
|
55
|
+
const {
|
|
56
|
+
toolbar, toolbarSlotProps, apiRef, orderedColumns, baseColumns, aggregationModel, onAggregationModelChange,
|
|
57
|
+
pivotMode, pivotModelProp, onPivotModelChangeProp, pivotModel, onPivotModelChange, filterModel,
|
|
58
|
+
onFilterModelChange, columnVisibilityModel, onColumnVisibilityModelChange, disableColumnReorder,
|
|
59
|
+
columnGroupingModel, moveColumn, resetColumnOrder, forceColumnsOpen, onColumnsPanelClose,
|
|
60
|
+
} = params;
|
|
61
|
+
|
|
62
|
+
return useMemo(() => {
|
|
63
|
+
if (!toolbar) return null;
|
|
64
|
+
const reorderHandler = disableColumnReorder
|
|
65
|
+
? undefined
|
|
66
|
+
: (fromField: string, toField: string) => {
|
|
67
|
+
if (!canReorderWithinColumnGroups(columnGroupingModel, fromField, toField)) return;
|
|
68
|
+
moveColumn(fromField, toField);
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
apiRef,
|
|
72
|
+
columns: orderedColumns as unknown as GridColDef[],
|
|
73
|
+
baseColumns: baseColumns as unknown as GridColDef[],
|
|
74
|
+
aggregationModel,
|
|
75
|
+
onAggregationModelChange,
|
|
76
|
+
...(pivotMode || pivotModelProp || onPivotModelChangeProp ? {
|
|
77
|
+
pivotModel,
|
|
78
|
+
onPivotModelChange,
|
|
79
|
+
} : {}),
|
|
80
|
+
filterModel,
|
|
81
|
+
onFilterModelChange,
|
|
82
|
+
columnVisibilityModel,
|
|
83
|
+
onColumnVisibilityModelChange,
|
|
84
|
+
onColumnReorder: reorderHandler,
|
|
85
|
+
onColumnOrderReset: disableColumnReorder ? undefined : resetColumnOrder,
|
|
86
|
+
forceColumnsOpen,
|
|
87
|
+
onColumnsPanelClose,
|
|
88
|
+
...toolbarSlotProps,
|
|
89
|
+
};
|
|
90
|
+
}, [
|
|
91
|
+
toolbar, disableColumnReorder, moveColumn, resetColumnOrder, orderedColumns, apiRef,
|
|
92
|
+
baseColumns, aggregationModel, onAggregationModelChange, pivotMode,
|
|
93
|
+
pivotModelProp, onPivotModelChangeProp, pivotModel, onPivotModelChange,
|
|
94
|
+
filterModel, onFilterModelChange, columnVisibilityModel,
|
|
95
|
+
onColumnVisibilityModelChange, forceColumnsOpen, onColumnsPanelClose, toolbarSlotProps, columnGroupingModel,
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import type { MutableRefObject } from 'react';
|
|
3
|
+
import { useGridScrollSync, type UseGridScrollSyncParams } from './useGridScrollSync';
|
|
4
|
+
import { useGridViewportSize } from './useGridViewportSize';
|
|
5
|
+
|
|
6
|
+
export interface UseGridViewportParams extends UseGridScrollSyncParams {
|
|
7
|
+
/** Receives the viewport's measured content size. */
|
|
8
|
+
setDimensions: (width: number, height: number) => void;
|
|
9
|
+
/** Set to the mounted viewport element. */
|
|
10
|
+
viewportRef: MutableRefObject<HTMLDivElement | null>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseGridViewportResult {
|
|
14
|
+
scrollTop: number;
|
|
15
|
+
scrollLeft: number;
|
|
16
|
+
overscanRows: number;
|
|
17
|
+
handleScroll: (event: React.UIEvent<HTMLDivElement>) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Callback ref for the viewport: every viewport that mounts (list view switched off again)
|
|
20
|
+
* is measured and gets its scroll position back.
|
|
21
|
+
*/
|
|
22
|
+
setViewportElement: (el: HTMLDivElement | null) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The grid viewport: scroll position and velocity overscan, size measurement, and its callback ref. */
|
|
26
|
+
export function useGridViewport(params: UseGridViewportParams): UseGridViewportResult {
|
|
27
|
+
const { setDimensions, viewportRef, ...scrollParams } = params;
|
|
28
|
+
|
|
29
|
+
const { scrollTop, scrollLeft, overscanRows, handleScroll, attachViewport } = useGridScrollSync(scrollParams);
|
|
30
|
+
const observeViewportSize = useGridViewportSize(setDimensions);
|
|
31
|
+
const setViewportElement = useCallback((el: HTMLDivElement | null) => {
|
|
32
|
+
viewportRef.current = el;
|
|
33
|
+
observeViewportSize(el);
|
|
34
|
+
attachViewport(el);
|
|
35
|
+
}, [viewportRef, observeViewportSize, attachViewport]);
|
|
36
|
+
|
|
37
|
+
return { scrollTop, scrollLeft, overscanRows, handleScroll, setViewportElement };
|
|
38
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns a callback ref that measures the element it is attached to with a ResizeObserver and
|
|
5
|
+
* reports its content size. Every element that mounts is observed (the grid viewport unmounts in
|
|
6
|
+
* list view and a new one mounts when list view is switched off); a detached element is released.
|
|
7
|
+
*/
|
|
8
|
+
export function useGridViewportSize(
|
|
9
|
+
setDimensions: (width: number, height: number) => void,
|
|
10
|
+
): (element: HTMLElement | null) => void {
|
|
11
|
+
const observerRef = useRef<ResizeObserver | null>(null);
|
|
12
|
+
|
|
13
|
+
return useCallback((element: HTMLElement | null) => {
|
|
14
|
+
observerRef.current?.disconnect();
|
|
15
|
+
observerRef.current = null;
|
|
16
|
+
if (!element || typeof ResizeObserver === 'undefined') return;
|
|
17
|
+
const observer = new ResizeObserver((entries) => {
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const { width, height } = entry.contentRect;
|
|
20
|
+
setDimensions(width, height);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
observer.observe(element);
|
|
24
|
+
observerRef.current = observer;
|
|
25
|
+
}, [setDimensions]);
|
|
26
|
+
}
|
|
@@ -13,13 +13,21 @@ export interface UseGridVisibleRowsParams<R extends GridRowModel> {
|
|
|
13
13
|
paginatedUnpinnedRows: R[];
|
|
14
14
|
sortedUnpinnedRows: R[];
|
|
15
15
|
pagination: boolean;
|
|
16
|
+
/** Resolves a row's id (rows are not guaranteed to carry it in `row.id`); defaults to `row.id`. */
|
|
17
|
+
getRowId?: (row: R) => GridRowId;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface GridVisibleRow<R extends GridRowModel> {
|
|
19
21
|
row: R;
|
|
22
|
+
/** The row's id (getRowId), resolved once for everything that renders the row. */
|
|
23
|
+
id: GridRowId;
|
|
20
24
|
rowIndex: number;
|
|
25
|
+
/** Set on the rows of the top / bottom pinned sections, which render outside the scrolling rows. */
|
|
26
|
+
pinned?: 'top' | 'bottom';
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
const defaultGetRowId = <R extends GridRowModel>(row: R): GridRowId => row.id;
|
|
30
|
+
|
|
23
31
|
export function useGridVisibleRows<R extends GridRowModel>(
|
|
24
32
|
params: UseGridVisibleRowsParams<R>
|
|
25
33
|
): GridVisibleRow<R>[] {
|
|
@@ -30,6 +38,7 @@ export function useGridVisibleRows<R extends GridRowModel>(
|
|
|
30
38
|
paginatedUnpinnedRows,
|
|
31
39
|
sortedUnpinnedRows,
|
|
32
40
|
pagination,
|
|
41
|
+
getRowId = defaultGetRowId,
|
|
33
42
|
} = params;
|
|
34
43
|
|
|
35
44
|
return useMemo<GridVisibleRow<R>[]>(() => {
|
|
@@ -37,30 +46,34 @@ export function useGridVisibleRows<R extends GridRowModel>(
|
|
|
37
46
|
const centerRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
|
|
38
47
|
const topPinnedCount = pinnedTopRows.length;
|
|
39
48
|
|
|
40
|
-
const topPinned = pinnedTopRows.map((row, index) => ({ row, rowIndex: index }));
|
|
49
|
+
const topPinned = pinnedTopRows.map((row, index) => ({ row, id: getRowId(row), rowIndex: index, pinned: 'top' as const }));
|
|
41
50
|
const bottomPinned = pinnedBottomRows.map((row, index) => ({
|
|
42
51
|
row,
|
|
52
|
+
id: getRowId(row),
|
|
43
53
|
rowIndex: topPinnedCount + centerRows.length + index,
|
|
54
|
+
pinned: 'bottom' as const,
|
|
44
55
|
}));
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
const
|
|
57
|
+
// renderContext indices come from the unpinned-row layout, so they index centerRows directly.
|
|
58
|
+
const centerStartIndex = Math.max(0, firstRowIndex);
|
|
59
|
+
const centerEndIndex = Math.min(centerRows.length, lastRowIndex + 1);
|
|
48
60
|
|
|
49
61
|
const centerVisible = centerRows
|
|
50
62
|
.slice(centerStartIndex, centerEndIndex)
|
|
51
63
|
.map((row, index) => ({
|
|
52
64
|
row,
|
|
65
|
+
id: getRowId(row),
|
|
53
66
|
rowIndex: topPinnedCount + centerStartIndex + index,
|
|
54
67
|
}));
|
|
55
68
|
|
|
56
|
-
const combined = [...topPinned, ...centerVisible, ...bottomPinned];
|
|
69
|
+
const combined: GridVisibleRow<R>[] = [...topPinned, ...centerVisible, ...bottomPinned];
|
|
57
70
|
|
|
58
71
|
// Deduplication guard: a row ID in both pinnedRows and rows would cause React key collisions
|
|
59
72
|
const seenIds = new Set<GridRowId>();
|
|
60
73
|
return combined.filter(item => {
|
|
61
|
-
if (seenIds.has(item.
|
|
62
|
-
seenIds.add(item.
|
|
74
|
+
if (seenIds.has(item.id)) return false;
|
|
75
|
+
seenIds.add(item.id);
|
|
63
76
|
return true;
|
|
64
77
|
});
|
|
65
|
-
}, [renderContext, pinnedTopRows, pinnedBottomRows, paginatedUnpinnedRows, sortedUnpinnedRows, pagination]);
|
|
78
|
+
}, [renderContext, pinnedTopRows, pinnedBottomRows, paginatedUnpinnedRows, sortedUnpinnedRows, pagination, getRowId]);
|
|
66
79
|
}
|