Skip to content

Commit 8490e56

Browse files
amanmahajan7nstepien
authored andcommitted
Add column groups (Comcast#3287)
* Initial commit to support column groups * idk * calculated groups * progress * progress * progress * remove level, fix header rows count * row span * fix incorrectly skipped headers * fix scrollPaddingBlock * Add `aria-rowspan` * add padding on top to move content at the bottom * fix colSpan * fix aria-rowspan * render grouped column header cells without duplicates * fix cell positions * remove `children` * disallow col groups in TreeDataGrid * fix test * add grouping test * tweak * dedupe headerRowsHeight * Remove TODO * Fix cell click selection on the column group * grouping -> row grouping * add col grouping page * revert common features * simplify getHeaderCellStyle * Fix params * add headerCellClass on ColumnGroup * group the Or types * remove TODO, add summary rows in the example * export CalculatedColumnParent and CalculatedColumnOrColumnGroup * minor tweaks * Combine `useMoreCalculatedColumnsStuff` and `useCalculatedColumns` * tweaks * consistent gridColumnEnd * fix tests * rm rowSelectedClassname usage * fix cell borders --------- Co-authored-by: Nicolas Stepien <567105+nstepien@users.noreply.github.com> Co-authored-by: Nicolas Stepien <stepien.nicolas@gmail.com>
1 parent a3f72ab commit 8490e56

25 files changed

+779
-115
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@
3131
- Click on a sortable column header to toggle between its ascending/descending sort order
3232
- Ctrl+Click / Meta+Click to sort an additional column
3333
- [Column spanning](https://adazzle.github.io/react-data-grid/#/column-spanning)
34+
- [Column grouping](https://adazzle.github.io/react-data-grid/#/column-grouping)
3435
- [Row selection](https://adazzle.github.io/react-data-grid/#/common-features)
35-
- [Row grouping](https://adazzle.github.io/react-data-grid/#/grouping)
36+
- [Row grouping](https://adazzle.github.io/react-data-grid/#/row-grouping)
3637
- [Summary rows](https://adazzle.github.io/react-data-grid/#/common-features)
3738
- [Dynamic row heights](https://adazzle.github.io/react-data-grid/#/variable-row-height)
3839
- [No rows fallback](https://adazzle.github.io/react-data-grid/#/no-rows)

src/Cell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ function Cell<R, SR>({
9393
<div
9494
role="gridcell"
9595
aria-colindex={column.idx + 1} // aria-colindex is 1-based
96-
aria-selected={isCellSelected}
9796
aria-colspan={colSpan}
97+
aria-selected={isCellSelected}
9898
aria-readonly={!isEditable || undefined}
9999
tabIndex={tabIndex}
100100
className={className}

src/DataGrid.tsx

Lines changed: 62 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import type {
3636
CellMouseEvent,
3737
CellNavigationMode,
3838
Column,
39+
ColumnOrColumnGroup,
3940
CopyEvent,
4041
Direction,
4142
FillEvent,
@@ -54,6 +55,7 @@ import {
5455
} from './DataGridDefaultRenderersProvider';
5556
import DragHandle from './DragHandle';
5657
import EditCell from './EditCell';
58+
import GroupedColumnHeaderRow from './GroupedColumnHeaderRow';
5759
import HeaderRow from './HeaderRow';
5860
import { defaultRenderRow } from './Row';
5961
import type { PartialPosition } from './ScrollToCell';
@@ -105,7 +107,7 @@ export interface DataGridProps<R, SR = unknown, K extends Key = Key> extends Sha
105107
* Grid and data Props
106108
*/
107109
/** An array of objects representing each column on the grid */
108-
columns: readonly Column<R, SR>[];
110+
columns: readonly ColumnOrColumnGroup<R, SR>[];
109111
/** A function called for each rendered row that should return a plain key/value pair object */
110112
rows: readonly R[];
111113
/**
@@ -258,14 +260,6 @@ function DataGrid<R, SR, K extends Key>(
258260
const enableVirtualization = rawEnableVirtualization ?? true;
259261
const direction = rawDirection ?? 'ltr';
260262

261-
const headerRowsCount = 1;
262-
const topSummaryRowsCount = topSummaryRows?.length ?? 0;
263-
const bottomSummaryRowsCount = bottomSummaryRows?.length ?? 0;
264-
const summaryRowsCount = topSummaryRowsCount + bottomSummaryRowsCount;
265-
const headerAndTopSummaryRowsCount = headerRowsCount + topSummaryRowsCount;
266-
const minRowIdx = -headerAndTopSummaryRowsCount;
267-
const maxRowIdx = rows.length + bottomSummaryRowsCount - 1;
268-
269263
/**
270264
* states
271265
*/
@@ -277,14 +271,45 @@ function DataGrid<R, SR, K extends Key>(
277271
const [measuredColumnWidths, setMeasuredColumnWidths] = useState(
278272
(): ReadonlyMap<string, number> => new Map()
279273
);
280-
const [selectedPosition, setSelectedPosition] = useState(
281-
(): SelectCellState | EditCellState<R> => ({ idx: -1, rowIdx: minRowIdx - 1, mode: 'SELECT' })
282-
);
283274
const [copiedCell, setCopiedCell] = useState<{ row: R; columnKey: string } | null>(null);
284275
const [isDragging, setDragging] = useState(false);
285276
const [draggedOverRowIdx, setOverRowIdx] = useState<number | undefined>(undefined);
286277
const [scrollToPosition, setScrollToPosition] = useState<PartialPosition | null>(null);
287278

279+
const [gridRef, gridWidth, gridHeight] = useGridDimensions();
280+
const {
281+
columns,
282+
colSpanColumns,
283+
lastFrozenColumnIndex,
284+
headerRowsCount,
285+
colOverscanStartIdx,
286+
colOverscanEndIdx,
287+
templateColumns,
288+
layoutCssVars,
289+
totalFrozenColumnWidth
290+
} = useCalculatedColumns({
291+
rawColumns,
292+
defaultColumnOptions,
293+
measuredColumnWidths,
294+
resizedColumnWidths,
295+
scrollLeft,
296+
viewportWidth: gridWidth,
297+
enableVirtualization
298+
});
299+
300+
const topSummaryRowsCount = topSummaryRows?.length ?? 0;
301+
const bottomSummaryRowsCount = bottomSummaryRows?.length ?? 0;
302+
const summaryRowsCount = topSummaryRowsCount + bottomSummaryRowsCount;
303+
const headerAndTopSummaryRowsCount = headerRowsCount + topSummaryRowsCount;
304+
const groupedColumnHeaderRowsCount = headerRowsCount - 1;
305+
const minRowIdx = -headerAndTopSummaryRowsCount;
306+
const mainHeaderIndex = minRowIdx + groupedColumnHeaderRowsCount;
307+
const maxRowIdx = rows.length + bottomSummaryRowsCount - 1;
308+
309+
const [selectedPosition, setSelectedPosition] = useState(
310+
(): SelectCellState | EditCellState<R> => ({ idx: -1, rowIdx: minRowIdx - 1, mode: 'SELECT' })
311+
);
312+
288313
/**
289314
* refs
290315
*/
@@ -298,8 +323,8 @@ function DataGrid<R, SR, K extends Key>(
298323
* computed values
299324
*/
300325
const isTreeGrid = role === 'treegrid';
301-
const [gridRef, gridWidth, gridHeight] = useGridDimensions();
302-
const clientHeight = gridHeight - headerRowHeight - summaryRowsCount * summaryRowHeight;
326+
const headerRowsHeight = headerRowsCount * headerRowHeight;
327+
const clientHeight = gridHeight - headerRowsHeight - summaryRowsCount * summaryRowHeight;
303328
const isSelectable = selectedRows != null && onSelectedRowsChange != null;
304329
const isRtl = direction === 'rtl';
305330
const leftKey = isRtl ? 'ArrowRight' : 'ArrowLeft';
@@ -326,25 +351,6 @@ function DataGrid<R, SR, K extends Key>(
326351
);
327352
}, [rows, selectedRows, rowKeyGetter]);
328353

329-
const {
330-
columns,
331-
colSpanColumns,
332-
colOverscanStartIdx,
333-
colOverscanEndIdx,
334-
templateColumns,
335-
layoutCssVars,
336-
lastFrozenColumnIndex,
337-
totalFrozenColumnWidth
338-
} = useCalculatedColumns({
339-
rawColumns,
340-
measuredColumnWidths,
341-
resizedColumnWidths,
342-
scrollLeft,
343-
viewportWidth: gridWidth,
344-
defaultColumnOptions,
345-
enableVirtualization
346-
});
347-
348354
const {
349355
rowOverscanStartIdx,
350356
rowOverscanEndIdx,
@@ -403,8 +409,8 @@ function DataGrid<R, SR, K extends Key>(
403409
const selectRowLatest = useLatestFunc(selectRow);
404410
const handleFormatterRowChangeLatest = useLatestFunc(updateRow);
405411
const selectCellLatest = useLatestFunc(selectCell);
406-
const selectHeaderCellLatest = useLatestFunc((idx: number) => {
407-
selectCell({ rowIdx: minRowIdx, idx });
412+
const selectHeaderCellLatest = useLatestFunc(({ idx, rowIdx }: Position) => {
413+
selectCell({ rowIdx: minRowIdx + rowIdx - 1, idx });
408414
});
409415

410416
/**
@@ -958,7 +964,7 @@ function DataGrid<R, SR, K extends Key>(
958964
setDraggedOverRowIdx(undefined);
959965
}
960966

961-
let templateRows = `${headerRowHeight}px`;
967+
let templateRows = `repeat(${headerRowsCount}, ${headerRowHeight}px)`;
962968
if (topSummaryRowsCount > 0) {
963969
templateRows += ` repeat(${topSummaryRowsCount}, ${summaryRowHeight}px)`;
964970
}
@@ -999,7 +1005,7 @@ function DataGrid<R, SR, K extends Key>(
9991005
scrollPaddingBlock:
10001006
isRowIdxWithinViewportBounds(selectedPosition.rowIdx) ||
10011007
scrollToPosition?.rowIdx !== undefined
1002-
? `${headerRowHeight + topSummaryRowsCount * summaryRowHeight}px ${
1008+
? `${headerRowsHeight + topSummaryRowsCount * summaryRowHeight}px ${
10031009
bottomSummaryRowsCount * summaryRowHeight
10041010
}px`
10051011
: undefined,
@@ -1020,14 +1026,27 @@ function DataGrid<R, SR, K extends Key>(
10201026
<DataGridDefaultRenderersProvider value={defaultGridComponents}>
10211027
<RowSelectionChangeProvider value={selectRowLatest}>
10221028
<RowSelectionProvider value={allRowsSelected}>
1029+
{Array.from({ length: groupedColumnHeaderRowsCount }, (_, index) => (
1030+
<GroupedColumnHeaderRow
1031+
key={index}
1032+
rowIdx={index + 1}
1033+
level={-groupedColumnHeaderRowsCount + index}
1034+
columns={getRowViewportColumns(minRowIdx + index)}
1035+
selectedCellIdx={
1036+
selectedPosition.rowIdx === minRowIdx + index ? selectedPosition.idx : undefined
1037+
}
1038+
selectCell={selectHeaderCellLatest}
1039+
/>
1040+
))}
10231041
<HeaderRow
1024-
columns={getRowViewportColumns(minRowIdx)}
1042+
rowIdx={headerRowsCount}
1043+
columns={getRowViewportColumns(mainHeaderIndex)}
10251044
onColumnResize={handleColumnResizeLatest}
10261045
sortColumns={sortColumns}
10271046
onSortColumnsChange={onSortColumnsChangeLatest}
10281047
lastFrozenColumnIndex={lastFrozenColumnIndex}
10291048
selectedCellIdx={
1030-
selectedPosition.rowIdx === minRowIdx ? selectedPosition.idx : undefined
1049+
selectedPosition.rowIdx === mainHeaderIndex ? selectedPosition.idx : undefined
10311050
}
10321051
selectCell={selectHeaderCellLatest}
10331052
shouldFocusGrid={!selectedCellIsWithinSelectionBounds}
@@ -1039,15 +1058,15 @@ function DataGrid<R, SR, K extends Key>(
10391058
) : (
10401059
<>
10411060
{topSummaryRows?.map((row, rowIdx) => {
1042-
const gridRowStart = headerRowsCount + rowIdx + 1;
1043-
const summaryRowIdx = rowIdx + minRowIdx + 1;
1061+
const gridRowStart = headerRowsCount + 1 + rowIdx;
1062+
const summaryRowIdx = mainHeaderIndex + 1 + rowIdx;
10441063
const isSummaryRowSelected = selectedPosition.rowIdx === summaryRowIdx;
1045-
const top = headerRowHeight + summaryRowHeight * rowIdx;
1064+
const top = headerRowsHeight + summaryRowHeight * rowIdx;
10461065

10471066
return (
10481067
<SummaryRow
1049-
aria-rowindex={gridRowStart}
10501068
key={rowIdx}
1069+
aria-rowindex={gridRowStart}
10511070
rowIdx={summaryRowIdx}
10521071
gridRowStart={gridRowStart}
10531072
row={row}

src/GroupedColumnHeaderCell.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import clsx from 'clsx';
2+
3+
import { useRovingTabIndex } from './hooks';
4+
import { getHeaderCellRowSpan, getHeaderCellStyle } from './utils';
5+
import type { CalculatedColumnParent } from './types';
6+
import { type GroupedColumnHeaderRowProps } from './GroupedColumnHeaderRow';
7+
import { cellClassname } from './style/cell';
8+
9+
type SharedGroupedColumnHeaderRowProps<R, SR> = Pick<
10+
GroupedColumnHeaderRowProps<R, SR>,
11+
'rowIdx' | 'selectCell'
12+
>;
13+
14+
interface GroupedColumnHeaderCellProps<R, SR> extends SharedGroupedColumnHeaderRowProps<R, SR> {
15+
column: CalculatedColumnParent<R, SR>;
16+
isCellSelected: boolean;
17+
}
18+
19+
export default function GroupedColumnHeaderCell<R, SR>({
20+
column,
21+
rowIdx,
22+
isCellSelected,
23+
selectCell
24+
}: GroupedColumnHeaderCellProps<R, SR>) {
25+
const { tabIndex, onFocus } = useRovingTabIndex(isCellSelected);
26+
const { colSpan } = column;
27+
const rowSpan = getHeaderCellRowSpan(column, rowIdx);
28+
const index = column.idx + 1;
29+
30+
function onClick() {
31+
selectCell({ idx: column.idx, rowIdx });
32+
}
33+
34+
return (
35+
<div
36+
role="columnheader"
37+
aria-colindex={index}
38+
aria-colspan={colSpan}
39+
aria-rowspan={rowSpan}
40+
aria-selected={isCellSelected}
41+
tabIndex={tabIndex}
42+
className={clsx(cellClassname, column.headerCellClass)}
43+
style={{
44+
...getHeaderCellStyle(column, rowIdx, rowSpan),
45+
gridColumnStart: index,
46+
gridColumnEnd: index + colSpan
47+
}}
48+
onFocus={onFocus}
49+
onClick={onClick}
50+
>
51+
{column.name}
52+
</div>
53+
);
54+
}

src/GroupedColumnHeaderRow.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { memo } from 'react';
2+
3+
import type { CalculatedColumn, CalculatedColumnParent, Position } from './types';
4+
import GroupedColumnHeaderCell from './GroupedColumnHeaderCell';
5+
import { headerRowClassname } from './HeaderRow';
6+
7+
export interface GroupedColumnHeaderRowProps<R, SR> {
8+
rowIdx: number;
9+
level: number;
10+
columns: readonly CalculatedColumn<R, SR>[];
11+
selectCell: (position: Position) => void;
12+
selectedCellIdx: number | undefined;
13+
}
14+
15+
function GroupedColumnHeaderRow<R, SR>({
16+
rowIdx,
17+
level,
18+
columns,
19+
selectedCellIdx,
20+
selectCell
21+
}: GroupedColumnHeaderRowProps<R, SR>) {
22+
const cells = [];
23+
const renderedParents = new Set<CalculatedColumnParent<R, SR>>();
24+
25+
for (const column of columns) {
26+
let { parent } = column;
27+
28+
if (parent === undefined) continue;
29+
30+
while (parent.level > level) {
31+
if (parent.parent === undefined) break;
32+
parent = parent.parent;
33+
}
34+
35+
if (parent.level === level && !renderedParents.has(parent)) {
36+
renderedParents.add(parent);
37+
const { idx } = parent;
38+
cells.push(
39+
<GroupedColumnHeaderCell<R, SR>
40+
key={idx}
41+
column={parent}
42+
rowIdx={rowIdx}
43+
isCellSelected={selectedCellIdx === idx}
44+
selectCell={selectCell}
45+
/>
46+
);
47+
}
48+
}
49+
50+
return (
51+
<div
52+
role="row"
53+
aria-rowindex={rowIdx} // aria-rowindex is 1 based
54+
className={headerRowClassname}
55+
>
56+
{cells}
57+
</div>
58+
);
59+
}
60+
61+
export default memo(GroupedColumnHeaderRow) as <R, SR>(
62+
props: GroupedColumnHeaderRowProps<R, SR>
63+
) => JSX.Element;

0 commit comments

Comments
 (0)