@@ -81,6 +81,8 @@ import {
81
81
} from "../internal/data-grid/event-args.js" ;
82
82
import { type Keybinds , useKeybindingsWithDefaults } from "./data-editor-keybindings.js" ;
83
83
import type { Highlight } from "../internal/data-grid/render/data-grid-render.cells.js" ;
84
+ import { useRowGroupingInner , type RowGroupingOptions } from "./row-grouping.js" ;
85
+ import { useRowGrouping } from "./row-grouping-api.js" ;
84
86
85
87
const DataGridOverlayEditor = React . lazy (
86
88
async ( ) => await import ( "../internal/data-grid-overlay-editor/data-grid-overlay-editor.js" )
@@ -121,6 +123,7 @@ type Props = Partial<
121
123
| "getCellContent"
122
124
| "getCellRenderer"
123
125
| "getCellsForSelection"
126
+ | "getRowThemeOverride"
124
127
| "gridRef"
125
128
| "groupHeaderHeight"
126
129
| "headerHeight"
@@ -443,6 +446,15 @@ export interface DataEditorProps extends Props, Pick<DataGridSearchProps, "image
443
446
*/
444
447
readonly markdownDivCreateNode ?: ( content : string ) => DocumentFragment ;
445
448
449
+ /**
450
+ * Allows overriding the theme of any row
451
+ * @param row represents the row index of the row, increasing by 1 for every represented row. Collapsed rows are not included.
452
+ * @param groupRow represents the row index of the group row. Only distinct when row grouping enabled.
453
+ * @param contentRow represents the index of the row excluding group headers. Only distinct when row grouping enabled.
454
+ * @returns
455
+ */
456
+ readonly getRowThemeOverride ?: ( row : number , groupRow : number , contentRow : number ) => Partial < Theme > | undefined ;
457
+
446
458
/** Callback for providing a custom editor for a cell.
447
459
* @group Editing
448
460
*/
@@ -580,6 +592,11 @@ export interface DataEditorProps extends Props, Pick<DataGridSearchProps, "image
580
592
*/
581
593
readonly verticalBorder ?: DataGridSearchProps [ "verticalBorder" ] | boolean ;
582
594
595
+ /**
596
+ * Controls the grouping of rows to be drawn in the grid.
597
+ */
598
+ readonly rowGrouping ?: RowGroupingOptions ;
599
+
583
600
/**
584
601
* Called when data is pasted into the grid. If left undefined, the `DataEditor` will operate in a
585
602
* fallback mode and attempt to paste the text buffer into the current cell assuming the current cell is not
@@ -722,12 +739,12 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
722
739
723
740
const {
724
741
imageEditorOverride,
725
- getRowThemeOverride,
742
+ getRowThemeOverride : getRowThemeOverrideIn ,
726
743
markdownDivCreateNode,
727
744
width,
728
745
height,
729
746
columns : columnsIn ,
730
- rows,
747
+ rows : rowsIn ,
731
748
getCellContent,
732
749
onCellClicked,
733
750
onCellActivated,
@@ -778,6 +795,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
778
795
onHeaderMenuClick,
779
796
onHeaderIndicatorClick,
780
797
getGroupDetails,
798
+ rowGrouping,
781
799
onSearchClose : onSearchCloseIn ,
782
800
onItemHovered,
783
801
onSelectionCleared,
@@ -847,22 +865,28 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
847
865
return window . getComputedStyle ( document . documentElement ) ;
848
866
} , [ ] ) ;
849
867
850
- const remSize = React . useMemo ( ( ) => Number . parseFloat ( docStyle . fontSize ) , [ docStyle ] ) ;
868
+ const {
869
+ rows,
870
+ rowNumberMapper,
871
+ rowHeight : rowHeightPostGrouping ,
872
+ getRowThemeOverride,
873
+ } = useRowGroupingInner ( rowGrouping , rowsIn , rowHeightIn , getRowThemeOverrideIn ) ;
851
874
875
+ const remSize = React . useMemo ( ( ) => Number . parseFloat ( docStyle . fontSize ) , [ docStyle ] ) ;
852
876
const { rowHeight, headerHeight, groupHeaderHeight, theme, overscrollX, overscrollY } = useRemAdjuster ( {
853
877
groupHeaderHeight : groupHeaderHeightIn ,
854
878
headerHeight : headerHeightIn ,
855
879
overscrollX : overscrollXIn ,
856
880
overscrollY : overscrollYIn ,
857
881
remSize,
858
- rowHeight : rowHeightIn ,
882
+ rowHeight : rowHeightPostGrouping ,
859
883
scaleToRem,
860
884
theme : themeIn ,
861
885
} ) ;
862
886
863
887
const keybindings = useKeybindingsWithDefaults ( keybindingsIn ) ;
864
888
865
- const rowMarkerWidth = rowMarkerWidthRaw ?? ( rows > 10_000 ? 48 : rows > 1000 ? 44 : rows > 100 ? 36 : 32 ) ;
889
+ const rowMarkerWidth = rowMarkerWidthRaw ?? ( rowsIn > 10_000 ? 48 : rowsIn > 1000 ? 44 : rowsIn > 100 ? 36 : 32 ) ;
866
890
const hasRowMarkers = rowMarkers !== "none" ;
867
891
const rowMarkerOffset = hasRowMarkers ? 1 : 0 ;
868
892
const showTrailingBlankRow = onRowAppended !== undefined ;
@@ -1267,13 +1291,15 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
1267
1291
if ( isTrailing ) {
1268
1292
return loadingCell ;
1269
1293
}
1294
+ const mappedRow = rowNumberMapper ( row ) ;
1295
+ if ( mappedRow === undefined ) return loadingCell ;
1270
1296
return {
1271
1297
kind : InnerGridCellKind . Marker ,
1272
1298
allowOverlay : false ,
1273
1299
checkboxStyle : rowMarkerCheckboxStyle ,
1274
1300
checked : gridSelection ?. rows . hasIndex ( row ) === true ,
1275
1301
markerKind : rowMarkers === "clickable-number" ? "number" : rowMarkers ,
1276
- row : rowMarkerStartIndex + row ,
1302
+ row : rowMarkerStartIndex + mappedRow ,
1277
1303
drawHandle : onRowMoved !== undefined ,
1278
1304
cursor : rowMarkers === "clickable-number" ? "pointer" : undefined ,
1279
1305
} ;
@@ -1335,6 +1361,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
1335
1361
showTrailingBlankRow ,
1336
1362
mangledRows ,
1337
1363
hasRowMarkers ,
1364
+ rowNumberMapper ,
1338
1365
rowMarkerCheckboxStyle ,
1339
1366
gridSelection ?. rows ,
1340
1367
rowMarkers ,
@@ -1721,6 +1748,10 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
1721
1748
[ getRowThemeOverride , mangledCols , mergedTheme ]
1722
1749
) ;
1723
1750
1751
+ const { mapper } = useRowGrouping ( rowGrouping , rowsIn ) ;
1752
+
1753
+ const rowGroupingNavBehavior = rowGrouping ?. navigationBehavior ;
1754
+
1724
1755
const handleSelect = React . useCallback (
1725
1756
( args : GridMouseEventArgs ) => {
1726
1757
const isMultiKey = browserIsOSX . value ? args . metaKey : args . ctrlKey ;
@@ -1818,6 +1849,11 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
1818
1849
return ;
1819
1850
}
1820
1851
}
1852
+
1853
+ if ( rowGroupingNavBehavior === "block" && mapper ( row ) . isGroupHeader ) {
1854
+ return ;
1855
+ }
1856
+
1821
1857
const isLastStickyRow = lastRowSticky && row === rows ;
1822
1858
1823
1859
const startedFromLastSticky =
@@ -1927,28 +1963,30 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
1927
1963
}
1928
1964
} ,
1929
1965
[
1930
- appendRow ,
1966
+ rowSelect ,
1931
1967
columnSelect ,
1932
- focus ,
1933
- getCellRenderer ,
1934
- getCustomNewRowTargetColumn ,
1935
- getMangledCellContent ,
1936
1968
gridSelection ,
1937
1969
hasRowMarkers ,
1938
- lastRowSticky ,
1939
- onSelectionCleared ,
1940
- onRowMoved ,
1941
1970
rowMarkerOffset ,
1971
+ showTrailingBlankRow ,
1972
+ rows ,
1942
1973
rowMarkers ,
1943
- rowSelect ,
1974
+ getMangledCellContent ,
1975
+ onRowMoved ,
1976
+ focus ,
1944
1977
rowSelectionMode ,
1945
- rows ,
1978
+ getCellRenderer ,
1979
+ themeForCell ,
1980
+ setSelectedRows ,
1981
+ getCustomNewRowTargetColumn ,
1982
+ appendRow ,
1983
+ rowGroupingNavBehavior ,
1984
+ mapper ,
1985
+ lastRowSticky ,
1946
1986
setCurrent ,
1947
- setGridSelection ,
1948
1987
setSelectedColumns ,
1949
- setSelectedRows ,
1950
- showTrailingBlankRow ,
1951
- themeForCell ,
1988
+ setGridSelection ,
1989
+ onSelectionCleared ,
1952
1990
]
1953
1991
) ;
1954
1992
const isActivelyDraggingHeader = React . useRef ( false ) ;
@@ -2542,6 +2580,27 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
2542
2580
isActivelyDragging . current = false ;
2543
2581
} , [ ] ) ;
2544
2582
2583
+ const rowGroupingSelectionBehavior = rowGrouping ?. selectionBehavior ;
2584
+
2585
+ const getSelectionRowLimits = React . useCallback (
2586
+ ( selectedRow : number ) : readonly [ number , number ] | undefined => {
2587
+ if ( rowGroupingSelectionBehavior !== "block-spanning" ) return undefined ;
2588
+
2589
+ const { isGroupHeader, path, groupRows } = mapper ( selectedRow ) ;
2590
+
2591
+ if ( isGroupHeader ) {
2592
+ return [ selectedRow , selectedRow ] ;
2593
+ }
2594
+
2595
+ const groupRowIndex = path [ path . length - 1 ] ;
2596
+ const lowerBounds = selectedRow - groupRowIndex ;
2597
+ const upperBounds = selectedRow + groupRows - groupRowIndex - 1 ;
2598
+
2599
+ return [ lowerBounds , upperBounds ] ;
2600
+ } ,
2601
+ [ mapper , rowGroupingSelectionBehavior ]
2602
+ ) ;
2603
+
2545
2604
const hoveredRef = React . useRef < GridMouseEventArgs > ( ) ;
2546
2605
const onItemHoveredImpl = React . useCallback (
2547
2606
( args : GridMouseEventArgs ) => {
@@ -2596,6 +2655,10 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
2596
2655
}
2597
2656
2598
2657
col = Math . max ( col , rowMarkerOffset ) ;
2658
+ const clampLimits = getSelectionRowLimits ( selectedRow ) ;
2659
+ row = clampLimits === undefined ? row : clamp ( row , clampLimits [ 0 ] , clampLimits [ 1 ] ) ;
2660
+
2661
+ // FIXME: Restrict row based on rowGrouping.selectionBehavior here
2599
2662
2600
2663
const deltaX = col - selectedCol ;
2601
2664
const deltaY = row - selectedRow ;
@@ -2622,7 +2685,6 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
2622
2685
onItemHovered ?.( { ...args , location : [ args . location [ 0 ] - rowMarkerOffset , args . location [ 1 ] ] as any } ) ;
2623
2686
} ,
2624
2687
[
2625
- allowedFillDirections ,
2626
2688
mouseState ,
2627
2689
rowMarkerOffset ,
2628
2690
rowSelect ,
@@ -2632,6 +2694,8 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
2632
2694
setSelectedRows ,
2633
2695
showTrailingBlankRow ,
2634
2696
rows ,
2697
+ allowedFillDirections ,
2698
+ getSelectionRowLimits ,
2635
2699
setCurrent ,
2636
2700
]
2637
2701
) ;
@@ -2676,20 +2740,23 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
2676
2740
let top = old . y ;
2677
2741
let bottom = old . y + old . height ;
2678
2742
2743
+ const [ minRow , maxRowRaw ] = getSelectionRowLimits ( row ) ?? [ 0 , rows - 1 ] ;
2744
+ const maxRow = maxRowRaw + 1 ; // we need an inclusive value
2745
+
2679
2746
// take care of vertical first in case new spans come in
2680
2747
if ( y !== 0 ) {
2681
2748
switch ( y ) {
2682
2749
case 2 : {
2683
2750
// go to end
2684
- bottom = rows ;
2751
+ bottom = maxRow ;
2685
2752
top = row ;
2686
2753
scrollTo ( 0 , bottom , "vertical" ) ;
2687
2754
2688
2755
break ;
2689
2756
}
2690
2757
case - 2 : {
2691
2758
// go to start
2692
- top = 0 ;
2759
+ top = minRow ;
2693
2760
bottom = row + 1 ;
2694
2761
scrollTo ( 0 , top , "vertical" ) ;
2695
2762
@@ -2701,7 +2768,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
2701
2768
top ++ ;
2702
2769
scrollTo ( 0 , top , "vertical" ) ;
2703
2770
} else {
2704
- bottom = Math . min ( rows , bottom + 1 ) ;
2771
+ bottom = Math . min ( maxRow , bottom + 1 ) ;
2705
2772
scrollTo ( 0 , bottom , "vertical" ) ;
2706
2773
}
2707
2774
@@ -2713,7 +2780,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
2713
2780
bottom -- ;
2714
2781
scrollTo ( 0 , bottom , "vertical" ) ;
2715
2782
} else {
2716
- top = Math . max ( 0 , top - 1 ) ;
2783
+ top = Math . max ( minRow , top - 1 ) ;
2717
2784
scrollTo ( 0 , top , "vertical" ) ;
2718
2785
}
2719
2786
@@ -2816,7 +2883,16 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
2816
2883
"keyboard-select"
2817
2884
) ;
2818
2885
} ,
2819
- [ getCellsForSelection , gridSelection , mangledCols . length , rowMarkerOffset , rows , scrollTo , setCurrent ]
2886
+ [
2887
+ getCellsForSelection ,
2888
+ getSelectionRowLimits ,
2889
+ gridSelection ,
2890
+ mangledCols . length ,
2891
+ rowMarkerOffset ,
2892
+ rows ,
2893
+ scrollTo ,
2894
+ setCurrent ,
2895
+ ]
2820
2896
) ;
2821
2897
2822
2898
const updateSelectedCell = React . useCallback (
@@ -2825,7 +2901,10 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
2825
2901
col = clamp ( col , rowMarkerOffset , columns . length - 1 + rowMarkerOffset ) ;
2826
2902
row = clamp ( row , 0 , rowMax ) ;
2827
2903
2828
- if ( col === currentCell ?. [ 0 ] && row === currentCell ?. [ 1 ] ) return false ;
2904
+ const curCol = currentCell ?. [ 0 ] ;
2905
+ const curRow = currentCell ?. [ 1 ] ;
2906
+
2907
+ if ( col === curCol && row === curRow ) return false ;
2829
2908
if ( freeMove && gridSelection . current !== undefined ) {
2830
2909
const newStack = [ ...gridSelection . current . rangeStack ] ;
2831
2910
if ( gridSelection . current . range . width > 1 || gridSelection . current . range . height > 1 ) {
@@ -3041,6 +3120,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
3041
3120
3042
3121
if ( gridSelection . current === undefined ) return false ;
3043
3122
let [ col , row ] = gridSelection . current . cell ;
3123
+ const [ , startRow ] = gridSelection . current . cell ;
3044
3124
let freeMove = false ;
3045
3125
let cancelOnlyOnMove = false ;
3046
3126
@@ -3174,6 +3254,37 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
3174
3254
}
3175
3255
// #endregion
3176
3256
3257
+ const mustRestrictRow = rowGroupingNavBehavior !== undefined && rowGroupingNavBehavior !== "normal" ;
3258
+
3259
+ if ( mustRestrictRow && row !== startRow ) {
3260
+ const skipUp =
3261
+ rowGroupingNavBehavior === "skip-up" ||
3262
+ rowGroupingNavBehavior === "skip" ||
3263
+ rowGroupingNavBehavior === "block" ;
3264
+ const skipDown =
3265
+ rowGroupingNavBehavior === "skip-down" ||
3266
+ rowGroupingNavBehavior === "skip" ||
3267
+ rowGroupingNavBehavior === "block" ;
3268
+ const didMoveUp = row < startRow ;
3269
+ if ( didMoveUp && skipUp ) {
3270
+ while ( row >= 0 && mapper ( row ) . isGroupHeader ) {
3271
+ row -- ;
3272
+ }
3273
+
3274
+ if ( row < 0 ) {
3275
+ row = startRow ;
3276
+ }
3277
+ } else if ( ! didMoveUp && skipDown ) {
3278
+ while ( row < rows && mapper ( row ) . isGroupHeader ) {
3279
+ row ++ ;
3280
+ }
3281
+
3282
+ if ( row >= rows ) {
3283
+ row = startRow ;
3284
+ }
3285
+ }
3286
+ }
3287
+
3177
3288
const moved = updateSelectedCell ( col , row , false , freeMove ) ;
3178
3289
3179
3290
const didMatch = details . didMatch ;
@@ -3185,13 +3296,15 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
3185
3296
return didMatch ;
3186
3297
} ,
3187
3298
[
3299
+ rowGroupingNavBehavior ,
3188
3300
overlayOpen ,
3189
3301
gridSelection ,
3190
3302
keybindings ,
3191
3303
columnSelect ,
3192
3304
rowSelect ,
3193
3305
rangeSelect ,
3194
3306
rowMarkerOffset ,
3307
+ mapper ,
3195
3308
rows ,
3196
3309
updateSelectedCell ,
3197
3310
setGridSelection ,
0 commit comments