@opencorestack/opengridx 0.1.9 → 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 (57) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/index.d.ts +60 -41
  3. package/dist/opengridx.es.js +5045 -4549
  4. package/dist/opengridx.es.js.map +1 -1
  5. package/dist/opengridx.umd.js +11 -11
  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 +9 -10
  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.tsx +336 -1018
  18. package/lib/components/DataGrid/GridAggregationFooter.tsx +68 -0
  19. package/lib/components/DataGrid/GridEmptyState.tsx +16 -0
  20. package/lib/components/DataGrid/GridErrorOverlay.tsx +33 -0
  21. package/lib/components/DataGrid/GridListView.tsx +116 -0
  22. package/lib/components/DataGrid/GridPinnedRows.tsx +98 -0
  23. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +124 -0
  24. package/lib/components/DataGrid/GridVirtualRows.tsx +202 -0
  25. package/lib/components/FilterPanel/FilterPanel.tsx +18 -10
  26. package/lib/components/Header/ColumnMenu.tsx +2 -2
  27. package/lib/components/Header/Header.tsx +4 -4
  28. package/lib/components/ListView/ListViewRow.tsx +2 -2
  29. package/lib/components/Row/Row.tsx +10 -10
  30. package/lib/components/SkeletonRow/SkeletonRow.tsx +9 -2
  31. package/lib/components/Toolbar/GridToolbar.tsx +6 -0
  32. package/lib/hooks/core/useDataGrid.regression.test.ts +66 -0
  33. package/lib/hooks/core/useDataGrid.ts +14 -34
  34. package/lib/hooks/core/useGridColumns.tsx +228 -0
  35. package/lib/hooks/core/useGridControlledState.ts +192 -0
  36. package/lib/hooks/core/useGridRowPipeline.ts +109 -0
  37. package/lib/hooks/core/useGridScrollSync.ts +52 -0
  38. package/lib/hooks/core/useGridStateSnapshot.ts +63 -0
  39. package/lib/hooks/core/useGridVirtualization.ts +186 -0
  40. package/lib/hooks/core/useGridVisibleRows.ts +66 -0
  41. package/lib/hooks/core/useLayout.ts +0 -6
  42. package/lib/hooks/features/useAggregation.ts +7 -7
  43. package/lib/hooks/features/useGridDataSource.ts +1 -1
  44. package/lib/hooks/features/useGridEditing.ts +31 -32
  45. package/lib/hooks/features/usePivot.test.ts +202 -0
  46. package/lib/hooks/features/usePivot.ts +0 -0
  47. package/lib/hooks/useColumnReorder.ts +1 -1
  48. package/lib/hooks/useRowGrouping.ts +14 -14
  49. package/lib/hooks/useTreeData.ts +1 -1
  50. package/lib/state/types.ts +1 -1
  51. package/lib/state/useGridStateStorage.ts +1 -1
  52. package/lib/types/index.ts +48 -32
  53. package/lib/utils/export/exportToExcelAdvanced.ts +14 -14
  54. package/lib/utils/export/index.ts +22 -17
  55. package/lib/utils/filtering/index.ts +20 -20
  56. package/lib/utils/sorting/index.ts +1 -1
  57. package/package.json +1 -1
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import { formatAggregationValue } from '../../hooks/features/useAggregation';
3
+ import type { GridColDef, GridAggregationModel, GridAggregationResult } from '../../types';
4
+
5
+ interface GridAggregationFooterProps {
6
+ columns: GridColDef[];
7
+ aggregationModel: GridAggregationModel;
8
+ aggregationResult: GridAggregationResult;
9
+ columnWidths: Record<string, number>;
10
+ rowHeight: number;
11
+ checkboxSelection: boolean;
12
+ hasDetailPanel: boolean;
13
+ rowReordering: boolean;
14
+ }
15
+
16
+ export function GridAggregationFooter({
17
+ columns,
18
+ aggregationModel,
19
+ aggregationResult,
20
+ columnWidths,
21
+ rowHeight,
22
+ checkboxSelection,
23
+ hasDetailPanel,
24
+ rowReordering,
25
+ }: GridAggregationFooterProps) {
26
+ return (
27
+ <div
28
+ className="ogx__aggregation-footer"
29
+ role="row"
30
+ aria-label="Aggregation totals"
31
+ aria-live="polite"
32
+ style={{ minHeight: `${rowHeight}px` }}
33
+ >
34
+ {checkboxSelection && <div style={{ width: 48, flexShrink: 0 }} />}
35
+ {hasDetailPanel && <div style={{ width: 48, flexShrink: 0 }} />}
36
+ {rowReordering && <div style={{ width: 48, flexShrink: 0 }} />}
37
+
38
+ {columns.map((col) => {
39
+ const fnName = (aggregationModel as Record<string, string>)[col.field];
40
+ const rawValue = aggregationResult[col.field];
41
+ const colWidth = columnWidths[col.field] ?? (typeof col.width === 'number' ? col.width : 120);
42
+
43
+ return (
44
+ <div
45
+ key={col.field}
46
+ className="ogx__aggregation-cell"
47
+ role="gridcell"
48
+ style={{
49
+ width: colWidth,
50
+ minWidth: colWidth,
51
+ maxWidth: colWidth,
52
+ textAlign: (col.align as React.CSSProperties['textAlign']) || 'left',
53
+ }}
54
+ >
55
+ {fnName ? (
56
+ <>
57
+ <span className="ogx__aggregation-label">{fnName}</span>
58
+ <span className="ogx__aggregation-value">
59
+ {formatAggregationValue(rawValue, fnName)}
60
+ </span>
61
+ </>
62
+ ) : null}
63
+ </div>
64
+ );
65
+ })}
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,16 @@
1
+ interface GridEmptyStateProps {
2
+ noRowsLabel: string;
3
+ width: number;
4
+ }
5
+
6
+ export function GridEmptyState({ noRowsLabel, width }: GridEmptyStateProps) {
7
+ return (
8
+ <div className="ogx__empty" style={{ width }}>
9
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
10
+ <rect x="3" y="3" width="18" height="18" rx="2" />
11
+ <path d="M3 9h18M9 21V9" />
12
+ </svg>
13
+ <span>{noRowsLabel}</span>
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,33 @@
1
+ interface GridErrorOverlayProps {
2
+ error: unknown;
3
+ }
4
+
5
+ export function GridErrorOverlay({ error }: GridErrorOverlayProps) {
6
+ if (!error) return null;
7
+ const message = error instanceof Error
8
+ ? error.message
9
+ : typeof error === 'string'
10
+ ? error
11
+ : 'An unexpected error occurred while loading the data.';
12
+ return (
13
+ <div className="ogx-error-overlay" aria-live="assertive" role="alert">
14
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="var(--ogx-color-error)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
15
+ <circle cx="12" cy="12" r="10" />
16
+ <line x1="12" y1="8" x2="12" y2="12" />
17
+ <line x1="12" y1="16" x2="12.01" y2="16" />
18
+ </svg>
19
+ <div className="ogx-error-overlay__title">
20
+ Oops! Something went wrong
21
+ </div>
22
+ <div className="ogx-error-overlay__message">
23
+ {message}
24
+ </div>
25
+ <button
26
+ onClick={() => window.location.reload()}
27
+ className="ogx-button ogx-button--primary"
28
+ >
29
+ Retry
30
+ </button>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,116 @@
1
+ import React from 'react';
2
+ import { Pagination } from '../Pagination/Pagination';
3
+ import { ListViewRow } from '../ListView/ListViewRow';
4
+ import type {
5
+ GridRowModel,
6
+ GridRowId,
7
+ GridRowParams,
8
+ GridListViewColDef,
9
+ GridPaginationModel,
10
+ GridDataSource,
11
+ } from '../../types';
12
+
13
+ export interface GridListViewProps<R extends GridRowModel> {
14
+ ariaLabel?: string;
15
+ allRenderableRows: R[];
16
+ filteredRows: R[];
17
+ pagination: boolean;
18
+ effectivePaginationModel: GridPaginationModel;
19
+ pageSizeOptions: number[];
20
+ selectedRowIds: Set<GridRowId>;
21
+ listViewColumn: GridListViewColDef<R>;
22
+ noRowsLabel: string;
23
+ rowHeight: number;
24
+ checkboxSelection: boolean;
25
+ paginationMode: 'client' | 'server' | 'infinite';
26
+ dataSource?: GridDataSource<R>;
27
+ serverRowCount: number;
28
+ paginationSlot?: React.ComponentType<Record<string, unknown>>;
29
+ paginationSlotProps?: Record<string, unknown>;
30
+ onRowClick: (params: GridRowParams<R>) => void;
31
+ onSelectionChange: (rowId: GridRowId, isSelected: boolean) => void;
32
+ onPaginationModelChange: (model: GridPaginationModel) => void;
33
+ }
34
+
35
+ export function GridListView<R extends GridRowModel>({
36
+ ariaLabel,
37
+ allRenderableRows,
38
+ filteredRows,
39
+ pagination,
40
+ effectivePaginationModel,
41
+ pageSizeOptions,
42
+ selectedRowIds,
43
+ listViewColumn,
44
+ noRowsLabel,
45
+ rowHeight,
46
+ checkboxSelection,
47
+ paginationMode,
48
+ dataSource,
49
+ serverRowCount,
50
+ paginationSlot,
51
+ paginationSlotProps,
52
+ onRowClick,
53
+ onSelectionChange,
54
+ onPaginationModelChange,
55
+ }: GridListViewProps<R>) {
56
+ const PaginationComponent = paginationSlot || Pagination;
57
+
58
+ const totalRowCount = (paginationMode === 'server' && dataSource)
59
+ ? (serverRowCount || 0)
60
+ : filteredRows.length;
61
+
62
+ return (
63
+ <div
64
+ className="ogx-list-view"
65
+ role="grid"
66
+ aria-label={ariaLabel || 'Data grid list view'}
67
+ aria-rowcount={allRenderableRows.length + 1}
68
+ >
69
+ <div className="ogx-list-view__toolbar">
70
+ <span>
71
+ {filteredRows.length} {filteredRows.length === 1 ? 'item' : 'items'}
72
+ {pagination ? ` · page ${effectivePaginationModel.page + 1} of ${Math.ceil(filteredRows.length / effectivePaginationModel.pageSize) || 1}` : ''}
73
+ {selectedRowIds.size > 0 ? ` · ${selectedRowIds.size} selected` : ''}
74
+ </span>
75
+ </div>
76
+
77
+ <div className="ogx-list-view__rows">
78
+ {allRenderableRows.length === 0 ? (
79
+ <div className="ogx-list-view__empty" aria-live="polite" role="status">
80
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
81
+ <rect x="3" y="3" width="18" height="18" rx="2" />
82
+ <path d="M3 9h18M9 21V9" />
83
+ </svg>
84
+ {noRowsLabel}
85
+ </div>
86
+ ) : (
87
+ allRenderableRows.map((row, idx) => (
88
+ <ListViewRow<R>
89
+ key={row.id}
90
+ row={row}
91
+ rowIndex={idx}
92
+ listViewColumn={listViewColumn}
93
+ isSelected={selectedRowIds.has(row.id)}
94
+ checkboxSelection={checkboxSelection}
95
+ rowHeight={rowHeight}
96
+ onRowClick={(r) => onRowClick({ row: r, id: r.id, rowIndex: idx })}
97
+ onSelectionChange={onSelectionChange}
98
+ />
99
+ ))
100
+ )}
101
+ </div>
102
+
103
+ {pagination && (
104
+ <PaginationComponent
105
+ page={effectivePaginationModel.page}
106
+ pageSize={effectivePaginationModel.pageSize}
107
+ rowCount={totalRowCount}
108
+ pageSizeOptions={pageSizeOptions}
109
+ onPageChange={(newPage: number) => onPaginationModelChange({ ...effectivePaginationModel, page: newPage })}
110
+ onPageSizeChange={(newPageSize: number) => onPaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 })}
111
+ {...paginationSlotProps}
112
+ />
113
+ )}
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,98 @@
1
+ import React from 'react';
2
+ import { Row } from '../Row/Row';
3
+ import type {
4
+ GridRowModel,
5
+ GridRowId,
6
+ GridColDef,
7
+ GridRowParams,
8
+ GridCellParams,
9
+ GridColumnPinning,
10
+ GridRowPinning,
11
+ GridDetailPanelParams,
12
+ GridDetailPanelHeight,
13
+ } from '../../types';
14
+ import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
15
+
16
+ export interface GridPinnedRowsProps<R extends GridRowModel> {
17
+ rows: R[];
18
+ position: 'top' | 'bottom';
19
+ columns: GridColDef<R>[];
20
+ selectedRowIds: Set<GridRowId>;
21
+ checkboxSelection: boolean;
22
+ onRowClick: (params: GridRowParams<R>) => void;
23
+ onCellClick: (params: GridCellParams<R>) => void;
24
+ onSelectionChange: (rowId: GridRowId, isSelected: boolean) => void;
25
+ columnWidths: Record<string, number>;
26
+ pinnedColumns: GridColumnPinning;
27
+ pinnedRows?: GridRowPinning;
28
+ hasDetailPanel: boolean;
29
+ expandedRowIds: Set<GridRowId>;
30
+ getDetailPanelContent?: (params: GridDetailPanelParams<R>) => React.ReactNode;
31
+ getDetailPanelHeight?: (params: GridDetailPanelParams<R>) => GridDetailPanelHeight;
32
+ onDetailPanelToggle: (rowId: GridRowId) => void;
33
+ pinCheckboxColumn?: boolean;
34
+ pinExpandColumn?: boolean;
35
+ focusedCell: { id: GridRowId; field: string } | null;
36
+ colspanMap?: Map<GridRowId, Record<string, CellColSpanInfo>>;
37
+ rowSpanningCaches?: RowSpanningCaches;
38
+ rowHeight: number;
39
+ }
40
+
41
+ export function GridPinnedRows<R extends GridRowModel>({
42
+ rows,
43
+ position,
44
+ columns,
45
+ selectedRowIds,
46
+ checkboxSelection,
47
+ onRowClick,
48
+ onCellClick,
49
+ onSelectionChange,
50
+ columnWidths,
51
+ pinnedColumns,
52
+ pinnedRows,
53
+ hasDetailPanel,
54
+ expandedRowIds,
55
+ getDetailPanelContent,
56
+ getDetailPanelHeight,
57
+ onDetailPanelToggle,
58
+ pinCheckboxColumn,
59
+ pinExpandColumn,
60
+ focusedCell,
61
+ colspanMap,
62
+ rowSpanningCaches,
63
+ rowHeight,
64
+ }: GridPinnedRowsProps<R>) {
65
+ if (rows.length === 0) return null;
66
+
67
+ return (
68
+ <div className={`ogx__pinned-rows ogx__pinned-rows--${position}`} role="rowgroup">
69
+ {rows.map((row, index) => (
70
+ <Row<R>
71
+ key={row.id}
72
+ row={row}
73
+ columns={columns}
74
+ rowIndex={index}
75
+ isSelected={selectedRowIds.has(row.id)}
76
+ checkboxSelection={checkboxSelection}
77
+ onRowClick={onRowClick}
78
+ onCellClick={onCellClick}
79
+ onSelectionChange={onSelectionChange}
80
+ columnWidths={columnWidths}
81
+ pinnedColumns={pinnedColumns}
82
+ pinnedRows={pinnedRows}
83
+ hasDetailPanel={hasDetailPanel}
84
+ isDetailPanelExpanded={expandedRowIds.has(row.id)}
85
+ detailPanelContent={getDetailPanelContent ? getDetailPanelContent({ row, id: row.id, rowIndex: index }) : null}
86
+ detailPanelHeight={getDetailPanelHeight?.({ row, id: row.id, rowIndex: index }) || 200}
87
+ onDetailPanelToggle={onDetailPanelToggle}
88
+ pinCheckboxColumn={pinCheckboxColumn}
89
+ pinExpandColumn={pinExpandColumn}
90
+ focusedCellField={focusedCell?.id === row.id ? focusedCell.field : null}
91
+ colspanMap={colspanMap}
92
+ rowSpanningCaches={rowSpanningCaches}
93
+ rowHeight={rowHeight}
94
+ />
95
+ ))}
96
+ </div>
97
+ );
98
+ }
@@ -0,0 +1,124 @@
1
+ import React, { useState, useLayoutEffect } from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibilityPanel';
4
+ import type { GridColDef, GridRowModel, GridColumnOrderChangeParams } from '../../types';
5
+
6
+ export interface GridStandaloneColumnPanelProps<R extends GridRowModel> {
7
+ isOpen: boolean;
8
+ containerRef: React.RefObject<HTMLDivElement | null>;
9
+ effectiveColumns: GridColDef<R>[];
10
+ columnVisibilityModel: Record<string, boolean>;
11
+ effectiveColumnOrder: string[];
12
+ columnOrder?: string[];
13
+ disableColumnReorder: boolean;
14
+ onClose: () => void;
15
+ onColumnVisibilityChange: (model: Record<string, boolean>) => void;
16
+ onColumnOrderChange?: (params: GridColumnOrderChangeParams) => void;
17
+ setInternalColumnOrder: React.Dispatch<React.SetStateAction<string[]>>;
18
+ }
19
+
20
+ export function GridStandaloneColumnPanel<R extends GridRowModel>({
21
+ isOpen,
22
+ containerRef,
23
+ effectiveColumns,
24
+ columnVisibilityModel,
25
+ effectiveColumnOrder,
26
+ columnOrder,
27
+ disableColumnReorder,
28
+ onClose,
29
+ onColumnVisibilityChange,
30
+ onColumnOrderChange,
31
+ setInternalColumnOrder,
32
+ }: GridStandaloneColumnPanelProps<R>) {
33
+ const [panelTop, setPanelTop] = useState<number>(16);
34
+ const [panelRight, setPanelRight] = useState<number>(16);
35
+ const [portalTarget, setPortalTarget] = useState<Element>(() => document.body);
36
+
37
+ useLayoutEffect(() => {
38
+ if (!isOpen || !containerRef.current) return;
39
+ const rect = containerRef.current.getBoundingClientRect();
40
+ setPanelTop(rect.top + 8);
41
+ setPanelRight(window.innerWidth - rect.right + 8);
42
+ setPortalTarget(containerRef.current.closest('.ogx-theme-provider') || document.body);
43
+ }, [isOpen, containerRef]);
44
+
45
+ if (!isOpen) return null;
46
+
47
+ const handleReorder = disableColumnReorder
48
+ ? undefined
49
+ : (fromField: string, toField: string) => {
50
+ const currentOrder = [...effectiveColumnOrder];
51
+ const fromIdx = currentOrder.indexOf(fromField);
52
+ const toIdx = currentOrder.indexOf(toField);
53
+ if (fromIdx === -1 || toIdx === -1) return;
54
+ const newOrder = [...currentOrder];
55
+ newOrder.splice(fromIdx, 1);
56
+ newOrder.splice(toIdx, 0, fromField);
57
+ if (!columnOrder) setInternalColumnOrder(newOrder);
58
+ onColumnOrderChange?.({
59
+ oldIndex: fromIdx,
60
+ targetIndex: toIdx,
61
+ column: effectiveColumns.find(c => c.field === fromField) as unknown as GridColDef,
62
+ });
63
+ };
64
+
65
+ return ReactDOM.createPortal(
66
+ <div
67
+ id="ogx-standalone-col-panel"
68
+ style={{
69
+ position: 'fixed',
70
+ top: panelTop,
71
+ right: panelRight,
72
+ zIndex: 9999,
73
+ display: 'inline-block',
74
+ }}
75
+ >
76
+ <button
77
+ onClick={onClose}
78
+ style={{
79
+ position: 'absolute',
80
+ top: -12,
81
+ right: -12,
82
+ width: 24,
83
+ height: 24,
84
+ display: 'flex',
85
+ alignItems: 'center',
86
+ justifyContent: 'center',
87
+ background: '#334155',
88
+ color: '#fff',
89
+ border: 'none',
90
+ borderRadius: '50%',
91
+ cursor: 'pointer',
92
+ fontSize: 14,
93
+ lineHeight: 1,
94
+ zIndex: 1,
95
+ boxShadow: '0 1px 4px rgba(0,0,0,0.2)',
96
+ }}
97
+ aria-label="Close"
98
+ >×</button>
99
+ <ColumnVisibilityPanel<R>
100
+ columns={effectiveColumns}
101
+ visibleColumns={new Set(
102
+ effectiveColumns
103
+ .filter(col => columnVisibilityModel[col.field] !== false)
104
+ .map(col => col.field)
105
+ )}
106
+ onVisibilityChange={(field, isVisible) => {
107
+ onColumnVisibilityChange({ ...columnVisibilityModel, [field]: isVisible });
108
+ }}
109
+ onShowAll={() => {
110
+ const next = { ...columnVisibilityModel };
111
+ effectiveColumns.forEach(col => { if (col.hideable !== false) next[col.field] = true; });
112
+ onColumnVisibilityChange(next);
113
+ }}
114
+ onHideAll={() => {
115
+ const next = { ...columnVisibilityModel };
116
+ effectiveColumns.forEach(col => { if (col.hideable !== false) next[col.field] = false; });
117
+ onColumnVisibilityChange(next);
118
+ }}
119
+ onColumnReorder={handleReorder}
120
+ />
121
+ </div>,
122
+ portalTarget
123
+ );
124
+ }
@@ -0,0 +1,202 @@
1
+ import React from 'react';
2
+ import { Row } from '../Row/Row';
3
+ import { SkeletonRow } from '../SkeletonRow';
4
+ import { isRowPinned } from '../../utils/pinning';
5
+ import type {
6
+ GridRowModel,
7
+ GridRowId,
8
+ GridColDef,
9
+ GridRowParams,
10
+ GridCellParams,
11
+ GridColumnPinning,
12
+ GridRowPinning,
13
+ GridDetailPanelParams,
14
+ GridDetailPanelHeight,
15
+ } from '../../types';
16
+ import type { GridVisibleRow } from '../../hooks/core/useGridVisibleRows';
17
+ import type { UseRowReorderReturn } from '../../hooks/useRowReorder';
18
+ import type { GridEditingState } from '../../hooks/features/useGridEditing';
19
+ import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
20
+
21
+ interface RowEditingHandlers {
22
+ editingCell: GridEditingState['editingCell'];
23
+ startCellEdit: (params: { id: GridRowId; field: string; value: unknown }) => void;
24
+ stopCellEdit: (params?: { cancel?: boolean }) => void;
25
+ setEditCellValue: (params: { id: GridRowId; field: string; value: unknown }) => void;
26
+ }
27
+
28
+ export interface GridVirtualRowsProps<R extends GridRowModel> {
29
+ virtualContainerHeight: number;
30
+ offsetTop: number;
31
+ effectiveLoading: boolean;
32
+ visibleRows: GridVisibleRow<R>[];
33
+ baseColumns: GridColDef<R>[];
34
+ virtualColumns: GridColDef<R>[];
35
+ viewportWidth: number;
36
+ checkboxSelection: boolean;
37
+ hasDetailPanel: boolean;
38
+ rowReordering: boolean;
39
+ rowHeight: number;
40
+ pinnedRows?: GridRowPinning;
41
+ selectedRowIds: Set<GridRowId>;
42
+ onRowClick: (params: GridRowParams<R>) => void;
43
+ onCellClick: (params: GridCellParams<R>) => void;
44
+ onSelectionChange: (rowId: GridRowId, isSelected: boolean) => void;
45
+ columnWidths: Record<string, number>;
46
+ pinnedColumns: GridColumnPinning;
47
+ expandedRowIds: Set<GridRowId>;
48
+ getDetailPanelContent?: (params: GridDetailPanelParams<R>) => React.ReactNode;
49
+ getDetailPanelHeight?: (params: GridDetailPanelParams<R>) => GridDetailPanelHeight;
50
+ onDetailPanelToggle: (rowId: GridRowId) => void;
51
+ pinCheckboxColumn?: boolean;
52
+ pinExpandColumn?: boolean;
53
+ rowReorderHandlers: UseRowReorderReturn;
54
+ editingHandlers: RowEditingHandlers;
55
+ focusedCell: { id: GridRowId; field: string } | null;
56
+ colspanMap?: Map<GridRowId, Record<string, CellColSpanInfo>>;
57
+ rowSpanningCaches?: RowSpanningCaches;
58
+ paginationMode: 'client' | 'server' | 'infinite';
59
+ dataSourceLoading: boolean;
60
+ sortedUnpinnedRowCount: number;
61
+ infiniteScrollSkeletonCount: number;
62
+ unpinnedRowsLength: number;
63
+ }
64
+
65
+ export function GridVirtualRows<R extends GridRowModel>({
66
+ virtualContainerHeight,
67
+ offsetTop,
68
+ effectiveLoading,
69
+ visibleRows,
70
+ baseColumns,
71
+ virtualColumns,
72
+ viewportWidth,
73
+ checkboxSelection,
74
+ hasDetailPanel,
75
+ rowReordering,
76
+ rowHeight,
77
+ pinnedRows,
78
+ selectedRowIds,
79
+ onRowClick,
80
+ onCellClick,
81
+ onSelectionChange,
82
+ columnWidths,
83
+ pinnedColumns,
84
+ expandedRowIds,
85
+ getDetailPanelContent,
86
+ getDetailPanelHeight,
87
+ onDetailPanelToggle,
88
+ pinCheckboxColumn,
89
+ pinExpandColumn,
90
+ rowReorderHandlers,
91
+ editingHandlers,
92
+ focusedCell,
93
+ colspanMap,
94
+ rowSpanningCaches,
95
+ paginationMode,
96
+ dataSourceLoading,
97
+ sortedUnpinnedRowCount,
98
+ infiniteScrollSkeletonCount,
99
+ unpinnedRowsLength,
100
+ }: GridVirtualRowsProps<R>) {
101
+ const skeletonColumns: GridColDef<R>[] = baseColumns.length > 0
102
+ ? baseColumns
103
+ : (() => {
104
+ const availableWidth = (viewportWidth || 1000) -
105
+ (checkboxSelection ? 48 : 0) -
106
+ (hasDetailPanel ? 48 : 0) -
107
+ (rowReordering ? 48 : 0);
108
+ const columnWidth = 150;
109
+ const columnCount = Math.max(1, Math.ceil(availableWidth / columnWidth));
110
+ return Array.from({ length: columnCount }, (_, i) => ({
111
+ field: `placeholder_${i}`,
112
+ headerName: '',
113
+ width: columnWidth,
114
+ })) as GridColDef<R>[];
115
+ })();
116
+
117
+ const centerRows = visibleRows
118
+ .filter(({ row }) => !isRowPinned(row.id, pinnedRows))
119
+ .filter((item, index, self) =>
120
+ index === self.findIndex(t => t.row.id === item.row.id)
121
+ );
122
+
123
+ return (
124
+ <div
125
+ className="ogx__virtual-container"
126
+ style={{ height: `${virtualContainerHeight}px` }}
127
+ role="presentation"
128
+ >
129
+ <div
130
+ className="ogx__rows"
131
+ style={{ transform: `translateY(${offsetTop}px)` }}
132
+ role="rowgroup"
133
+ >
134
+ {effectiveLoading && visibleRows.length === 0 ? (
135
+ Array.from({ length: 10 }).map((_, index) => (
136
+ <SkeletonRow
137
+ key={`skeleton-${index}`}
138
+ columns={skeletonColumns as unknown as GridColDef[]}
139
+ rowHeight={rowHeight}
140
+ checkboxSelection={checkboxSelection}
141
+ hasDetailPanel={hasDetailPanel}
142
+ rowReordering={rowReordering}
143
+ />
144
+ ))
145
+ ) : (
146
+ centerRows.map(({ row, rowIndex: actualIndex }) => (
147
+ <Row<R>
148
+ key={row.id}
149
+ row={row}
150
+ columns={virtualColumns}
151
+ rowIndex={actualIndex}
152
+ isSelected={selectedRowIds.has(row.id)}
153
+ checkboxSelection={checkboxSelection}
154
+ onRowClick={onRowClick}
155
+ onCellClick={onCellClick}
156
+ onSelectionChange={onSelectionChange}
157
+ columnWidths={columnWidths}
158
+ pinnedColumns={pinnedColumns}
159
+ pinnedRows={pinnedRows}
160
+ hasDetailPanel={hasDetailPanel}
161
+ isDetailPanelExpanded={expandedRowIds.has(row.id)}
162
+ detailPanelContent={getDetailPanelContent ? getDetailPanelContent({ row, id: row.id, rowIndex: actualIndex }) : null}
163
+ detailPanelHeight={getDetailPanelHeight?.({ row, id: row.id, rowIndex: actualIndex }) || 200}
164
+ onDetailPanelToggle={onDetailPanelToggle}
165
+ pinCheckboxColumn={pinCheckboxColumn}
166
+ pinExpandColumn={pinExpandColumn}
167
+ rowReordering={rowReordering}
168
+ onDragStart={rowReorderHandlers.onDragStart}
169
+ onDragOver={rowReorderHandlers.onDragOver}
170
+ onDragEnd={rowReorderHandlers.onDragEnd}
171
+ onDrop={rowReorderHandlers.onDrop}
172
+ isDragging={rowReorderHandlers.draggedRowId === row.id}
173
+ isDragOver={rowReorderHandlers.dragOverRowId === row.id}
174
+ editingCell={editingHandlers.editingCell}
175
+ onEditStart={editingHandlers.startCellEdit}
176
+ onEditStop={editingHandlers.stopCellEdit}
177
+ onEditCellValueChange={editingHandlers.setEditCellValue}
178
+ focusedCellField={focusedCell != null && focusedCell.id === row.id ? focusedCell.field : null}
179
+ colspanMap={colspanMap}
180
+ rowSpanningCaches={rowSpanningCaches}
181
+ rowHeight={rowHeight}
182
+ />
183
+ ))
184
+ )}
185
+
186
+ {paginationMode === 'infinite' && dataSourceLoading && sortedUnpinnedRowCount > 0 && (
187
+ <div className="ogx__skeleton-group">
188
+ {Array.from({ length: infiniteScrollSkeletonCount }).map((_, i) => (
189
+ <Row<R>
190
+ key={`__skeleton_${i}__`}
191
+ row={{ id: `__skeleton_${i}__`, _isSkeleton: true } as unknown as R}
192
+ columns={virtualColumns}
193
+ rowIndex={unpinnedRowsLength + i}
194
+ rowHeight={rowHeight}
195
+ />
196
+ ))}
197
+ </div>
198
+ )}
199
+ </div>
200
+ </div>
201
+ );
202
+ }