@opencorestack/opengridx 0.1.8 → 0.1.9
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 +16 -0
- package/dist/opengridx.es.js +3485 -3370
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +11 -11
- package/dist/opengridx.umd.js.map +1 -1
- package/lib/components/Cell/Cell.tsx +53 -55
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +58 -0
- package/lib/components/DataGrid/DataGrid.tsx +65 -641
- package/lib/components/Row/Row.tsx +39 -38
- package/lib/components/Toolbar/GlobalSearch.tsx +1 -1
- package/lib/components/Toolbar/GridToolbar.tsx +20 -5
- package/lib/components/Toolbar/PivotPanel.tsx +7 -2
- package/lib/components/ui/DragHandleIcon.tsx +1 -1
- package/lib/hooks/core/useDataGrid.ts +2 -2
- package/lib/hooks/core/useGridKeyboardNavigation.ts +371 -0
- package/lib/hooks/core/useLayout.ts +354 -0
- package/lib/hooks/features/useAggregation.test.ts +135 -0
- package/lib/hooks/features/useGridClipboard.ts +1 -1
- package/lib/hooks/features/useGridDataSource.ts +1 -1
- package/lib/state/useGridStateStorage.ts +1 -1
- package/lib/utils/export/index.ts +1 -1
- package/lib/utils/filtering/filtering.test.ts +178 -0
- package/lib/utils/sorting/sorting.test.ts +88 -0
- package/package.json +12 -5
- package/lib/utils/virtualization/index.ts +0 -106
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React, { useRef, useEffect, useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { useLayout } from '../../hooks/core/useLayout';
|
|
3
|
+
import { useGridKeyboardNavigation } from '../../hooks/core/useGridKeyboardNavigation';
|
|
2
4
|
import ReactDOM from 'react-dom';
|
|
3
5
|
import { ListViewRow } from '../ListView/ListViewRow';
|
|
4
6
|
import type { GridState } from '../../state/types';
|
|
@@ -19,11 +21,10 @@ import { usePivot } from '../../hooks/features/usePivot';
|
|
|
19
21
|
import { useGridClipboard } from '../../hooks/features/useGridClipboard';
|
|
20
22
|
import { ExpandIcon } from '../ui/ExpandIcon';
|
|
21
23
|
import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibilityPanel';
|
|
22
|
-
import { getPinnedRowGroups, isRowPinned
|
|
24
|
+
import { getPinnedRowGroups, isRowPinned } from '../../utils/pinning';
|
|
23
25
|
import { sortRows } from '../../utils/sorting';
|
|
24
26
|
import { filterRows } from '../../utils/filtering';
|
|
25
27
|
import type { DataGridProps, GridRowModel, GridRowId, GridSortDirection, GridColumnOrderChangeParams, GridColDef, GridRenderCellParams, GridRowParams, GridCellParams, GridPaginationModel, GridDataSource, GridAggregationResult, GridAggregationModel, GridPivotModel, GridColumnPinning, GridFilterModel } from '../../types';
|
|
26
|
-
import '../../styles/opengridx.css';
|
|
27
28
|
|
|
28
29
|
export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridProps<R>) {
|
|
29
30
|
const {
|
|
@@ -229,8 +230,6 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
229
230
|
const defaultGetRowId = useCallback((row: R) => row.id, []);
|
|
230
231
|
const effectiveGetRowId = getRowId || defaultGetRowId;
|
|
231
232
|
|
|
232
|
-
const [focusedCell, setFocusedCell] = useState<{ id: GridRowId; field: string } | null>(null);
|
|
233
|
-
|
|
234
233
|
// Keyboard-mode flag: toggled via DOM classname — no React state needed
|
|
235
234
|
// so the ring appears instantly without a re-render cycle.
|
|
236
235
|
const setKeyboardMode = useCallback((on: boolean) => {
|
|
@@ -724,325 +723,19 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
724
723
|
gridData.apiRef.current.getAggregationModel = () => hasAggregation ? aggregationModel : null;
|
|
725
724
|
}, [aggregationResult, aggregationModel, hasAggregation, gridData.apiRef]);
|
|
726
725
|
|
|
727
|
-
const layout =
|
|
728
|
-
const baseRowHeight = rowHeight;
|
|
729
|
-
|
|
730
|
-
const unpinnedRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
|
|
731
|
-
const rowHeights = unpinnedRows.map((row, index) => {
|
|
732
|
-
let height = baseRowHeight;
|
|
733
|
-
if (expandedRowIds.has(row.id)) {
|
|
734
|
-
const detailHeight = getDetailPanelHeight?.({ row, id: row.id, rowIndex: pinnedTopRows.length + index }) ?? 200;
|
|
735
|
-
height += typeof detailHeight === 'number' ? detailHeight : parseInt(String(detailHeight), 10) || 200;
|
|
736
|
-
}
|
|
737
|
-
return height;
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
const cumulativeHeights = rowHeights.reduce((acc, height, index) => {
|
|
741
|
-
acc.push((acc[index - 1] || 0) + height);
|
|
742
|
-
return acc;
|
|
743
|
-
}, [] as number[]);
|
|
744
|
-
|
|
745
|
-
const skeletonCount = (paginationMode === 'infinite' && state.dataSource.loading && (unpinnedRows.length > 0))
|
|
746
|
-
? Math.min(effectivePaginationModel.pageSize, 20)
|
|
747
|
-
: 0;
|
|
748
|
-
const skeletonRowsHeight = skeletonCount * baseRowHeight;
|
|
749
|
-
const unpinnedRowsHeight = (cumulativeHeights[cumulativeHeights.length - 1] || 0) + skeletonRowsHeight;
|
|
750
|
-
const pinnedTopHeight = pinnedTopRows.length * baseRowHeight;
|
|
751
|
-
const pinnedBottomHeight = pinnedBottomRows.length * baseRowHeight;
|
|
752
|
-
|
|
753
|
-
const rawLeftPinnedCols: GridColDef<R>[] = [];
|
|
754
|
-
const rawRightPinnedCols: GridColDef<R>[] = [];
|
|
755
|
-
const unpinnedCols: GridColDef<R>[] = [];
|
|
756
|
-
const unpinnedColWidths: number[] = [];
|
|
757
|
-
|
|
758
|
-
visibleOrderedColumns.forEach(col => {
|
|
759
|
-
const pinned = isColumnPinned(col.field, pinnedColumns);
|
|
760
|
-
if (pinned === 'left') {
|
|
761
|
-
rawLeftPinnedCols.push(col);
|
|
762
|
-
} else if (pinned === 'right') {
|
|
763
|
-
rawRightPinnedCols.push(col);
|
|
764
|
-
} else {
|
|
765
|
-
unpinnedCols.push(col);
|
|
766
|
-
}
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
const viewportWidth = state.dimensions.viewportWidth || 1000;
|
|
770
|
-
const systemColumnsWidth = (checkboxSelection ? 48 : 0) + (hasDetailPanel ? 48 : 0) + (rowReordering ? 48 : 0);
|
|
771
|
-
|
|
772
|
-
const getColWidth = (c: GridColDef<R>) => columnWidths[c.field] ?? c.width;
|
|
773
|
-
|
|
774
|
-
const parseWidth = (width: number | string | undefined): { type: 'fixed' | 'percentage' | 'auto', value: number } => {
|
|
775
|
-
if (width === undefined) return { type: 'auto', value: 0 };
|
|
776
|
-
if (typeof width === 'number') return { type: 'fixed', value: width };
|
|
777
|
-
if (typeof width === 'string') {
|
|
778
|
-
if (width.toLowerCase() === 'auto') return { type: 'auto', value: 0 };
|
|
779
|
-
if (width.endsWith('%')) {
|
|
780
|
-
const percentage = parseFloat(width);
|
|
781
|
-
return { type: 'percentage', value: percentage };
|
|
782
|
-
}
|
|
783
|
-
const floatVal = parseFloat(width);
|
|
784
|
-
if (!isNaN(floatVal)) return { type: 'fixed', value: floatVal };
|
|
785
|
-
}
|
|
786
|
-
return { type: 'fixed', value: 100 };
|
|
787
|
-
};
|
|
788
|
-
|
|
789
|
-
const leftWidth = rawLeftPinnedCols.reduce((sum, c) => {
|
|
790
|
-
const parsed = parseWidth(getColWidth(c));
|
|
791
|
-
return sum + (parsed.type === 'fixed' ? parsed.value : 100);
|
|
792
|
-
}, 0);
|
|
793
|
-
const rightWidth = rawRightPinnedCols.reduce((sum, c) => {
|
|
794
|
-
const parsed = parseWidth(getColWidth(c));
|
|
795
|
-
return sum + (parsed.type === 'fixed' ? parsed.value : 100);
|
|
796
|
-
}, 0);
|
|
797
|
-
|
|
798
|
-
const naturalFlexWidth = unpinnedCols.reduce((sum, col) => {
|
|
799
|
-
const hasManualWidth = Object.prototype.hasOwnProperty.call(columnWidths, col.field);
|
|
800
|
-
if (!hasManualWidth && col.flex && col.flex > 0) {
|
|
801
|
-
const minWidth = col.minWidth ?? 150;
|
|
802
|
-
const width = typeof col.width === 'number' ? col.width : 150;
|
|
803
|
-
return sum + (minWidth || width);
|
|
804
|
-
}
|
|
805
|
-
const parsed = parseWidth(getColWidth(col));
|
|
806
|
-
if (parsed.type === 'fixed') return sum + parsed.value;
|
|
807
|
-
if (parsed.type === 'percentage') return sum + 100;
|
|
808
|
-
return sum + (col.minWidth ?? 150);
|
|
809
|
-
}, 0);
|
|
810
|
-
|
|
811
|
-
const availableWidth = Math.max(
|
|
812
|
-
viewportWidth - systemColumnsWidth - leftWidth - rightWidth,
|
|
813
|
-
naturalFlexWidth
|
|
814
|
-
);
|
|
815
|
-
|
|
816
|
-
const fixedWidthCols: Array<{ col: GridColDef<R>, width: number }> = [];
|
|
817
|
-
const percentageCols: Array<{ col: GridColDef<R>, percentage: number }> = [];
|
|
818
|
-
const flexCols: Array<{ col: GridColDef<R>, flex: number }> = [];
|
|
819
|
-
let totalFlexUnits = 0;
|
|
820
|
-
|
|
821
|
-
unpinnedCols.forEach(col => {
|
|
822
|
-
// If the user has manually resized this column, columnWidths has an explicit
|
|
823
|
-
// entry for it. Respect that stored width as fixed-width so the resize sticks,
|
|
824
|
-
// even if the column definition originally had flex.
|
|
825
|
-
const hasManualWidth = Object.prototype.hasOwnProperty.call(columnWidths, col.field);
|
|
826
|
-
|
|
827
|
-
const parsed = parseWidth(getColWidth(col));
|
|
828
|
-
|
|
829
|
-
if (!hasManualWidth && col.flex && col.flex > 0) {
|
|
830
|
-
flexCols.push({ col, flex: col.flex });
|
|
831
|
-
totalFlexUnits += col.flex;
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
if (parsed.type === 'fixed') {
|
|
836
|
-
fixedWidthCols.push({ col, width: parsed.value });
|
|
837
|
-
} else if (parsed.type === 'percentage') {
|
|
838
|
-
percentageCols.push({ col, percentage: parsed.value });
|
|
839
|
-
} else {
|
|
840
|
-
|
|
841
|
-
const implicitFlex = 1;
|
|
842
|
-
flexCols.push({ col, flex: implicitFlex });
|
|
843
|
-
totalFlexUnits += implicitFlex;
|
|
844
|
-
}
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
const fixedWidth = fixedWidthCols.reduce((sum, { width }) => sum + width, 0);
|
|
848
|
-
|
|
849
|
-
let remainingSpaceForFlex = Math.max(0, availableWidth - fixedWidth);
|
|
850
|
-
|
|
851
|
-
const percentageWidthMap = new Map<string, number>();
|
|
852
|
-
percentageCols.forEach(({ col, percentage }) => {
|
|
853
|
-
const calculatedWidth = (percentage / 100) * remainingSpaceForFlex;
|
|
854
|
-
percentageWidthMap.set(col.field, calculatedWidth);
|
|
855
|
-
|
|
856
|
-
remainingSpaceForFlex -= calculatedWidth;
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
remainingSpaceForFlex = Math.max(0, remainingSpaceForFlex);
|
|
860
|
-
|
|
861
|
-
const finalFlexWidths = new Map<string, number>();
|
|
862
|
-
|
|
863
|
-
const flexItems = flexCols.map(f => ({
|
|
864
|
-
...f,
|
|
865
|
-
minWidth: f.col.minWidth ?? 50,
|
|
866
|
-
maxWidth: f.col.maxWidth ?? Infinity,
|
|
867
|
-
frozen: false,
|
|
868
|
-
computedWidth: 0
|
|
869
|
-
}));
|
|
870
|
-
|
|
871
|
-
const solveFlexAllocation = () => {
|
|
872
|
-
|
|
873
|
-
let iterations = 0;
|
|
874
|
-
const maxIterations = flexItems.length * 2;
|
|
875
|
-
|
|
876
|
-
while (iterations < maxIterations) {
|
|
877
|
-
iterations++;
|
|
878
|
-
|
|
879
|
-
const unfrozen = flexItems.filter(f => !f.frozen);
|
|
880
|
-
|
|
881
|
-
if (unfrozen.length === 0) break;
|
|
882
|
-
|
|
883
|
-
const unfrozenFlexTotal = unfrozen.reduce((sum, f) => sum + f.flex, 0);
|
|
884
|
-
|
|
885
|
-
const frozenWidthTotal = flexItems.reduce((sum, f) => f.frozen ? sum + f.computedWidth : sum, 0);
|
|
886
|
-
const currentFreeSpace = Math.max(0, remainingSpaceForFlex - frozenWidthTotal);
|
|
887
|
-
|
|
888
|
-
if (unfrozenFlexTotal <= 0) {
|
|
889
|
-
unfrozen.forEach(f => {
|
|
890
|
-
f.computedWidth = f.minWidth;
|
|
891
|
-
f.frozen = true;
|
|
892
|
-
});
|
|
893
|
-
break;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
const pixelsPerFlex = currentFreeSpace / unfrozenFlexTotal;
|
|
897
|
-
|
|
898
|
-
let totalViolation = 0;
|
|
899
|
-
const minViolators: typeof flexItems = [];
|
|
900
|
-
const maxViolators: typeof flexItems = [];
|
|
901
|
-
|
|
902
|
-
unfrozen.forEach(f => {
|
|
903
|
-
const rawWidth = pixelsPerFlex * f.flex;
|
|
904
|
-
|
|
905
|
-
if (rawWidth < f.minWidth) {
|
|
906
|
-
const diff = f.minWidth - rawWidth;
|
|
907
|
-
totalViolation += diff;
|
|
908
|
-
minViolators.push(f);
|
|
909
|
-
} else if (rawWidth > f.maxWidth) {
|
|
910
|
-
const diff = f.maxWidth - rawWidth;
|
|
911
|
-
totalViolation += diff;
|
|
912
|
-
maxViolators.push(f);
|
|
913
|
-
} else {
|
|
914
|
-
|
|
915
|
-
f.computedWidth = rawWidth;
|
|
916
|
-
}
|
|
917
|
-
});
|
|
918
|
-
|
|
919
|
-
if (minViolators.length === 0 && maxViolators.length === 0) {
|
|
920
|
-
unfrozen.forEach(f => f.frozen = true);
|
|
921
|
-
break;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (totalViolation > 0) {
|
|
925
|
-
minViolators.forEach(f => {
|
|
926
|
-
f.computedWidth = f.minWidth;
|
|
927
|
-
f.frozen = true;
|
|
928
|
-
});
|
|
929
|
-
} else if (totalViolation < 0) {
|
|
930
|
-
maxViolators.forEach(f => {
|
|
931
|
-
f.computedWidth = f.maxWidth;
|
|
932
|
-
f.frozen = true;
|
|
933
|
-
});
|
|
934
|
-
} else {
|
|
935
|
-
|
|
936
|
-
minViolators.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
|
|
937
|
-
maxViolators.forEach(f => { f.computedWidth = f.maxWidth; f.frozen = true; });
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
};
|
|
941
|
-
|
|
942
|
-
solveFlexAllocation();
|
|
943
|
-
flexItems.forEach(f => finalFlexWidths.set(f.col.field, f.computedWidth));
|
|
944
|
-
|
|
945
|
-
const computedWidthMap = new Map<string, number>();
|
|
946
|
-
|
|
947
|
-
unpinnedCols.forEach(col => {
|
|
948
|
-
let computedWidth: number;
|
|
949
|
-
|
|
950
|
-
if (finalFlexWidths.has(col.field)) {
|
|
951
|
-
computedWidth = finalFlexWidths.get(col.field)!;
|
|
952
|
-
} else {
|
|
953
|
-
const fixedCol = fixedWidthCols.find(f => f.col.field === col.field);
|
|
954
|
-
const pctCol = percentageCols.find(p => p.col.field === col.field);
|
|
955
|
-
|
|
956
|
-
if (fixedCol) {
|
|
957
|
-
computedWidth = fixedCol.width;
|
|
958
|
-
} else if (pctCol) {
|
|
959
|
-
const rawPctWidth = percentageWidthMap.get(col.field) || 0;
|
|
960
|
-
computedWidth = Math.max(col.minWidth ?? 50, rawPctWidth || 100);
|
|
961
|
-
} else {
|
|
962
|
-
computedWidth = 100;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
unpinnedColWidths.push(computedWidth);
|
|
967
|
-
computedWidthMap.set(col.field, computedWidth);
|
|
968
|
-
});
|
|
969
|
-
|
|
970
|
-
const unpinnedColsWithWidth = unpinnedCols.map(col => {
|
|
971
|
-
const flexItem = flexItems.find(f => f.col.field === col.field);
|
|
972
|
-
return {
|
|
973
|
-
...col,
|
|
974
|
-
width: computedWidthMap.get(col.field) ?? (typeof col.width === 'number' ? col.width : 100),
|
|
975
|
-
flex: flexItem ? flexItem.flex : col.flex
|
|
976
|
-
};
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
const baseLeftZ = 11;
|
|
980
|
-
const leftPinnedCols = rawLeftPinnedCols.map((col, i) => {
|
|
981
|
-
const parsed = parseWidth(getColWidth(col));
|
|
982
|
-
const numericWidth = parsed.type === 'fixed' ? parsed.value : 100;
|
|
983
|
-
return {
|
|
984
|
-
...col,
|
|
985
|
-
width: numericWidth,
|
|
986
|
-
zIndex: baseLeftZ + i
|
|
987
|
-
};
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
const baseRightZ = 11;
|
|
991
|
-
const rightPinnedCols = rawRightPinnedCols.map((col, i) => {
|
|
992
|
-
const parsed = parseWidth(getColWidth(col));
|
|
993
|
-
const numericWidth = parsed.type === 'fixed' ? parsed.value : 100;
|
|
994
|
-
return {
|
|
995
|
-
...col,
|
|
996
|
-
width: numericWidth,
|
|
997
|
-
zIndex: baseRightZ + (rawRightPinnedCols.length - 1 - i)
|
|
998
|
-
};
|
|
999
|
-
});
|
|
1000
|
-
|
|
1001
|
-
const unpinnedTotalWidth = unpinnedColWidths.reduce((sum, w) => sum + w, 0);
|
|
1002
|
-
const totalWidth = leftWidth + unpinnedTotalWidth + rightWidth + systemColumnsWidth;
|
|
1003
|
-
|
|
1004
|
-
const unpinnedAccWidths = unpinnedColWidths.reduce((acc, w, i) => {
|
|
1005
|
-
acc.push((acc[i - 1] || 0) + w);
|
|
1006
|
-
return acc;
|
|
1007
|
-
}, [] as number[]);
|
|
1008
|
-
|
|
1009
|
-
return {
|
|
1010
|
-
rowHeights,
|
|
1011
|
-
cumulativeHeights,
|
|
1012
|
-
unpinnedRowsHeight,
|
|
1013
|
-
pinnedTopHeight,
|
|
1014
|
-
pinnedBottomHeight,
|
|
1015
|
-
leftPinnedCols,
|
|
1016
|
-
rightPinnedCols,
|
|
1017
|
-
unpinnedColsWithWidth,
|
|
1018
|
-
unpinnedColWidths,
|
|
1019
|
-
unpinnedAccWidths,
|
|
1020
|
-
totalWidth,
|
|
1021
|
-
leftWidth,
|
|
1022
|
-
rightWidth,
|
|
1023
|
-
unpinnedTotalWidth,
|
|
1024
|
-
systemColumnsWidth,
|
|
1025
|
-
pinnedTopRowsLength: pinnedTopRows.length,
|
|
1026
|
-
pinnedBottomRowsLength: pinnedBottomRows.length,
|
|
1027
|
-
unpinnedRowsLength: unpinnedRows.length
|
|
1028
|
-
};
|
|
1029
|
-
}, [
|
|
726
|
+
const layout = useLayout({
|
|
1030
727
|
rowHeight,
|
|
1031
728
|
pagination,
|
|
1032
729
|
paginatedUnpinnedRows,
|
|
1033
730
|
sortedUnpinnedRows,
|
|
1034
731
|
expandedRowIds,
|
|
1035
732
|
getDetailPanelHeight,
|
|
1036
|
-
pinnedTopRows.length,
|
|
1037
|
-
pinnedBottomRows.length,
|
|
1038
|
-
expandedRowIds,
|
|
1039
|
-
getDetailPanelHeight,
|
|
1040
|
-
pinnedTopRows.length,
|
|
1041
|
-
pinnedBottomRows.length,
|
|
733
|
+
pinnedTopRowsLength: pinnedTopRows.length,
|
|
734
|
+
pinnedBottomRowsLength: pinnedBottomRows.length,
|
|
1042
735
|
visibleOrderedColumns,
|
|
1043
736
|
pinnedColumns,
|
|
1044
737
|
columnWidths,
|
|
1045
|
-
state.dimensions.viewportWidth,
|
|
738
|
+
viewportWidth: state.dimensions.viewportWidth || 1000,
|
|
1046
739
|
checkboxSelection,
|
|
1047
740
|
hasDetailPanel,
|
|
1048
741
|
rowReordering,
|
|
@@ -1050,9 +743,45 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1050
743
|
pinExpandColumn,
|
|
1051
744
|
autoHeight,
|
|
1052
745
|
paginationMode,
|
|
1053
|
-
state.dataSource.loading,
|
|
1054
|
-
effectivePaginationModel.pageSize
|
|
1055
|
-
|
|
746
|
+
isLoading: state.dataSource.loading,
|
|
747
|
+
pageSize: effectivePaginationModel.pageSize,
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
useEffect(() => {
|
|
751
|
+
gridData.apiRef.current.scrollToIndexes = ({ rowIndex, colIndex }) => {
|
|
752
|
+
const el = viewportRef.current;
|
|
753
|
+
if (!el) return;
|
|
754
|
+
|
|
755
|
+
if (rowIndex !== undefined && rowIndex >= 0) {
|
|
756
|
+
const rowTop = rowIndex > 0 ? layout.cumulativeHeights[rowIndex - 1] : 0;
|
|
757
|
+
const rowBottom = layout.cumulativeHeights[rowIndex] ?? rowTop;
|
|
758
|
+
const { scrollTop, clientHeight } = el;
|
|
759
|
+
if (rowTop < scrollTop) {
|
|
760
|
+
el.scrollTop = rowTop;
|
|
761
|
+
} else if (rowBottom > scrollTop + clientHeight) {
|
|
762
|
+
el.scrollTop = rowBottom - clientHeight;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (colIndex !== undefined && colIndex >= 0) {
|
|
767
|
+
// colIndex is an index into all data columns: leftPinned + unpinned + rightPinned
|
|
768
|
+
const allDataCols = [...layout.leftPinnedCols, ...layout.unpinnedColsWithWidth, ...layout.rightPinnedCols];
|
|
769
|
+
const targetCol = allDataCols[colIndex];
|
|
770
|
+
if (!targetCol) return;
|
|
771
|
+
const unpinnedIndex = layout.unpinnedColsWithWidth.findIndex(c => c.field === targetCol.field);
|
|
772
|
+
if (unpinnedIndex === -1) return; // pinned column — always visible, no scroll needed
|
|
773
|
+
const colLocalLeft = unpinnedIndex > 0 ? layout.unpinnedAccWidths[unpinnedIndex - 1] : 0;
|
|
774
|
+
const colLocalRight = layout.unpinnedAccWidths[unpinnedIndex] ?? colLocalLeft;
|
|
775
|
+
const colRight = layout.leftWidth + colLocalRight;
|
|
776
|
+
const { scrollLeft, clientWidth } = el;
|
|
777
|
+
if (colLocalLeft < scrollLeft) {
|
|
778
|
+
el.scrollLeft = colLocalLeft;
|
|
779
|
+
} else if (colRight > scrollLeft + clientWidth) {
|
|
780
|
+
el.scrollLeft = colRight - clientWidth;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}, [layout, viewportRef, gridData.apiRef]);
|
|
1056
785
|
|
|
1057
786
|
const virtualization = useMemo(() => {
|
|
1058
787
|
const {
|
|
@@ -1250,13 +979,31 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1250
979
|
onSortModelChange?.(newSortModel as any);
|
|
1251
980
|
}, [isSortControlled, onSortModelChange]);
|
|
1252
981
|
|
|
982
|
+
const { focusedCell, setFocusedCell, handleFocus, handleBlur, handleKeyDown } = useGridKeyboardNavigation({
|
|
983
|
+
allRenderableRows,
|
|
984
|
+
navigationColumns,
|
|
985
|
+
checkboxSelection,
|
|
986
|
+
selectedRowIds,
|
|
987
|
+
handleSelectionChange,
|
|
988
|
+
handleDetailPanelToggle,
|
|
989
|
+
editingHandlers,
|
|
990
|
+
setKeyboardMode,
|
|
991
|
+
sortModel,
|
|
992
|
+
handleSort,
|
|
993
|
+
isCellEditable,
|
|
994
|
+
pagination,
|
|
995
|
+
pageSize: effectivePaginationModel.pageSize,
|
|
996
|
+
virtualization,
|
|
997
|
+
viewportRef,
|
|
998
|
+
});
|
|
999
|
+
|
|
1253
1000
|
const handleCellClick = useCallback((params: GridCellParams<R>) => {
|
|
1254
1001
|
setKeyboardMode(false);
|
|
1255
1002
|
setFocusedCell({ id: params.row.id, field: params.field });
|
|
1256
1003
|
|
|
1257
1004
|
gridRef.current?.focus({ preventScroll: true });
|
|
1258
1005
|
onCellClick?.(params);
|
|
1259
|
-
}, [onCellClick, setKeyboardMode]);
|
|
1006
|
+
}, [onCellClick, setKeyboardMode, setFocusedCell]);
|
|
1260
1007
|
|
|
1261
1008
|
const prevEditingCellRef = useRef(editingHandlers.editingCell);
|
|
1262
1009
|
useEffect(() => {
|
|
@@ -1314,329 +1061,6 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1314
1061
|
const allSelected = rows.length > 0 && selectedRowIds.size === rows.length;
|
|
1315
1062
|
const someSelected = selectedRowIds.size > 0 && selectedRowIds.size < rows.length;
|
|
1316
1063
|
|
|
1317
|
-
const handleFocus = useCallback((event: React.FocusEvent<HTMLDivElement>) => {
|
|
1318
|
-
|
|
1319
|
-
if (event.target === event.currentTarget) {
|
|
1320
|
-
setFocusedCell(prev => {
|
|
1321
|
-
if (prev) return prev;
|
|
1322
|
-
|
|
1323
|
-
if (checkboxSelection) {
|
|
1324
|
-
return { id: 'HEADER', field: '__checkbox_col__' };
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
const firstRow = allRenderableRows[0];
|
|
1328
|
-
const firstCol = navigationColumns[0];
|
|
1329
|
-
if (firstRow && firstCol) {
|
|
1330
|
-
return { id: firstRow.id, field: firstCol.field };
|
|
1331
|
-
}
|
|
1332
|
-
return null;
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
}, [allRenderableRows, navigationColumns]);
|
|
1336
|
-
|
|
1337
|
-
const handleBlur = useCallback((event: React.FocusEvent<HTMLDivElement>) => {
|
|
1338
|
-
|
|
1339
|
-
if (!event.currentTarget.contains(event.relatedTarget as Node)) {
|
|
1340
|
-
setFocusedCell(null);
|
|
1341
|
-
}
|
|
1342
|
-
}, []);
|
|
1343
|
-
|
|
1344
|
-
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
|
|
1345
|
-
setKeyboardMode(true);
|
|
1346
|
-
|
|
1347
|
-
if (!focusedCell) return;
|
|
1348
|
-
|
|
1349
|
-
const { id, field } = focusedCell;
|
|
1350
|
-
|
|
1351
|
-
const isEditing = Boolean(editingHandlers.editingCell);
|
|
1352
|
-
|
|
1353
|
-
if (event.key === 'Enter') {
|
|
1354
|
-
event.preventDefault();
|
|
1355
|
-
if (isEditing) {
|
|
1356
|
-
editingHandlers.stopCellEdit();
|
|
1357
|
-
} else {
|
|
1358
|
-
|
|
1359
|
-
const col = navigationColumns.find(c => c.field === field);
|
|
1360
|
-
if (col?.editable) {
|
|
1361
|
-
const row = allRenderableRows.find(r => r.id === id);
|
|
1362
|
-
if (row) {
|
|
1363
|
-
editingHandlers.startCellEdit({
|
|
1364
|
-
id,
|
|
1365
|
-
field,
|
|
1366
|
-
value: (row as any)[field]
|
|
1367
|
-
});
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
if (event.key === ' ' || event.key === 'Spacebar') {
|
|
1375
|
-
if (!isEditing) {
|
|
1376
|
-
if (field === '__checkbox_col__') {
|
|
1377
|
-
event.preventDefault();
|
|
1378
|
-
handleSelectionChange(id, !selectedRowIds.has(id));
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
if (field === '__expand_col__') {
|
|
1382
|
-
event.preventDefault();
|
|
1383
|
-
handleDetailPanelToggle(id);
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
if (id === 'HEADER' && (event.key === 'Enter' || event.key === ' ')) {
|
|
1390
|
-
event.preventDefault();
|
|
1391
|
-
|
|
1392
|
-
const col = navigationColumns.find(c => c.field === field);
|
|
1393
|
-
if (col && (col as any).sortable !== false) {
|
|
1394
|
-
const currentSort = sortModel.find(item => item.field === field);
|
|
1395
|
-
let newDirection: GridSortDirection = 'asc';
|
|
1396
|
-
|
|
1397
|
-
if (currentSort) {
|
|
1398
|
-
if (currentSort.sort === 'asc') {
|
|
1399
|
-
newDirection = 'desc';
|
|
1400
|
-
} else {
|
|
1401
|
-
newDirection = null;
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
handleSort(field, newDirection);
|
|
1405
|
-
}
|
|
1406
|
-
return;
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
if (event.key === 'Escape') {
|
|
1410
|
-
if (isEditing) {
|
|
1411
|
-
event.preventDefault();
|
|
1412
|
-
editingHandlers.stopCellEdit({ cancel: true });
|
|
1413
|
-
}
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
if (isEditing && event.key !== 'Tab') {
|
|
1418
|
-
return;
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
const isHeader = id === 'HEADER';
|
|
1422
|
-
const rowIndex = isHeader ? -1 : allRenderableRows.findIndex(r => r.id === id);
|
|
1423
|
-
const colIndex = navigationColumns.findIndex(c => c.field === field);
|
|
1424
|
-
|
|
1425
|
-
if (!isHeader && rowIndex === -1) return;
|
|
1426
|
-
if (colIndex === -1) return;
|
|
1427
|
-
|
|
1428
|
-
// Find the next FOCUSABLE cell (any cell — editability not required for arrow navigation)
|
|
1429
|
-
const findNextCell = (
|
|
1430
|
-
startRow: number,
|
|
1431
|
-
startCol: number,
|
|
1432
|
-
deltaRow: number,
|
|
1433
|
-
deltaCol: number,
|
|
1434
|
-
wrapRow: boolean,
|
|
1435
|
-
allowHeader: boolean = false
|
|
1436
|
-
) => {
|
|
1437
|
-
let r = startRow + deltaRow;
|
|
1438
|
-
let c = startCol + deltaCol;
|
|
1439
|
-
|
|
1440
|
-
if (wrapRow && deltaCol !== 0) {
|
|
1441
|
-
if (deltaCol > 0 && c >= navigationColumns.length) { c = 0; r++; }
|
|
1442
|
-
else if (deltaCol < 0 && c < 0) { c = navigationColumns.length - 1; r--; }
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
if (!wrapRow && (c < 0 || c >= navigationColumns.length)) return null;
|
|
1446
|
-
if (r < (allowHeader ? -1 : 0) || r >= allRenderableRows.length) return null;
|
|
1447
|
-
|
|
1448
|
-
return { r, c };
|
|
1449
|
-
};
|
|
1450
|
-
|
|
1451
|
-
// Find the next EDITABLE cell (for Tab key navigation)
|
|
1452
|
-
const findNextEditable = (
|
|
1453
|
-
startRow: number,
|
|
1454
|
-
startCol: number,
|
|
1455
|
-
deltaRow: number,
|
|
1456
|
-
deltaCol: number,
|
|
1457
|
-
wrapRow: boolean,
|
|
1458
|
-
allowHeader: boolean = false
|
|
1459
|
-
) => {
|
|
1460
|
-
let r = startRow;
|
|
1461
|
-
let c = startCol;
|
|
1462
|
-
let steps = 0;
|
|
1463
|
-
|
|
1464
|
-
const maxSteps = allRenderableRows.length * navigationColumns.length + navigationColumns.length;
|
|
1465
|
-
|
|
1466
|
-
while (steps < maxSteps) {
|
|
1467
|
-
steps++;
|
|
1468
|
-
|
|
1469
|
-
r += deltaRow;
|
|
1470
|
-
c += deltaCol;
|
|
1471
|
-
|
|
1472
|
-
if (wrapRow && deltaCol !== 0) {
|
|
1473
|
-
if (deltaCol > 0) {
|
|
1474
|
-
if (c >= navigationColumns.length) { c = 0; r++; }
|
|
1475
|
-
} else {
|
|
1476
|
-
if (c < 0) { c = navigationColumns.length - 1; r--; }
|
|
1477
|
-
}
|
|
1478
|
-
} else {
|
|
1479
|
-
if (c < 0 || c >= navigationColumns.length) return null;
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
if (r < -1 || r >= allRenderableRows.length) return null;
|
|
1483
|
-
|
|
1484
|
-
if (r === -1) {
|
|
1485
|
-
if (!allowHeader) continue;
|
|
1486
|
-
const col = navigationColumns[c];
|
|
1487
|
-
const isSortable = (col as any).sortable !== false;
|
|
1488
|
-
const isInteractive = ['__checkbox_col__'].includes(col.field) || isSortable;
|
|
1489
|
-
if (isInteractive) return { r, c };
|
|
1490
|
-
continue;
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
const row = allRenderableRows[r];
|
|
1494
|
-
const col = navigationColumns[c];
|
|
1495
|
-
|
|
1496
|
-
const isInteractable = ['__checkbox_col__', '__expand_col__', '__reorder_col__'].includes(col.field);
|
|
1497
|
-
let isEditable = col.editable || isInteractable;
|
|
1498
|
-
|
|
1499
|
-
if (isCellEditable && !isInteractable) {
|
|
1500
|
-
try {
|
|
1501
|
-
isEditable = isCellEditable({
|
|
1502
|
-
row,
|
|
1503
|
-
field: col.field,
|
|
1504
|
-
value: (row as any)[col.field],
|
|
1505
|
-
colDef: col,
|
|
1506
|
-
rowIndex: r,
|
|
1507
|
-
colIndex: c
|
|
1508
|
-
});
|
|
1509
|
-
} catch (e) {
|
|
1510
|
-
console.error('Error in isCellEditable:', e);
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
if (isEditable) return { r, c };
|
|
1515
|
-
}
|
|
1516
|
-
return null;
|
|
1517
|
-
};
|
|
1518
|
-
|
|
1519
|
-
let nextRowIndex = rowIndex;
|
|
1520
|
-
let nextColIndex = colIndex;
|
|
1521
|
-
let handled = false;
|
|
1522
|
-
|
|
1523
|
-
if (event.key === 'Tab') {
|
|
1524
|
-
// Tab moves to next EDITABLE cell
|
|
1525
|
-
const dir = event.shiftKey ? -1 : 1;
|
|
1526
|
-
const res = findNextEditable(rowIndex, colIndex, 0, dir, true, false);
|
|
1527
|
-
if (res) {
|
|
1528
|
-
nextRowIndex = res.r;
|
|
1529
|
-
nextColIndex = res.c;
|
|
1530
|
-
handled = true;
|
|
1531
|
-
}
|
|
1532
|
-
} else if (event.key === 'ArrowRight') {
|
|
1533
|
-
const res = findNextCell(rowIndex, colIndex, 0, 1, true, true);
|
|
1534
|
-
if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
|
|
1535
|
-
} else if (event.key === 'ArrowLeft') {
|
|
1536
|
-
const res = findNextCell(rowIndex, colIndex, 0, -1, true, true);
|
|
1537
|
-
if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
|
|
1538
|
-
} else if (event.key === 'ArrowDown') {
|
|
1539
|
-
const res = findNextCell(rowIndex, colIndex, 1, 0, false, true);
|
|
1540
|
-
if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
|
|
1541
|
-
} else if (event.key === 'ArrowUp') {
|
|
1542
|
-
const res = findNextCell(rowIndex, colIndex, -1, 0, false, true);
|
|
1543
|
-
if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
|
|
1544
|
-
} else if (event.key === 'Home') {
|
|
1545
|
-
if (event.ctrlKey || event.metaKey) {
|
|
1546
|
-
nextRowIndex = -1;
|
|
1547
|
-
nextColIndex = 0;
|
|
1548
|
-
} else {
|
|
1549
|
-
nextColIndex = 0;
|
|
1550
|
-
}
|
|
1551
|
-
handled = true;
|
|
1552
|
-
} else if (event.key === 'End') {
|
|
1553
|
-
if (event.ctrlKey || event.metaKey) {
|
|
1554
|
-
nextRowIndex = allRenderableRows.length - 1;
|
|
1555
|
-
nextColIndex = navigationColumns.length - 1;
|
|
1556
|
-
} else {
|
|
1557
|
-
nextColIndex = navigationColumns.length - 1;
|
|
1558
|
-
}
|
|
1559
|
-
handled = true;
|
|
1560
|
-
} else if (event.key === 'PageUp') {
|
|
1561
|
-
const pageSize = pagination ? effectivePaginationModel.pageSize : 10;
|
|
1562
|
-
nextRowIndex = Math.max(-1, nextRowIndex - pageSize);
|
|
1563
|
-
handled = true;
|
|
1564
|
-
} else if (event.key === 'PageDown') {
|
|
1565
|
-
const pageSize = pagination ? effectivePaginationModel.pageSize : 10;
|
|
1566
|
-
nextRowIndex = Math.min(allRenderableRows.length - 1, nextRowIndex + pageSize);
|
|
1567
|
-
handled = true;
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
if (handled) {
|
|
1571
|
-
event.preventDefault();
|
|
1572
|
-
|
|
1573
|
-
if (isEditing && event.key === 'Tab') {
|
|
1574
|
-
editingHandlers.stopCellEdit();
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
if (nextRowIndex >= -1 && nextRowIndex < allRenderableRows.length &&
|
|
1578
|
-
nextColIndex >= 0 && nextColIndex < navigationColumns.length) {
|
|
1579
|
-
|
|
1580
|
-
const nextCol = navigationColumns[nextColIndex];
|
|
1581
|
-
|
|
1582
|
-
if (nextRowIndex === -1) {
|
|
1583
|
-
setFocusedCell({ id: 'HEADER', field: nextCol.field });
|
|
1584
|
-
} else {
|
|
1585
|
-
const nextRow = allRenderableRows[nextRowIndex];
|
|
1586
|
-
setFocusedCell({ id: nextRow.id, field: nextCol.field });
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
if (viewportRef.current) {
|
|
1590
|
-
const { cumulativeHeights, pinnedTopHeight } = virtualization;
|
|
1591
|
-
const { clientHeight, clientWidth, scrollTop, scrollLeft } = viewportRef.current;
|
|
1592
|
-
|
|
1593
|
-
const rowTop = nextRowIndex === 0 ? 0 : cumulativeHeights[nextRowIndex - 1];
|
|
1594
|
-
const rowBottom = cumulativeHeights[nextRowIndex];
|
|
1595
|
-
const pinnedOffset = pinnedTopHeight;
|
|
1596
|
-
const visibleTop = scrollTop + pinnedOffset;
|
|
1597
|
-
const visibleBottom = scrollTop + clientHeight;
|
|
1598
|
-
|
|
1599
|
-
let newScrollTop = scrollTop;
|
|
1600
|
-
if (rowTop < visibleTop) {
|
|
1601
|
-
newScrollTop = Math.max(0, rowTop - pinnedOffset);
|
|
1602
|
-
} else if (rowBottom > visibleBottom) {
|
|
1603
|
-
newScrollTop = rowBottom - clientHeight;
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
if (newScrollTop !== scrollTop) {
|
|
1607
|
-
viewportRef.current.scrollTop = newScrollTop;
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
if (virtualization.columnMetrics) {
|
|
1611
|
-
const { leftPinnedWidth, rightPinnedWidth, unpinnedAccWidths, unpinnedCols, totalSpecialsWidth, pinnedSpecialsWidth } = virtualization.columnMetrics;
|
|
1612
|
-
const field = nextCol.field;
|
|
1613
|
-
const unpinnedIndex = unpinnedCols.findIndex((c: any) => c.field === field);
|
|
1614
|
-
|
|
1615
|
-
if (unpinnedIndex !== -1) {
|
|
1616
|
-
const colLeft = totalSpecialsWidth + leftPinnedWidth + (unpinnedIndex > 0 ? unpinnedAccWidths[unpinnedIndex - 1] : 0);
|
|
1617
|
-
const colRight = totalSpecialsWidth + leftPinnedWidth + unpinnedAccWidths[unpinnedIndex];
|
|
1618
|
-
|
|
1619
|
-
const visibleStart = scrollLeft + pinnedSpecialsWidth + leftPinnedWidth;
|
|
1620
|
-
const visibleEnd = scrollLeft + clientWidth - rightPinnedWidth;
|
|
1621
|
-
|
|
1622
|
-
let newScrollLeft = scrollLeft;
|
|
1623
|
-
|
|
1624
|
-
if (colLeft < visibleStart) {
|
|
1625
|
-
newScrollLeft = Math.max(0, colLeft - pinnedSpecialsWidth - leftPinnedWidth);
|
|
1626
|
-
} else if (colRight > visibleEnd) {
|
|
1627
|
-
newScrollLeft = colRight - clientWidth + rightPinnedWidth;
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
if (newScrollLeft !== scrollLeft) {
|
|
1631
|
-
viewportRef.current.scrollLeft = newScrollLeft;
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
}, [focusedCell, allRenderableRows, navigationColumns, editingHandlers, virtualization, selectedRowIds, handleSelectionChange, handleDetailPanelToggle]);
|
|
1639
|
-
|
|
1640
1064
|
const hasRowSpanning = React.useMemo(() => effectiveColumns.some(c => !!c.rowSpan), [effectiveColumns]);
|
|
1641
1065
|
const [columnsPanelOpen, setColumnsPanelOpen] = React.useState(false);
|
|
1642
1066
|
const containerRef = React.useRef<HTMLDivElement>(null);
|