@opencorestack/opengridx 2.1.0 → 3.0.1
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 +399 -0
- package/README.md +95 -72
- package/dist/index.d.ts +590 -131
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7891 -5631
- 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 -166
- 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 +6 -7
- package/docs/components/datagrid.md +15 -13
- 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 +90 -38
- 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 +126 -39
- 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 +72 -19
- package/docs/features/pinning.md +24 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +64 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +36 -8
- package/docs/features/virtualization.md +37 -14
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1233 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +5 -5
- package/lib/components/Cell/Cell.css +17 -1
- package/lib/components/Cell/Cell.tsx +129 -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 +15 -2
- 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 +85 -12
- package/lib/components/DataGrid/DataGrid.tsx +582 -876
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +165 -24
- 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 +62 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
- 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 +6 -3
- package/lib/components/Header/Header.tsx +181 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +76 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +177 -77
- 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 +19 -4
- 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 +279 -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 +104 -5
- 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 +447 -243
- 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 +17 -5
- 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 +82 -51
- 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 +298 -160
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +287 -220
- package/lib/index.ts +28 -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 +313 -116
- package/lib/utils/aggregation/index.ts +119 -24
- 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 +278 -139
- 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 +55 -5
- 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 +261 -164
- package/package.json +13 -5
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
- 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/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
- 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 -298
- package/lib/hooks/useTreeData.test.ts +0 -83
- package/lib/utils/aggregation/aggregation.test.ts +0 -16
- package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
- 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,16 +1,24 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import type { GridColDef, GridRowModel, GridRowId, GridColumnPinning, GridDetailPanelHeight } from '../../types';
|
|
3
|
-
import {
|
|
3
|
+
import { getPinnedColumnGroups, SYSTEM_COLUMN_WIDTH } from '../../utils/pinning';
|
|
4
|
+
import { clampColumnWidth } from '../../utils/columnWidth';
|
|
5
|
+
import { callGetDetailPanelHeight, getDetailPanelLayoutHeight } from '../../utils/detailPanel';
|
|
4
6
|
|
|
5
7
|
export interface UseLayoutParams<R extends GridRowModel> {
|
|
6
8
|
rowHeight: number;
|
|
7
9
|
pagination: boolean;
|
|
8
10
|
paginatedUnpinnedRows: R[];
|
|
9
11
|
sortedUnpinnedRows: R[];
|
|
12
|
+
/** Rows whose detail panel is open; the caller leaves out synthetic group rows (getDetailPanelRowIds). */
|
|
10
13
|
expandedRowIds: Set<GridRowId>;
|
|
11
14
|
getDetailPanelHeight?: (params: { row: R; id: GridRowId; rowIndex: number }) => GridDetailPanelHeight;
|
|
15
|
+
/** Rendered heights of `'auto'` detail panels, by row id (measured by the panels themselves). */
|
|
16
|
+
detailPanelHeights?: Map<GridRowId, number>;
|
|
12
17
|
pinnedTopRowsLength: number;
|
|
13
18
|
pinnedBottomRowsLength: number;
|
|
19
|
+
/** The pinned rows themselves; when given, their expanded detail panels count towards the pinned heights. */
|
|
20
|
+
pinnedTopRows?: R[];
|
|
21
|
+
pinnedBottomRows?: R[];
|
|
14
22
|
visibleOrderedColumns: GridColDef<R>[];
|
|
15
23
|
pinnedColumns?: GridColumnPinning;
|
|
16
24
|
columnWidths: Record<string, number>;
|
|
@@ -24,15 +32,21 @@ export interface UseLayoutParams<R extends GridRowModel> {
|
|
|
24
32
|
paginationMode: string;
|
|
25
33
|
isLoading: boolean;
|
|
26
34
|
pageSize: number;
|
|
35
|
+
/** Resolves a row's id (getRowId) for the expanded-detail-panel lookup; defaults to `row.id`. */
|
|
36
|
+
getRowId?: (row: R) => GridRowId;
|
|
27
37
|
}
|
|
28
38
|
|
|
39
|
+
const defaultGetRowId = <R extends GridRowModel>(row: R): GridRowId => row.id;
|
|
40
|
+
|
|
29
41
|
export interface LayoutResult<R extends GridRowModel> {
|
|
30
42
|
rowHeights: number[];
|
|
31
43
|
cumulativeHeights: number[];
|
|
32
44
|
unpinnedRowsHeight: number;
|
|
33
45
|
pinnedTopHeight: number;
|
|
34
46
|
pinnedBottomHeight: number;
|
|
47
|
+
/** Left-pinned columns in render order (`pinnedColumns.left` order) with resolved widths. */
|
|
35
48
|
leftPinnedCols: (GridColDef<R> & { width: number; zIndex: number })[];
|
|
49
|
+
/** Right-pinned columns in render order (`pinnedColumns.right` order) with resolved widths. */
|
|
36
50
|
rightPinnedCols: (GridColDef<R> & { width: number; zIndex: number })[];
|
|
37
51
|
unpinnedColsWithWidth: (GridColDef<R> & { width: number })[];
|
|
38
52
|
unpinnedColWidths: number[];
|
|
@@ -41,7 +55,10 @@ export interface LayoutResult<R extends GridRowModel> {
|
|
|
41
55
|
leftWidth: number;
|
|
42
56
|
rightWidth: number;
|
|
43
57
|
unpinnedTotalWidth: number;
|
|
58
|
+
/** Width of all system columns (drag handle, detail toggle, checkbox). */
|
|
44
59
|
systemColumnsWidth: number;
|
|
60
|
+
/** Width of the system columns that stay sticky on horizontal scroll. */
|
|
61
|
+
pinnedSystemColumnsWidth: number;
|
|
45
62
|
pinnedTopRowsLength: number;
|
|
46
63
|
pinnedBottomRowsLength: number;
|
|
47
64
|
unpinnedRowsLength: number;
|
|
@@ -62,6 +79,144 @@ function parseWidth(width: number | string | undefined): { type: 'fixed' | 'perc
|
|
|
62
79
|
return { type: 'fixed', value: 100 };
|
|
63
80
|
}
|
|
64
81
|
|
|
82
|
+
type WidthSpec =
|
|
83
|
+
| { kind: 'fixed'; width: number }
|
|
84
|
+
| { kind: 'percentage'; percentage: number }
|
|
85
|
+
| { kind: 'flex'; flex: number };
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolves every visible column to a pixel width. Pinned and unpinned columns follow the same
|
|
89
|
+
* rules, and every width is clamped to the column's minWidth / maxWidth (as the cell CSS is):
|
|
90
|
+
* - a number (or a manual resize) is used as is;
|
|
91
|
+
* - percentages are all taken of the same base: the data area left after the fixed-width columns;
|
|
92
|
+
* - flex and 'auto' columns share what is left after that.
|
|
93
|
+
*/
|
|
94
|
+
function resolveColumnWidths<R extends GridRowModel>(
|
|
95
|
+
columns: GridColDef<R>[],
|
|
96
|
+
columnWidths: Record<string, number>,
|
|
97
|
+
dataAreaWidth: number,
|
|
98
|
+
): { widths: Map<string, number>; flexByField: Map<string, number> } {
|
|
99
|
+
const specs = new Map<string, WidthSpec>();
|
|
100
|
+
columns.forEach(col => {
|
|
101
|
+
const hasManualWidth = Object.prototype.hasOwnProperty.call(columnWidths, col.field);
|
|
102
|
+
if (!hasManualWidth && col.flex && col.flex > 0) {
|
|
103
|
+
specs.set(col.field, { kind: 'flex', flex: col.flex });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const parsed = parseWidth(columnWidths[col.field] ?? col.width);
|
|
107
|
+
if (parsed.type === 'fixed') {
|
|
108
|
+
specs.set(col.field, { kind: 'fixed', width: clampColumnWidth(parsed.value, col.minWidth, col.maxWidth) });
|
|
109
|
+
} else if (parsed.type === 'percentage') {
|
|
110
|
+
specs.set(col.field, { kind: 'percentage', percentage: parsed.value });
|
|
111
|
+
} else {
|
|
112
|
+
specs.set(col.field, { kind: 'flex', flex: 1 });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Widths the columns need before any sharing, so an overflowing grid scrolls instead of squashing.
|
|
117
|
+
const naturalWidth = columns.reduce((sum, col) => {
|
|
118
|
+
const spec = specs.get(col.field)!;
|
|
119
|
+
if (spec.kind === 'fixed') return sum + spec.width;
|
|
120
|
+
if (spec.kind === 'percentage') return sum + 100;
|
|
121
|
+
if (col.flex && col.flex > 0) {
|
|
122
|
+
const minWidth = col.minWidth ?? 150;
|
|
123
|
+
return sum + (minWidth || (typeof col.width === 'number' ? col.width : 150));
|
|
124
|
+
}
|
|
125
|
+
return sum + (col.minWidth ?? 150);
|
|
126
|
+
}, 0);
|
|
127
|
+
|
|
128
|
+
const availableWidth = Math.max(dataAreaWidth, naturalWidth);
|
|
129
|
+
const fixedWidth = columns.reduce((sum, col) => {
|
|
130
|
+
const spec = specs.get(col.field)!;
|
|
131
|
+
return spec.kind === 'fixed' ? sum + spec.width : sum;
|
|
132
|
+
}, 0);
|
|
133
|
+
|
|
134
|
+
const widths = new Map<string, number>();
|
|
135
|
+
const percentageBase = Math.max(0, availableWidth - fixedWidth);
|
|
136
|
+
let percentageTotal = 0;
|
|
137
|
+
columns.forEach(col => {
|
|
138
|
+
const spec = specs.get(col.field)!;
|
|
139
|
+
if (spec.kind === 'fixed') {
|
|
140
|
+
widths.set(col.field, spec.width);
|
|
141
|
+
} else if (spec.kind === 'percentage') {
|
|
142
|
+
const raw = (spec.percentage / 100) * percentageBase;
|
|
143
|
+
const width = clampColumnWidth(raw > 0 ? raw : 100, col.minWidth ?? 50, col.maxWidth);
|
|
144
|
+
widths.set(col.field, width);
|
|
145
|
+
percentageTotal += width;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const freeSpace = Math.max(0, percentageBase - percentageTotal);
|
|
150
|
+
const flexByField = new Map<string, number>();
|
|
151
|
+
const flexItems = columns.flatMap(col => {
|
|
152
|
+
const spec = specs.get(col.field)!;
|
|
153
|
+
if (spec.kind !== 'flex') return [];
|
|
154
|
+
flexByField.set(col.field, spec.flex);
|
|
155
|
+
return [{
|
|
156
|
+
field: col.field,
|
|
157
|
+
flex: spec.flex,
|
|
158
|
+
minWidth: col.minWidth ?? 50,
|
|
159
|
+
maxWidth: col.maxWidth ?? Infinity,
|
|
160
|
+
frozen: false,
|
|
161
|
+
computedWidth: 0,
|
|
162
|
+
}];
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Distributes the free space by flex weight, freezing items that hit their min/max and
|
|
166
|
+
// redistributing, the way CSS flexbox resolves flexible lengths.
|
|
167
|
+
let iterations = 0;
|
|
168
|
+
const maxIterations = flexItems.length * 2;
|
|
169
|
+
while (iterations < maxIterations) {
|
|
170
|
+
iterations++;
|
|
171
|
+
const unfrozen = flexItems.filter(f => !f.frozen);
|
|
172
|
+
if (unfrozen.length === 0) break;
|
|
173
|
+
|
|
174
|
+
const unfrozenFlexTotal = unfrozen.reduce((sum, f) => sum + f.flex, 0);
|
|
175
|
+
const frozenWidthTotal = flexItems.reduce((sum, f) => f.frozen ? sum + f.computedWidth : sum, 0);
|
|
176
|
+
const currentFreeSpace = Math.max(0, freeSpace - frozenWidthTotal);
|
|
177
|
+
|
|
178
|
+
if (unfrozenFlexTotal <= 0) {
|
|
179
|
+
unfrozen.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const pixelsPerFlex = currentFreeSpace / unfrozenFlexTotal;
|
|
184
|
+
let totalViolation = 0;
|
|
185
|
+
const minViolators: typeof flexItems = [];
|
|
186
|
+
const maxViolators: typeof flexItems = [];
|
|
187
|
+
|
|
188
|
+
unfrozen.forEach(f => {
|
|
189
|
+
const rawWidth = pixelsPerFlex * f.flex;
|
|
190
|
+
if (rawWidth < f.minWidth) {
|
|
191
|
+
totalViolation += f.minWidth - rawWidth;
|
|
192
|
+
minViolators.push(f);
|
|
193
|
+
} else if (rawWidth > f.maxWidth) {
|
|
194
|
+
totalViolation += f.maxWidth - rawWidth;
|
|
195
|
+
maxViolators.push(f);
|
|
196
|
+
} else {
|
|
197
|
+
f.computedWidth = rawWidth;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (minViolators.length === 0 && maxViolators.length === 0) {
|
|
202
|
+
unfrozen.forEach(f => { f.frozen = true; });
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (totalViolation > 0) {
|
|
207
|
+
minViolators.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
|
|
208
|
+
} else if (totalViolation < 0) {
|
|
209
|
+
maxViolators.forEach(f => { f.computedWidth = f.maxWidth; f.frozen = true; });
|
|
210
|
+
} else {
|
|
211
|
+
minViolators.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
|
|
212
|
+
maxViolators.forEach(f => { f.computedWidth = f.maxWidth; f.frozen = true; });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
flexItems.forEach(f => widths.set(f.field, f.computedWidth));
|
|
216
|
+
|
|
217
|
+
return { widths, flexByField };
|
|
218
|
+
}
|
|
219
|
+
|
|
65
220
|
export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): LayoutResult<R> {
|
|
66
221
|
const {
|
|
67
222
|
rowHeight,
|
|
@@ -70,8 +225,11 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
70
225
|
sortedUnpinnedRows,
|
|
71
226
|
expandedRowIds,
|
|
72
227
|
getDetailPanelHeight,
|
|
228
|
+
detailPanelHeights,
|
|
73
229
|
pinnedTopRowsLength,
|
|
74
230
|
pinnedBottomRowsLength,
|
|
231
|
+
pinnedTopRows,
|
|
232
|
+
pinnedBottomRows,
|
|
75
233
|
visibleOrderedColumns,
|
|
76
234
|
pinnedColumns,
|
|
77
235
|
columnWidths,
|
|
@@ -79,22 +237,25 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
79
237
|
checkboxSelection,
|
|
80
238
|
hasDetailPanel,
|
|
81
239
|
rowReordering,
|
|
240
|
+
pinCheckboxColumn,
|
|
241
|
+
pinExpandColumn,
|
|
82
242
|
paginationMode,
|
|
83
243
|
isLoading,
|
|
84
244
|
pageSize,
|
|
245
|
+
getRowId = defaultGetRowId,
|
|
85
246
|
} = params;
|
|
86
247
|
|
|
87
248
|
return useMemo<LayoutResult<R>>(() => {
|
|
88
249
|
const unpinnedRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
|
|
89
250
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
if (expandedRowIds.has(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
251
|
+
const heightOf = (row: R, rowIndex: number) => {
|
|
252
|
+
const id = getRowId(row);
|
|
253
|
+
if (!expandedRowIds.has(id)) return rowHeight;
|
|
254
|
+
const panelHeight = callGetDetailPanelHeight(getDetailPanelHeight, { row, id, rowIndex });
|
|
255
|
+
return rowHeight + getDetailPanelLayoutHeight(panelHeight, detailPanelHeights?.get(id));
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const rowHeights = unpinnedRows.map((row, index) => heightOf(row, pinnedTopRowsLength + index));
|
|
98
259
|
|
|
99
260
|
const cumulativeHeights = rowHeights.reduce((acc, height, index) => {
|
|
100
261
|
acc.push((acc[index - 1] || 0) + height);
|
|
@@ -105,199 +266,51 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
105
266
|
? Math.min(pageSize, 20)
|
|
106
267
|
: 0;
|
|
107
268
|
const unpinnedRowsHeight = (cumulativeHeights[cumulativeHeights.length - 1] || 0) + skeletonCount * rowHeight;
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const leftWidth = rawLeftPinnedCols.reduce((sum, c) => {
|
|
132
|
-
const parsed = parseWidth(getColWidth(c));
|
|
133
|
-
return sum + (parsed.type === 'fixed' ? parsed.value : 100);
|
|
134
|
-
}, 0);
|
|
135
|
-
const rightWidth = rawRightPinnedCols.reduce((sum, c) => {
|
|
136
|
-
const parsed = parseWidth(getColWidth(c));
|
|
137
|
-
return sum + (parsed.type === 'fixed' ? parsed.value : 100);
|
|
138
|
-
}, 0);
|
|
139
|
-
|
|
140
|
-
const naturalFlexWidth = unpinnedCols.reduce((sum, col) => {
|
|
141
|
-
const hasManualWidth = Object.prototype.hasOwnProperty.call(columnWidths, col.field);
|
|
142
|
-
if (!hasManualWidth && col.flex && col.flex > 0) {
|
|
143
|
-
const minWidth = col.minWidth ?? 150;
|
|
144
|
-
const w = typeof col.width === 'number' ? col.width : 150;
|
|
145
|
-
return sum + (minWidth || w);
|
|
146
|
-
}
|
|
147
|
-
const parsed = parseWidth(getColWidth(col));
|
|
148
|
-
if (parsed.type === 'fixed') return sum + parsed.value;
|
|
149
|
-
if (parsed.type === 'percentage') return sum + 100;
|
|
150
|
-
return sum + (col.minWidth ?? 150);
|
|
151
|
-
}, 0);
|
|
152
|
-
|
|
153
|
-
const availableWidth = Math.max(
|
|
154
|
-
viewportWidth - systemColumnsWidth - leftWidth - rightWidth,
|
|
155
|
-
naturalFlexWidth
|
|
269
|
+
// Pinned rows are indexed from 0 within their section (as GridPinnedRows renders them).
|
|
270
|
+
const pinnedBlockHeight = (rows: R[] | undefined, count: number) =>
|
|
271
|
+
rows ? rows.reduce((sum, row, index) => sum + heightOf(row, index), 0) : count * rowHeight;
|
|
272
|
+
const pinnedTopHeight = pinnedBlockHeight(pinnedTopRows, pinnedTopRowsLength);
|
|
273
|
+
const pinnedBottomHeight = pinnedBlockHeight(pinnedBottomRows, pinnedBottomRowsLength);
|
|
274
|
+
|
|
275
|
+
// Pinned columns render in pinnedColumns.left / .right order; unpinned ones in column order.
|
|
276
|
+
const { left: rawLeftPinnedCols, center: unpinnedCols, right: rawRightPinnedCols } =
|
|
277
|
+
getPinnedColumnGroups(visibleOrderedColumns, pinnedColumns);
|
|
278
|
+
|
|
279
|
+
const systemColumnsWidth =
|
|
280
|
+
(checkboxSelection ? SYSTEM_COLUMN_WIDTH : 0) +
|
|
281
|
+
(hasDetailPanel ? SYSTEM_COLUMN_WIDTH : 0) +
|
|
282
|
+
(rowReordering ? SYSTEM_COLUMN_WIDTH : 0);
|
|
283
|
+
const pinnedSystemColumnsWidth =
|
|
284
|
+
(rowReordering ? SYSTEM_COLUMN_WIDTH : 0) +
|
|
285
|
+
(hasDetailPanel && pinExpandColumn ? SYSTEM_COLUMN_WIDTH : 0) +
|
|
286
|
+
(checkboxSelection && pinCheckboxColumn ? SYSTEM_COLUMN_WIDTH : 0);
|
|
287
|
+
|
|
288
|
+
const { widths, flexByField } = resolveColumnWidths(
|
|
289
|
+
[...rawLeftPinnedCols, ...unpinnedCols, ...rawRightPinnedCols],
|
|
290
|
+
columnWidths,
|
|
291
|
+
viewportWidth - systemColumnsWidth,
|
|
156
292
|
);
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
unpinnedCols.forEach(col => {
|
|
163
|
-
const hasManualWidth = Object.prototype.hasOwnProperty.call(columnWidths, col.field);
|
|
164
|
-
const parsed = parseWidth(getColWidth(col));
|
|
165
|
-
|
|
166
|
-
if (!hasManualWidth && col.flex && col.flex > 0) {
|
|
167
|
-
flexCols.push({ col, flex: col.flex });
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (parsed.type === 'fixed') {
|
|
172
|
-
fixedWidthCols.push({ col, width: parsed.value });
|
|
173
|
-
} else if (parsed.type === 'percentage') {
|
|
174
|
-
percentageCols.push({ col, percentage: parsed.value });
|
|
175
|
-
} else {
|
|
176
|
-
flexCols.push({ col, flex: 1 });
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
const fixedWidth = fixedWidthCols.reduce((sum, { width }) => sum + width, 0);
|
|
181
|
-
let remainingSpaceForFlex = Math.max(0, availableWidth - fixedWidth);
|
|
182
|
-
|
|
183
|
-
const percentageWidthMap = new Map<string, number>();
|
|
184
|
-
percentageCols.forEach(({ col, percentage }) => {
|
|
185
|
-
const calculatedWidth = (percentage / 100) * remainingSpaceForFlex;
|
|
186
|
-
percentageWidthMap.set(col.field, calculatedWidth);
|
|
187
|
-
remainingSpaceForFlex -= calculatedWidth;
|
|
188
|
-
});
|
|
189
|
-
remainingSpaceForFlex = Math.max(0, remainingSpaceForFlex);
|
|
190
|
-
|
|
191
|
-
const finalFlexWidths = new Map<string, number>();
|
|
192
|
-
const flexItems = flexCols.map(f => ({
|
|
193
|
-
...f,
|
|
194
|
-
minWidth: f.col.minWidth ?? 50,
|
|
195
|
-
maxWidth: f.col.maxWidth ?? Infinity,
|
|
196
|
-
frozen: false,
|
|
197
|
-
computedWidth: 0
|
|
198
|
-
}));
|
|
199
|
-
|
|
200
|
-
const solveFlexAllocation = () => {
|
|
201
|
-
let iterations = 0;
|
|
202
|
-
const maxIterations = flexItems.length * 2;
|
|
203
|
-
|
|
204
|
-
while (iterations < maxIterations) {
|
|
205
|
-
iterations++;
|
|
206
|
-
const unfrozen = flexItems.filter(f => !f.frozen);
|
|
207
|
-
if (unfrozen.length === 0) break;
|
|
208
|
-
|
|
209
|
-
const unfrozenFlexTotal = unfrozen.reduce((sum, f) => sum + f.flex, 0);
|
|
210
|
-
const frozenWidthTotal = flexItems.reduce((sum, f) => f.frozen ? sum + f.computedWidth : sum, 0);
|
|
211
|
-
const currentFreeSpace = Math.max(0, remainingSpaceForFlex - frozenWidthTotal);
|
|
212
|
-
|
|
213
|
-
if (unfrozenFlexTotal <= 0) {
|
|
214
|
-
unfrozen.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
|
|
215
|
-
break;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const pixelsPerFlex = currentFreeSpace / unfrozenFlexTotal;
|
|
219
|
-
let totalViolation = 0;
|
|
220
|
-
const minViolators: typeof flexItems = [];
|
|
221
|
-
const maxViolators: typeof flexItems = [];
|
|
222
|
-
|
|
223
|
-
unfrozen.forEach(f => {
|
|
224
|
-
const rawWidth = pixelsPerFlex * f.flex;
|
|
225
|
-
if (rawWidth < f.minWidth) {
|
|
226
|
-
totalViolation += f.minWidth - rawWidth;
|
|
227
|
-
minViolators.push(f);
|
|
228
|
-
} else if (rawWidth > f.maxWidth) {
|
|
229
|
-
totalViolation += f.maxWidth - rawWidth;
|
|
230
|
-
maxViolators.push(f);
|
|
231
|
-
} else {
|
|
232
|
-
f.computedWidth = rawWidth;
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
if (minViolators.length === 0 && maxViolators.length === 0) {
|
|
237
|
-
unfrozen.forEach(f => f.frozen = true);
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (totalViolation > 0) {
|
|
242
|
-
minViolators.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
|
|
243
|
-
} else if (totalViolation < 0) {
|
|
244
|
-
maxViolators.forEach(f => { f.computedWidth = f.maxWidth; f.frozen = true; });
|
|
245
|
-
} else {
|
|
246
|
-
minViolators.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
|
|
247
|
-
maxViolators.forEach(f => { f.computedWidth = f.maxWidth; f.frozen = true; });
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
solveFlexAllocation();
|
|
253
|
-
flexItems.forEach(f => finalFlexWidths.set(f.col.field, f.computedWidth));
|
|
254
|
-
|
|
255
|
-
const computedWidthMap = new Map<string, number>();
|
|
256
|
-
unpinnedCols.forEach(col => {
|
|
257
|
-
let computedWidth: number;
|
|
258
|
-
if (finalFlexWidths.has(col.field)) {
|
|
259
|
-
computedWidth = finalFlexWidths.get(col.field)!;
|
|
260
|
-
} else {
|
|
261
|
-
const fixedCol = fixedWidthCols.find(f => f.col.field === col.field);
|
|
262
|
-
const pctCol = percentageCols.find(p => p.col.field === col.field);
|
|
263
|
-
if (fixedCol) {
|
|
264
|
-
computedWidth = fixedCol.width;
|
|
265
|
-
} else if (pctCol) {
|
|
266
|
-
const rawPctWidth = percentageWidthMap.get(col.field) || 0;
|
|
267
|
-
computedWidth = Math.max(col.minWidth ?? 50, rawPctWidth || 100);
|
|
268
|
-
} else {
|
|
269
|
-
computedWidth = 100;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
unpinnedColWidths.push(computedWidth);
|
|
273
|
-
computedWidthMap.set(col.field, computedWidth);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
const unpinnedColsWithWidth = unpinnedCols.map(col => {
|
|
277
|
-
const flexItem = flexItems.find(f => f.col.field === col.field);
|
|
278
|
-
return {
|
|
279
|
-
...col,
|
|
280
|
-
width: computedWidthMap.get(col.field) ?? (typeof col.width === 'number' ? col.width : 100),
|
|
281
|
-
flex: flexItem ? flexItem.flex : col.flex
|
|
282
|
-
};
|
|
293
|
+
const widthOf = (col: GridColDef<R>) => widths.get(col.field) ?? 100;
|
|
294
|
+
const withWidth = (col: GridColDef<R>) => ({
|
|
295
|
+
...col,
|
|
296
|
+
width: widthOf(col),
|
|
297
|
+
flex: flexByField.get(col.field) ?? col.flex,
|
|
283
298
|
});
|
|
284
299
|
|
|
285
300
|
const baseLeftZ = 11;
|
|
286
|
-
const leftPinnedCols = rawLeftPinnedCols.map((col, i) => {
|
|
287
|
-
const parsed = parseWidth(getColWidth(col));
|
|
288
|
-
return { ...col, width: parsed.type === 'fixed' ? parsed.value : 100, zIndex: baseLeftZ + i };
|
|
289
|
-
});
|
|
301
|
+
const leftPinnedCols = rawLeftPinnedCols.map((col, i) => ({ ...withWidth(col), zIndex: baseLeftZ + i }));
|
|
290
302
|
|
|
291
303
|
const baseRightZ = 11;
|
|
292
|
-
const rightPinnedCols = rawRightPinnedCols.map((col, i) => {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
});
|
|
304
|
+
const rightPinnedCols = rawRightPinnedCols.map((col, i) => ({
|
|
305
|
+
...withWidth(col),
|
|
306
|
+
zIndex: baseRightZ + (rawRightPinnedCols.length - 1 - i),
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
const unpinnedColsWithWidth = unpinnedCols.map(withWidth);
|
|
310
|
+
const unpinnedColWidths = unpinnedColsWithWidth.map(col => col.width);
|
|
300
311
|
|
|
312
|
+
const leftWidth = leftPinnedCols.reduce((sum, col) => sum + col.width, 0);
|
|
313
|
+
const rightWidth = rightPinnedCols.reduce((sum, col) => sum + col.width, 0);
|
|
301
314
|
const unpinnedTotalWidth = unpinnedColWidths.reduce((sum, w) => sum + w, 0);
|
|
302
315
|
const totalWidth = leftWidth + unpinnedTotalWidth + rightWidth + systemColumnsWidth;
|
|
303
316
|
const unpinnedAccWidths = unpinnedColWidths.reduce((acc, w, i) => {
|
|
@@ -321,6 +334,7 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
321
334
|
rightWidth,
|
|
322
335
|
unpinnedTotalWidth,
|
|
323
336
|
systemColumnsWidth,
|
|
337
|
+
pinnedSystemColumnsWidth,
|
|
324
338
|
pinnedTopRowsLength,
|
|
325
339
|
pinnedBottomRowsLength,
|
|
326
340
|
unpinnedRowsLength: unpinnedRows.length,
|
|
@@ -332,8 +346,11 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
332
346
|
sortedUnpinnedRows,
|
|
333
347
|
expandedRowIds,
|
|
334
348
|
getDetailPanelHeight,
|
|
349
|
+
detailPanelHeights,
|
|
335
350
|
pinnedTopRowsLength,
|
|
336
351
|
pinnedBottomRowsLength,
|
|
352
|
+
pinnedTopRows,
|
|
353
|
+
pinnedBottomRows,
|
|
337
354
|
visibleOrderedColumns,
|
|
338
355
|
pinnedColumns,
|
|
339
356
|
columnWidths,
|
|
@@ -341,8 +358,11 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
341
358
|
checkboxSelection,
|
|
342
359
|
hasDetailPanel,
|
|
343
360
|
rowReordering,
|
|
361
|
+
pinCheckboxColumn,
|
|
362
|
+
pinExpandColumn,
|
|
344
363
|
paginationMode,
|
|
345
364
|
isLoading,
|
|
346
365
|
pageSize,
|
|
366
|
+
getRowId,
|
|
347
367
|
]);
|
|
348
368
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
import type { GridRowScrollEndParams } from '../../types';
|
|
4
|
+
import { getRowsScrollEndParams } from '../../utils/scroll/scrollEnd';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A scroll handler that calls `onRowsScrollEnd` once per arrival near the end of a row container.
|
|
8
|
+
* It re-arms when the container leaves the threshold or `rowCount` changes (new rows loaded), so a
|
|
9
|
+
* consumer that loads the next page from the callback is not flooded with calls while it loads.
|
|
10
|
+
* The grid viewport has the same rule built into useGridScrollSync; the list view uses this hook.
|
|
11
|
+
*/
|
|
12
|
+
export function useRowsScrollEndLatch(
|
|
13
|
+
onRowsScrollEnd: ((params: GridRowScrollEndParams) => void) | undefined,
|
|
14
|
+
rowCount: number,
|
|
15
|
+
): ((event: React.UIEvent<HTMLElement>) => void) | undefined {
|
|
16
|
+
const armedRef = useRef(true);
|
|
17
|
+
const callbackRef = useRef(onRowsScrollEnd);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
callbackRef.current = onRowsScrollEnd;
|
|
21
|
+
}, [onRowsScrollEnd]);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
armedRef.current = true;
|
|
25
|
+
}, [rowCount]);
|
|
26
|
+
|
|
27
|
+
const handleScroll = useCallback((event: React.UIEvent<HTMLElement>) => {
|
|
28
|
+
const params = getRowsScrollEndParams(event.currentTarget);
|
|
29
|
+
if (!params) {
|
|
30
|
+
armedRef.current = true;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!armedRef.current) return;
|
|
34
|
+
const callback = callbackRef.current;
|
|
35
|
+
if (!callback) return;
|
|
36
|
+
armedRef.current = false;
|
|
37
|
+
callback(params);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
return onRowsScrollEnd ? handleScroll : undefined;
|
|
41
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
function shallowEqualObjects(a: object, b: object): boolean {
|
|
4
|
+
if (a === b) return true;
|
|
5
|
+
const aKeys = Object.keys(a);
|
|
6
|
+
if (aKeys.length !== Object.keys(b).length) return false;
|
|
7
|
+
const aRecord = a as Record<string, unknown>;
|
|
8
|
+
const bRecord = b as Record<string, unknown>;
|
|
9
|
+
return aKeys.every(key => Object.prototype.hasOwnProperty.call(b, key) && Object.is(aRecord[key], bRecord[key]));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Same length, and each column has the same own properties with identical values (functions by identity). */
|
|
13
|
+
export function columnsShallowEqual<C extends object>(a: readonly C[], b: readonly C[]): boolean {
|
|
14
|
+
if (a === b) return true;
|
|
15
|
+
if (a.length !== b.length) return false;
|
|
16
|
+
return a.every((col, index) => shallowEqualObjects(col, b[index]));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The `columns` prop with a stable identity: while each column definition is shallowly equal to
|
|
21
|
+
* the previous one (same properties, same values, callbacks compared by identity), the previous
|
|
22
|
+
* array is returned. An inline `columns={[...]}` (a new but equal array on every parent render)
|
|
23
|
+
* then does not re-run the filter, sort, aggregation, grouping and spanning passes, which are all
|
|
24
|
+
* memoised on column identity. A changed property, including a new inline callback, is a new array.
|
|
25
|
+
*
|
|
26
|
+
* The previous array is held in state and replaced during render (React's "adjust state when a
|
|
27
|
+
* prop changes" pattern), so no ref is written during render.
|
|
28
|
+
*/
|
|
29
|
+
export function useStableColumns<T extends readonly object[]>(columns: T): T {
|
|
30
|
+
const [stable, setStable] = useState(columns);
|
|
31
|
+
const same = columnsShallowEqual(stable, columns);
|
|
32
|
+
if (!same) setStable(columns);
|
|
33
|
+
return same ? stable : columns;
|
|
34
|
+
}
|