@opencorestack/opengridx 0.1.8 → 1.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/index.d.ts +60 -41
  3. package/dist/opengridx.es.js +5271 -4660
  4. package/dist/opengridx.es.js.map +1 -1
  5. package/dist/opengridx.umd.js +12 -12
  6. package/dist/opengridx.umd.js.map +1 -1
  7. package/docs/README.md +14 -0
  8. package/docs/components/aggregation-footer.md +48 -0
  9. package/docs/components/datagrid.md +3 -0
  10. package/docs/components/empty-state.md +50 -0
  11. package/docs/components/error-overlay.md +40 -0
  12. package/docs/roadmap.md +20 -1
  13. package/lib/components/Cell/Cell.tsx +61 -64
  14. package/lib/components/Cell/GridEditInputCell.tsx +4 -4
  15. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
  16. package/lib/components/DataGrid/DataGrid.regression.test.tsx +132 -0
  17. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +58 -0
  18. package/lib/components/DataGrid/DataGrid.tsx +396 -1654
  19. package/lib/components/DataGrid/GridAggregationFooter.tsx +68 -0
  20. package/lib/components/DataGrid/GridEmptyState.tsx +16 -0
  21. package/lib/components/DataGrid/GridErrorOverlay.tsx +33 -0
  22. package/lib/components/DataGrid/GridListView.tsx +116 -0
  23. package/lib/components/DataGrid/GridPinnedRows.tsx +98 -0
  24. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +124 -0
  25. package/lib/components/DataGrid/GridVirtualRows.tsx +202 -0
  26. package/lib/components/FilterPanel/FilterPanel.tsx +18 -10
  27. package/lib/components/Header/ColumnMenu.tsx +2 -2
  28. package/lib/components/Header/Header.tsx +4 -4
  29. package/lib/components/ListView/ListViewRow.tsx +2 -2
  30. package/lib/components/Row/Row.tsx +46 -45
  31. package/lib/components/SkeletonRow/SkeletonRow.tsx +9 -2
  32. package/lib/components/Toolbar/GlobalSearch.tsx +1 -1
  33. package/lib/components/Toolbar/GridToolbar.tsx +26 -5
  34. package/lib/components/Toolbar/PivotPanel.tsx +7 -2
  35. package/lib/components/ui/DragHandleIcon.tsx +1 -1
  36. package/lib/hooks/core/useDataGrid.regression.test.ts +66 -0
  37. package/lib/hooks/core/useDataGrid.ts +15 -35
  38. package/lib/hooks/core/useGridColumns.tsx +228 -0
  39. package/lib/hooks/core/useGridControlledState.ts +192 -0
  40. package/lib/hooks/core/useGridKeyboardNavigation.ts +371 -0
  41. package/lib/hooks/core/useGridRowPipeline.ts +109 -0
  42. package/lib/hooks/core/useGridScrollSync.ts +52 -0
  43. package/lib/hooks/core/useGridStateSnapshot.ts +63 -0
  44. package/lib/hooks/core/useGridVirtualization.ts +186 -0
  45. package/lib/hooks/core/useGridVisibleRows.ts +66 -0
  46. package/lib/hooks/core/useLayout.ts +348 -0
  47. package/lib/hooks/features/useAggregation.test.ts +135 -0
  48. package/lib/hooks/features/useAggregation.ts +7 -7
  49. package/lib/hooks/features/useGridClipboard.ts +1 -1
  50. package/lib/hooks/features/useGridDataSource.ts +2 -2
  51. package/lib/hooks/features/useGridEditing.ts +31 -32
  52. package/lib/hooks/features/usePivot.test.ts +202 -0
  53. package/lib/hooks/features/usePivot.ts +0 -0
  54. package/lib/hooks/useColumnReorder.ts +1 -1
  55. package/lib/hooks/useRowGrouping.ts +14 -14
  56. package/lib/hooks/useTreeData.ts +1 -1
  57. package/lib/state/types.ts +1 -1
  58. package/lib/state/useGridStateStorage.ts +2 -2
  59. package/lib/types/index.ts +48 -32
  60. package/lib/utils/export/exportToExcelAdvanced.ts +14 -14
  61. package/lib/utils/export/index.ts +23 -18
  62. package/lib/utils/filtering/filtering.test.ts +178 -0
  63. package/lib/utils/filtering/index.ts +20 -20
  64. package/lib/utils/sorting/index.ts +1 -1
  65. package/lib/utils/sorting/sorting.test.ts +88 -0
  66. package/package.json +12 -5
  67. package/lib/utils/virtualization/index.ts +0 -106
@@ -0,0 +1,371 @@
1
+ import { useState, useCallback } from 'react';
2
+ import type { GridRowModel, GridRowId, GridColDef, GridCellParams, GridSortItem, GridSortDirection } from '../../types';
3
+ import type { GridEditingState } from '../features/useGridEditing';
4
+
5
+ interface ColumnMetrics {
6
+ leftPinnedWidth: number;
7
+ rightPinnedWidth: number;
8
+ unpinnedAccWidths: number[];
9
+ unpinnedCols: { field: string }[];
10
+ totalSpecialsWidth: number;
11
+ pinnedSpecialsWidth: number;
12
+ }
13
+
14
+ interface VirtualizationSnapshot {
15
+ cumulativeHeights: number[];
16
+ pinnedTopHeight: number;
17
+ columnMetrics: ColumnMetrics | null;
18
+ }
19
+
20
+ export interface UseGridKeyboardNavigationParams<R extends GridRowModel> {
21
+ allRenderableRows: R[];
22
+ navigationColumns: { field: string; editable?: boolean; sortable?: boolean }[];
23
+ checkboxSelection: boolean;
24
+ selectedRowIds: Set<GridRowId>;
25
+ handleSelectionChange: (id: GridRowId, selected: boolean) => void;
26
+ handleDetailPanelToggle: (id: GridRowId) => void;
27
+ editingHandlers: {
28
+ editingCell: GridEditingState['editingCell'];
29
+ startCellEdit: (params: { id: GridRowId; field: string; value: unknown }) => void;
30
+ stopCellEdit: (params?: { cancel?: boolean }) => void;
31
+ };
32
+ setKeyboardMode: (mode: boolean) => void;
33
+ sortModel: GridSortItem[];
34
+ handleSort: (field: string, direction: GridSortDirection) => void;
35
+ isCellEditable?: (params: GridCellParams<R>) => boolean;
36
+ pagination: boolean;
37
+ pageSize: number;
38
+ virtualization: VirtualizationSnapshot;
39
+ viewportRef: React.RefObject<HTMLDivElement | null>;
40
+ }
41
+
42
+ export interface FocusedCell {
43
+ id: GridRowId;
44
+ field: string;
45
+ }
46
+
47
+ export interface UseGridKeyboardNavigationReturn {
48
+ focusedCell: FocusedCell | null;
49
+ setFocusedCell: React.Dispatch<React.SetStateAction<FocusedCell | null>>;
50
+ handleFocus: (event: React.FocusEvent<HTMLDivElement>) => void;
51
+ handleBlur: (event: React.FocusEvent<HTMLDivElement>) => void;
52
+ handleKeyDown: (event: React.KeyboardEvent) => void;
53
+ }
54
+
55
+ export function useGridKeyboardNavigation<R extends GridRowModel>(
56
+ params: UseGridKeyboardNavigationParams<R>
57
+ ): UseGridKeyboardNavigationReturn {
58
+ const {
59
+ allRenderableRows,
60
+ navigationColumns,
61
+ checkboxSelection,
62
+ selectedRowIds,
63
+ handleSelectionChange,
64
+ handleDetailPanelToggle,
65
+ editingHandlers,
66
+ setKeyboardMode,
67
+ sortModel,
68
+ handleSort,
69
+ isCellEditable,
70
+ pagination,
71
+ pageSize,
72
+ virtualization,
73
+ viewportRef,
74
+ } = params;
75
+
76
+ const [focusedCell, setFocusedCell] = useState<FocusedCell | null>(null);
77
+
78
+ const handleFocus = useCallback((event: React.FocusEvent<HTMLDivElement>) => {
79
+ if (event.target === event.currentTarget) {
80
+ setFocusedCell(prev => {
81
+ if (prev) return prev;
82
+ if (checkboxSelection) {
83
+ return { id: 'HEADER', field: '__checkbox_col__' };
84
+ }
85
+ const firstRow = allRenderableRows[0];
86
+ const firstCol = navigationColumns[0];
87
+ if (firstRow && firstCol) {
88
+ return { id: firstRow.id, field: firstCol.field };
89
+ }
90
+ return null;
91
+ });
92
+ }
93
+ }, [allRenderableRows, navigationColumns, checkboxSelection]);
94
+
95
+ const handleBlur = useCallback((event: React.FocusEvent<HTMLDivElement>) => {
96
+ if (!event.currentTarget.contains(event.relatedTarget as Node)) {
97
+ setFocusedCell(null);
98
+ }
99
+ }, []);
100
+
101
+ const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
102
+ setKeyboardMode(true);
103
+
104
+ if (!focusedCell) return;
105
+
106
+ const { id, field } = focusedCell;
107
+ const isEditing = Boolean(editingHandlers.editingCell);
108
+
109
+ if (id === 'HEADER' && (event.key === 'Enter' || event.key === ' ')) {
110
+ event.preventDefault();
111
+ const col = navigationColumns.find(c => c.field === field);
112
+ if (col && col.sortable !== false) {
113
+ const currentSort = sortModel.find(item => item.field === field);
114
+ let newDirection: GridSortDirection = 'asc';
115
+ if (currentSort) {
116
+ newDirection = currentSort.sort === 'asc' ? 'desc' : null;
117
+ }
118
+ handleSort(field, newDirection);
119
+ }
120
+ return;
121
+ }
122
+
123
+ if (event.key === 'Enter') {
124
+ event.preventDefault();
125
+ if (isEditing) {
126
+ editingHandlers.stopCellEdit();
127
+ } else {
128
+ const col = navigationColumns.find(c => c.field === field);
129
+ if (col?.editable) {
130
+ const row = allRenderableRows.find(r => r.id === id);
131
+ if (row) {
132
+ editingHandlers.startCellEdit({ id, field, value: (row as R & Record<string, unknown>)[field] });
133
+ }
134
+ }
135
+ }
136
+ return;
137
+ }
138
+
139
+ if (event.key === ' ' || event.key === 'Spacebar') {
140
+ if (!isEditing) {
141
+ if (field === '__checkbox_col__') {
142
+ event.preventDefault();
143
+ handleSelectionChange(id, !selectedRowIds.has(id));
144
+ return;
145
+ }
146
+ if (field === '__expand_col__') {
147
+ event.preventDefault();
148
+ handleDetailPanelToggle(id);
149
+ return;
150
+ }
151
+ }
152
+ }
153
+
154
+ if (event.key === 'Escape') {
155
+ if (isEditing) {
156
+ event.preventDefault();
157
+ editingHandlers.stopCellEdit({ cancel: true });
158
+ }
159
+ return;
160
+ }
161
+
162
+ if (isEditing && event.key !== 'Tab') return;
163
+
164
+ const isHeader = id === 'HEADER';
165
+ const rowIndex = isHeader ? -1 : allRenderableRows.findIndex(r => r.id === id);
166
+ const colIndex = navigationColumns.findIndex(c => c.field === field);
167
+
168
+ if (!isHeader && rowIndex === -1) return;
169
+ if (colIndex === -1) return;
170
+
171
+ const findNextCell = (
172
+ startRow: number,
173
+ startCol: number,
174
+ deltaRow: number,
175
+ deltaCol: number,
176
+ wrapRow: boolean,
177
+ allowHeader = false
178
+ ) => {
179
+ let r = startRow + deltaRow;
180
+ let c = startCol + deltaCol;
181
+
182
+ if (wrapRow && deltaCol !== 0) {
183
+ if (deltaCol > 0 && c >= navigationColumns.length) { c = 0; r++; }
184
+ else if (deltaCol < 0 && c < 0) { c = navigationColumns.length - 1; r--; }
185
+ }
186
+
187
+ if (!wrapRow && (c < 0 || c >= navigationColumns.length)) return null;
188
+ if (r < (allowHeader ? -1 : 0) || r >= allRenderableRows.length) return null;
189
+
190
+ return { r, c };
191
+ };
192
+
193
+ const findNextEditable = (
194
+ startRow: number,
195
+ startCol: number,
196
+ deltaRow: number,
197
+ deltaCol: number,
198
+ wrapRow: boolean,
199
+ allowHeader = false
200
+ ) => {
201
+ let r = startRow;
202
+ let c = startCol;
203
+ let steps = 0;
204
+ const maxSteps = allRenderableRows.length * navigationColumns.length + navigationColumns.length;
205
+
206
+ while (steps < maxSteps) {
207
+ steps++;
208
+ r += deltaRow;
209
+ c += deltaCol;
210
+
211
+ if (wrapRow && deltaCol !== 0) {
212
+ if (deltaCol > 0) {
213
+ if (c >= navigationColumns.length) { c = 0; r++; }
214
+ } else {
215
+ if (c < 0) { c = navigationColumns.length - 1; r--; }
216
+ }
217
+ } else {
218
+ if (c < 0 || c >= navigationColumns.length) return null;
219
+ }
220
+
221
+ if (r < -1 || r >= allRenderableRows.length) return null;
222
+
223
+ if (r === -1) {
224
+ if (!allowHeader) continue;
225
+ const col = navigationColumns[c];
226
+ const isSortable = col.sortable !== false;
227
+ if (['__checkbox_col__'].includes(col.field) || isSortable) return { r, c };
228
+ continue;
229
+ }
230
+
231
+ const row = allRenderableRows[r];
232
+ const col = navigationColumns[c];
233
+ const isInteractable = ['__checkbox_col__', '__expand_col__', '__reorder_col__'].includes(col.field);
234
+ let cellEditable = col.editable || isInteractable;
235
+
236
+ if (isCellEditable && !isInteractable) {
237
+ try {
238
+ cellEditable = isCellEditable({
239
+ row,
240
+ field: col.field,
241
+ value: (row as R & Record<string, unknown>)[col.field],
242
+ colDef: col as GridColDef<R>,
243
+ rowIndex: r,
244
+ colIndex: c
245
+ });
246
+ } catch {
247
+ // isCellEditable threw — treat cell as non-editable
248
+ }
249
+ }
250
+
251
+ if (cellEditable) return { r, c };
252
+ }
253
+ return null;
254
+ };
255
+
256
+ let nextRowIndex = rowIndex;
257
+ let nextColIndex = colIndex;
258
+ let handled = false;
259
+
260
+ if (event.key === 'Tab') {
261
+ const dir = event.shiftKey ? -1 : 1;
262
+ const res = findNextEditable(rowIndex, colIndex, 0, dir, true, false);
263
+ if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
264
+ } else if (event.key === 'ArrowRight') {
265
+ const res = findNextCell(rowIndex, colIndex, 0, 1, true, true);
266
+ if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
267
+ } else if (event.key === 'ArrowLeft') {
268
+ const res = findNextCell(rowIndex, colIndex, 0, -1, true, true);
269
+ if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
270
+ } else if (event.key === 'ArrowDown') {
271
+ const res = findNextCell(rowIndex, colIndex, 1, 0, false, true);
272
+ if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
273
+ } else if (event.key === 'ArrowUp') {
274
+ const res = findNextCell(rowIndex, colIndex, -1, 0, false, true);
275
+ if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
276
+ } else if (event.key === 'Home') {
277
+ nextColIndex = 0;
278
+ if (event.ctrlKey || event.metaKey) nextRowIndex = -1;
279
+ handled = true;
280
+ } else if (event.key === 'End') {
281
+ nextColIndex = navigationColumns.length - 1;
282
+ if (event.ctrlKey || event.metaKey) nextRowIndex = allRenderableRows.length - 1;
283
+ handled = true;
284
+ } else if (event.key === 'PageUp') {
285
+ const pg = pagination ? pageSize : 10;
286
+ nextRowIndex = Math.max(-1, nextRowIndex - pg);
287
+ handled = true;
288
+ } else if (event.key === 'PageDown') {
289
+ const pg = pagination ? pageSize : 10;
290
+ nextRowIndex = Math.min(allRenderableRows.length - 1, nextRowIndex + pg);
291
+ handled = true;
292
+ }
293
+
294
+ if (handled) {
295
+ event.preventDefault();
296
+
297
+ if (isEditing && event.key === 'Tab') {
298
+ editingHandlers.stopCellEdit();
299
+ }
300
+
301
+ if (nextRowIndex >= -1 && nextRowIndex < allRenderableRows.length &&
302
+ nextColIndex >= 0 && nextColIndex < navigationColumns.length) {
303
+
304
+ const nextCol = navigationColumns[nextColIndex];
305
+
306
+ if (nextRowIndex === -1) {
307
+ setFocusedCell({ id: 'HEADER', field: nextCol.field });
308
+ } else {
309
+ setFocusedCell({ id: allRenderableRows[nextRowIndex].id, field: nextCol.field });
310
+ }
311
+
312
+ const el = viewportRef.current;
313
+ if (el) {
314
+ const { cumulativeHeights, pinnedTopHeight, columnMetrics } = virtualization;
315
+ const { clientHeight, clientWidth, scrollTop, scrollLeft } = el;
316
+
317
+ const rowTop = nextRowIndex <= 0 ? 0 : cumulativeHeights[nextRowIndex - 1];
318
+ const rowBottom = cumulativeHeights[nextRowIndex];
319
+ const visibleTop = scrollTop + pinnedTopHeight;
320
+ const visibleBottom = scrollTop + clientHeight;
321
+
322
+ let newScrollTop = scrollTop;
323
+ if (rowTop < visibleTop) {
324
+ newScrollTop = Math.max(0, rowTop - pinnedTopHeight);
325
+ } else if (rowBottom > visibleBottom) {
326
+ newScrollTop = rowBottom - clientHeight;
327
+ }
328
+ if (newScrollTop !== scrollTop) el.scrollTop = newScrollTop;
329
+
330
+ if (columnMetrics) {
331
+ const { leftPinnedWidth, rightPinnedWidth, unpinnedAccWidths, unpinnedCols, totalSpecialsWidth, pinnedSpecialsWidth } = columnMetrics;
332
+ const unpinnedIndex = unpinnedCols.findIndex(c => c.field === nextCol.field);
333
+
334
+ if (unpinnedIndex !== -1) {
335
+ const colLeft = totalSpecialsWidth + leftPinnedWidth + (unpinnedIndex > 0 ? unpinnedAccWidths[unpinnedIndex - 1] : 0);
336
+ const colRight = totalSpecialsWidth + leftPinnedWidth + unpinnedAccWidths[unpinnedIndex];
337
+ const visibleStart = scrollLeft + pinnedSpecialsWidth + leftPinnedWidth;
338
+ const visibleEnd = scrollLeft + clientWidth - rightPinnedWidth;
339
+
340
+ let newScrollLeft = scrollLeft;
341
+ if (colLeft < visibleStart) {
342
+ newScrollLeft = Math.max(0, colLeft - pinnedSpecialsWidth - leftPinnedWidth);
343
+ } else if (colRight > visibleEnd) {
344
+ newScrollLeft = colRight - clientWidth + rightPinnedWidth;
345
+ }
346
+ if (newScrollLeft !== scrollLeft) el.scrollLeft = newScrollLeft;
347
+ }
348
+ }
349
+ }
350
+ }
351
+ }
352
+ }, [
353
+ focusedCell,
354
+ allRenderableRows,
355
+ navigationColumns,
356
+ editingHandlers,
357
+ virtualization,
358
+ selectedRowIds,
359
+ handleSelectionChange,
360
+ handleDetailPanelToggle,
361
+ setKeyboardMode,
362
+ sortModel,
363
+ handleSort,
364
+ isCellEditable,
365
+ pagination,
366
+ pageSize,
367
+ viewportRef,
368
+ ]);
369
+
370
+ return { focusedCell, setFocusedCell, handleFocus, handleBlur, handleKeyDown };
371
+ }
@@ -0,0 +1,109 @@
1
+ import { useMemo } from 'react';
2
+ import { filterRows } from '../../utils/filtering';
3
+ import { sortRows } from '../../utils/sorting';
4
+ import { getPinnedRowGroups } from '../../utils/pinning';
5
+ import type { GridRowModel, GridFilterModel, GridSortItem, GridPaginationModel, GridRowPinning, GridDataSource } from '../../types';
6
+
7
+ interface HierarchyHandlers<R extends GridRowModel> {
8
+ getVisibleRows: () => R[];
9
+ }
10
+
11
+ export interface UseGridRowPipelineParams<R extends GridRowModel> {
12
+ effectiveRows: R[];
13
+ activeHierarchyHandlers: HierarchyHandlers<R> | null;
14
+ filterMode: string;
15
+ filterModel: GridFilterModel;
16
+ dataSource?: GridDataSource<R>;
17
+ sortModel: GridSortItem[];
18
+ sortingMode: string;
19
+ pagination: boolean;
20
+ paginationMode: string;
21
+ effectivePaginationModel: GridPaginationModel;
22
+ pinnedRows?: GridRowPinning;
23
+ isLoading: boolean;
24
+ pageSize: number;
25
+ }
26
+
27
+ export interface GridRowPipelineResult<R extends GridRowModel> {
28
+ filteredRows: R[];
29
+ pinnedTopRows: R[];
30
+ unpinnedRows: R[];
31
+ pinnedBottomRows: R[];
32
+ sortedUnpinnedRows: R[];
33
+ paginatedUnpinnedRows: R[];
34
+ allRenderableRows: R[];
35
+ }
36
+
37
+ export function useGridRowPipeline<R extends GridRowModel>(
38
+ params: UseGridRowPipelineParams<R>
39
+ ): GridRowPipelineResult<R> {
40
+ const {
41
+ effectiveRows,
42
+ activeHierarchyHandlers,
43
+ filterMode,
44
+ filterModel,
45
+ dataSource,
46
+ sortModel,
47
+ sortingMode,
48
+ pagination,
49
+ paginationMode,
50
+ effectivePaginationModel,
51
+ pinnedRows,
52
+ isLoading,
53
+ pageSize,
54
+ } = params;
55
+
56
+ const filteredRows = useMemo<R[]>(() => {
57
+ if (activeHierarchyHandlers) return (activeHierarchyHandlers.getVisibleRows() || []) as R[];
58
+ if (filterMode === 'server' && dataSource) return effectiveRows;
59
+ return filterRows(effectiveRows, filterModel) as R[];
60
+ }, [effectiveRows, filterModel, activeHierarchyHandlers, filterMode, dataSource]);
61
+
62
+ const { top: pinnedTopRows, center: unpinnedRows, bottom: pinnedBottomRows } = useMemo(() => {
63
+ if (activeHierarchyHandlers) return { top: [] as R[], center: filteredRows, bottom: [] as R[] };
64
+ return getPinnedRowGroups(filteredRows, pinnedRows);
65
+ }, [filteredRows, pinnedRows, activeHierarchyHandlers]);
66
+
67
+ const sortedUnpinnedRows = useMemo<R[]>(() => {
68
+ if (activeHierarchyHandlers) return unpinnedRows;
69
+ if (sortingMode === 'server' && dataSource) return unpinnedRows;
70
+ return sortRows(unpinnedRows, sortModel) as R[];
71
+ }, [unpinnedRows, sortModel, activeHierarchyHandlers, sortingMode, dataSource]);
72
+
73
+ const paginatedUnpinnedRows = useMemo<R[]>(() => {
74
+ if (!pagination) return sortedUnpinnedRows;
75
+ if (paginationMode === 'server' && dataSource) return sortedUnpinnedRows;
76
+ const start = effectivePaginationModel.page * effectivePaginationModel.pageSize;
77
+ return sortedUnpinnedRows.slice(start, start + effectivePaginationModel.pageSize);
78
+ }, [sortedUnpinnedRows, pagination, effectivePaginationModel.page, effectivePaginationModel.pageSize, paginationMode, dataSource]);
79
+
80
+ const allRenderableRows = useMemo<R[]>(() => {
81
+ if (activeHierarchyHandlers) return unpinnedRows;
82
+ const centerRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
83
+ const base = [...pinnedTopRows, ...centerRows, ...pinnedBottomRows];
84
+
85
+ if (paginationMode === 'infinite' && isLoading && base.length > 0) {
86
+ const skeletonCount = Math.min(pageSize, 20);
87
+ const skeletons = Array.from({ length: skeletonCount }, (_, i) => ({
88
+ id: `__skeleton_${i}__`,
89
+ _isSkeleton: true,
90
+ }));
91
+ return [...base, ...skeletons] as R[];
92
+ }
93
+
94
+ return base;
95
+ }, [
96
+ pinnedTopRows, paginatedUnpinnedRows, sortedUnpinnedRows, pinnedBottomRows,
97
+ pagination, activeHierarchyHandlers, unpinnedRows, paginationMode, isLoading, pageSize,
98
+ ]);
99
+
100
+ return {
101
+ filteredRows,
102
+ pinnedTopRows,
103
+ unpinnedRows,
104
+ pinnedBottomRows,
105
+ sortedUnpinnedRows,
106
+ paginatedUnpinnedRows,
107
+ allRenderableRows,
108
+ };
109
+ }
@@ -0,0 +1,52 @@
1
+ import { useRef, useState, useCallback, useEffect } from 'react';
2
+ import type { GridRowScrollEndParams } from '../../types';
3
+
4
+ export interface UseGridScrollSyncParams {
5
+ onRowsScrollEnd?: (params: GridRowScrollEndParams) => void;
6
+ }
7
+
8
+ export interface UseGridScrollSyncResult {
9
+ scrollTop: number;
10
+ scrollLeft: number;
11
+ handleScroll: (event: React.UIEvent<HTMLDivElement>) => void;
12
+ }
13
+
14
+ export function useGridScrollSync(params: UseGridScrollSyncParams): UseGridScrollSyncResult {
15
+ const { onRowsScrollEnd } = params;
16
+
17
+ const pendingScrollRef = useRef({ scrollTop: 0, scrollLeft: 0 });
18
+ const scrollRafRef = useRef<number | null>(null);
19
+ const [scrollPos, setScrollPos] = useState({ scrollTop: 0, scrollLeft: 0 });
20
+
21
+ useEffect(() => {
22
+ return () => {
23
+ if (scrollRafRef.current !== null) cancelAnimationFrame(scrollRafRef.current);
24
+ };
25
+ }, []);
26
+
27
+ const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
28
+ const target = event.currentTarget;
29
+ pendingScrollRef.current = { scrollTop: target.scrollTop, scrollLeft: target.scrollLeft };
30
+
31
+ if (scrollRafRef.current !== null) cancelAnimationFrame(scrollRafRef.current);
32
+ scrollRafRef.current = requestAnimationFrame(() => {
33
+ scrollRafRef.current = null;
34
+ setScrollPos(pendingScrollRef.current);
35
+ });
36
+
37
+ if (onRowsScrollEnd) {
38
+ const { scrollTop, scrollHeight, clientHeight } = target;
39
+ const scrollThreshold = 100;
40
+
41
+ if (scrollHeight - scrollTop - clientHeight < scrollThreshold) {
42
+ onRowsScrollEnd({
43
+ visibleTop: scrollTop,
44
+ visibleBottom: scrollTop + clientHeight,
45
+ viewportHeight: clientHeight,
46
+ });
47
+ }
48
+ }
49
+ }, [onRowsScrollEnd]);
50
+
51
+ return { scrollTop: scrollPos.scrollTop, scrollLeft: scrollPos.scrollLeft, handleScroll };
52
+ }
@@ -0,0 +1,63 @@
1
+ import { useRef, useEffect, useLayoutEffect } from 'react';
2
+ import type { GridState } from '../../state/types';
3
+ import type {
4
+ GridSortItem,
5
+ GridFilterModel,
6
+ GridPaginationModel,
7
+ GridColumnPinning,
8
+ } from '../../types';
9
+
10
+ export interface UseGridStateSnapshotParams {
11
+ onStateChange?: (state: GridState) => void;
12
+ sortModel: GridSortItem[];
13
+ filterModel: GridFilterModel;
14
+ effectivePaginationModel: GridPaginationModel;
15
+ columnWidths: Record<string, number>;
16
+ effectiveColumnOrder: string[];
17
+ columnVisibilityModel: Record<string, boolean>;
18
+ pinnedColumns: GridColumnPinning;
19
+ }
20
+
21
+ export function useGridStateSnapshot(params: UseGridStateSnapshotParams): void {
22
+ const {
23
+ onStateChange,
24
+ sortModel,
25
+ filterModel,
26
+ effectivePaginationModel,
27
+ columnWidths,
28
+ effectiveColumnOrder,
29
+ columnVisibilityModel,
30
+ pinnedColumns,
31
+ } = params;
32
+
33
+ const onStateChangeRef = useRef(onStateChange);
34
+ useLayoutEffect(() => {
35
+ onStateChangeRef.current = onStateChange;
36
+ });
37
+
38
+ useEffect(() => {
39
+ if (!onStateChangeRef.current) return;
40
+
41
+ const snapshot: GridState = {
42
+ sorting: { sortModel: sortModel as { field: string; sort: 'asc' | 'desc' }[] },
43
+ filter: { filterModel },
44
+ pagination: { paginationModel: effectivePaginationModel },
45
+ columns: {
46
+ columnWidths,
47
+ columnOrder: effectiveColumnOrder,
48
+ columnVisibilityModel,
49
+ pinnedColumns,
50
+ },
51
+ };
52
+
53
+ onStateChangeRef.current(snapshot);
54
+ }, [
55
+ sortModel,
56
+ filterModel,
57
+ effectivePaginationModel,
58
+ columnWidths,
59
+ effectiveColumnOrder,
60
+ columnVisibilityModel,
61
+ pinnedColumns,
62
+ ]);
63
+ }