Skip to content

Commit 7fcfe88

Browse files
authored
Header Groups (#88)
* Basic rendering in place * Start roughing in events, still ugly * Handle most events on group headers for now * Draw trailing border on groups * Add story and improve themes * Update API docs * Update API.md
1 parent 9fcff99 commit 7fcfe88

File tree

11 files changed

+283
-28
lines changed

11 files changed

+283
-28
lines changed

core/API.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ export interface Rectangle {
306306
export interface GridColumn {
307307
readonly width: number; // The width of the column
308308
readonly title: string; // The title of the column
309+
readonly group?: string; // The group header the column should be under
309310
readonly icon?: GridColumnIcon | string; // The icon
310311
readonly overlayIcon?: GridColumnIcon | string; // An icon to draw on top (like a lock indicator)
311312
readonly hasMenu?: boolean; // If the column should draw a menu triangle
@@ -334,6 +335,7 @@ The Grid supports the following kinds of cells:
334335
- `LoadingCell` is currently rendered empty, but should be used for data that's not loaded in yet.
335336
- `ProtectedCell` is for data that the user isn't supposed to see, for example other user's passwords.
336337
- `DrilldownCell` displays bubbles with a small thumbnail and text.
338+
- `CustomCell` a cell designed to be used as a base type for all custom cells.
337339

338340
```ts
339341
export type GridCell = EditableGridCell | BubbleCell | RowIDCell | LoadingCell | ProtectedCell | DrilldownCell;
@@ -364,7 +366,7 @@ interface BubbleCell extends BaseGridCell {
364366
readonly data: readonly string[];
365367
}
366368

367-
export interface DrilldownCellData {
369+
interface DrilldownCellData {
368370
readonly text: string;
369371
readonly img?: string;
370372
}
@@ -404,6 +406,12 @@ interface ProtectedCell extends BaseGridCell {
404406
readonly kind: GridCellKind.Protected;
405407
}
406408

409+
interface CustomCell<T extends {} = {}> extends BaseGridCell {
410+
readonly kind: GridCellKind.Custom;
411+
readonly data: T;
412+
readonly copyData: string;
413+
}
414+
407415
export enum GridCellKind {
408416
Uri = "uri",
409417
Text = "text",

core/src/common/styles.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,46 @@
11
import baseStyled, { ThemedStyledInterface } from "styled-components";
22

3-
const dataEditorBaseTheme = {
3+
export interface Theme {
4+
accentColor: string;
5+
accentFg: string;
6+
accentLight: string;
7+
8+
textDark: string;
9+
textMedium: string;
10+
textLight: string;
11+
textBubble: string;
12+
13+
bgIconHeader: string;
14+
fgIconHeader: string;
15+
textHeader: string;
16+
textGroupHeader?: string;
17+
textHeaderSelected: string;
18+
19+
bgCell: string;
20+
bgCellMedium: string;
21+
bgHeader: string;
22+
bgHeaderHasFocus: string;
23+
bgHeaderHovered: string;
24+
25+
bgBubble: string;
26+
bgBubbleSelected: string;
27+
28+
bgSearchResult: string;
29+
30+
borderColor: string;
31+
drilldownBorder: string;
32+
33+
linkColor: string;
34+
35+
cellHorizontalPadding: number;
36+
cellVerticalPadding: number;
37+
38+
headerFontStyle: string;
39+
baseFontStyle: string;
40+
fontFamily: string;
41+
}
42+
43+
const dataEditorBaseTheme: Theme = {
444
accentColor: "#4F5DFF",
545
accentFg: "#FFFFFF",
646
accentLight: "rgba(62, 116, 253, 0.1)",
@@ -13,6 +53,7 @@ const dataEditorBaseTheme = {
1353
bgIconHeader: "#737383",
1454
fgIconHeader: "#FFFFFF",
1555
textHeader: "#313139",
56+
textGroupHeader: "#313139BB",
1657
textHeaderSelected: "#FFFFFF",
1758

1859
bgCell: "#FFFFFF",
@@ -40,8 +81,6 @@ const dataEditorBaseTheme = {
4081
"Inter, Roboto, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, noto, arial, sans-serif",
4182
};
4283

43-
export type Theme = typeof dataEditorBaseTheme;
44-
4584
export const styled = baseStyled as ThemedStyledInterface<Theme>;
4685

4786
export function getDataEditorTheme(): Theme {

core/src/data-editor/data-editor-beautiful.stories.tsx

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,10 @@ const BeautifulWrapper: React.FC<BeautifulProps> = p => {
143143
);
144144
};
145145

146-
function createTextColumnInfo(index: number): GridColumnWithMockingInfo {
146+
function createTextColumnInfo(index: number, group: boolean): GridColumnWithMockingInfo {
147147
return {
148148
title: `Column ${index}`,
149+
group: group ? `Group ${Math.round(index / 3)}` : undefined,
149150
width: 120,
150151
icon: GridColumnIcon.HeaderString,
151152
hasMenu: false,
@@ -163,10 +164,11 @@ function createTextColumnInfo(index: number): GridColumnWithMockingInfo {
163164
};
164165
}
165166

166-
function getResizableColumns(amount: number): GridColumnWithMockingInfo[] {
167+
function getResizableColumns(amount: number, group: boolean): GridColumnWithMockingInfo[] {
167168
const defaultColumns: GridColumnWithMockingInfo[] = [
168169
{
169170
title: "First name",
171+
group: group ? "Name" : undefined,
170172
width: 120,
171173
icon: GridColumnIcon.HeaderString,
172174
hasMenu: false,
@@ -183,6 +185,7 @@ function getResizableColumns(amount: number): GridColumnWithMockingInfo[] {
183185
},
184186
{
185187
title: "Last name",
188+
group: group ? "Name" : undefined,
186189
width: 120,
187190
icon: GridColumnIcon.HeaderString,
188191
hasMenu: false,
@@ -200,6 +203,7 @@ function getResizableColumns(amount: number): GridColumnWithMockingInfo[] {
200203
{
201204
title: "Avatar",
202205
width: 120,
206+
group: group ? "Info" : undefined,
203207
icon: GridColumnIcon.HeaderImage,
204208
hasMenu: false,
205209
getContent: () => {
@@ -217,6 +221,7 @@ function getResizableColumns(amount: number): GridColumnWithMockingInfo[] {
217221
{
218222
title: "Email",
219223
width: 120,
224+
group: group ? "Info" : undefined,
220225
icon: GridColumnIcon.HeaderString,
221226
hasMenu: false,
222227
getContent: () => {
@@ -233,6 +238,7 @@ function getResizableColumns(amount: number): GridColumnWithMockingInfo[] {
233238
{
234239
title: "Title",
235240
width: 120,
241+
group: group ? "Info" : undefined,
236242
icon: GridColumnIcon.HeaderString,
237243
hasMenu: false,
238244
getContent: () => {
@@ -249,6 +255,7 @@ function getResizableColumns(amount: number): GridColumnWithMockingInfo[] {
249255
{
250256
title: "More Info",
251257
width: 120,
258+
group: group ? "Info" : undefined,
252259
icon: GridColumnIcon.HeaderUri,
253260
hasMenu: false,
254261
getContent: () => {
@@ -271,7 +278,7 @@ function getResizableColumns(amount: number): GridColumnWithMockingInfo[] {
271278
const extraColumnsAmount = amount - defaultColumns.length;
272279

273280
const extraColumns = [...new Array(extraColumnsAmount)].map((_, index) =>
274-
createTextColumnInfo(index + defaultColumns.length)
281+
createTextColumnInfo(index + defaultColumns.length, group)
275282
);
276283

277284
return [...defaultColumns, ...extraColumns];
@@ -301,14 +308,14 @@ class ContentCache {
301308
}
302309
}
303310

304-
function useMockDataGenerator(numCols: number, readonly: boolean = true) {
311+
function useMockDataGenerator(numCols: number, readonly: boolean = true, group: boolean = false) {
305312
const cache = React.useRef<ContentCache>(new ContentCache());
306313

307-
const [colsMap, setColsMap] = React.useState(() => getResizableColumns(numCols));
314+
const [colsMap, setColsMap] = React.useState(() => getResizableColumns(numCols, group));
308315

309316
React.useEffect(() => {
310-
setColsMap(getResizableColumns(numCols));
311-
}, [numCols]);
317+
setColsMap(getResizableColumns(numCols, group));
318+
}, [group, numCols]);
312319

313320
const onColumnResized = React.useCallback((column: GridColumn, newSize: number) => {
314321
setColsMap(prevColsMap => {
@@ -2060,3 +2067,30 @@ export const ReorderRows: React.VFC = () => {
20602067
showPanel: false,
20612068
},
20622069
};
2070+
2071+
export const ColumnGroups: React.VFC = () => {
2072+
const { cols, getCellContent } = useMockDataGenerator(100000, true, true);
2073+
2074+
return (
2075+
<BeautifulWrapper
2076+
title="Column Grouping"
2077+
description={
2078+
<Description>
2079+
Columns in the data grid may be grouped by setting their <PropName>group</PropName> property.
2080+
</Description>
2081+
}>
2082+
<DataEditor
2083+
{...defaultProps}
2084+
getCellContent={getCellContent}
2085+
columns={cols}
2086+
rows={1000}
2087+
rowMarkers="both"
2088+
/>
2089+
</BeautifulWrapper>
2090+
);
2091+
};
2092+
(ColumnGroups as any).parameters = {
2093+
options: {
2094+
showPanel: false,
2095+
},
2096+
};

core/src/data-editor/data-editor.tsx

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type Props = Omit<
4141
| "cellYOffset"
4242
| "className"
4343
| "disabledRows"
44+
| "enableGroups"
4445
| "firstColSticky"
4546
| "getCellContent"
4647
| "gridRef"
@@ -142,7 +143,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
142143
getCellsForSelection,
143144
rowMarkers = "none",
144145
rowHeight = 34,
145-
headerHeight = 36,
146+
headerHeight: rawHeaderHeight = 36,
146147
rowMarkerWidth: rowMarkerWidthRaw,
147148
imageEditorOverride,
148149
markdownDivCreateNode,
@@ -184,6 +185,8 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
184185
const showTrailingBlankRow = onRowAppended !== undefined;
185186
const lastRowSticky = trailingRowOptions?.sticky === true;
186187

188+
const mangledFreezeColumns = freezeColumns + (hasRowMarkers ? 1 : 0);
189+
187190
const gridSelection = gridSelectionOuter ?? gridSelectionInner;
188191
const setGridSelection = onGridSelectionChange ?? setGridSelectionInner;
189192
const selectedRows = selectedRowsOuter ?? selectedRowsInner;
@@ -199,6 +202,12 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
199202
[rowMarkerOffset, setSelectedColumnsOuter]
200203
);
201204

205+
const enableGroups = React.useMemo(() => {
206+
return columns.some(c => c.group !== undefined);
207+
}, [columns]);
208+
209+
const headerHeight = enableGroups ? rawHeaderHeight * 2 : rawHeaderHeight;
210+
202211
const setSelectedColumns =
203212
setSelectedColumnsOuter !== undefined ? mangledSetSelectedColumns : setSelectedColumnsInner;
204213

@@ -568,6 +577,41 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
568577
setSelectedRows(CompactSelection.empty());
569578
lastSelectedRowRef.current = undefined;
570579
lastSelectedColRef.current = undefined;
580+
} else if (args.kind === "group-header") {
581+
const [col] = args.location;
582+
583+
if (col < rowMarkerOffset) return;
584+
585+
const needle = mangledCols[col];
586+
let start = col;
587+
let end = col;
588+
for (let i = col - 1; i >= rowMarkerOffset; i--) {
589+
if (needle.group !== mangledCols[i].group) break;
590+
start--;
591+
}
592+
593+
for (let i = col + 1; i < mangledCols.length; i++) {
594+
if (needle.group !== mangledCols[i].group) break;
595+
end++;
596+
}
597+
598+
setSelectedRows(CompactSelection.empty());
599+
setGridSelection(undefined);
600+
focus();
601+
602+
if (isMultiKey) {
603+
if (selectedColumns.hasAll([start, end + 1])) {
604+
let newVal = selectedColumns;
605+
for (let index = start; index <= end; index++) {
606+
newVal = newVal.remove(index);
607+
}
608+
setSelectedColumns(newVal);
609+
} else {
610+
setSelectedColumns(selectedColumns.add([start, end + 1]));
611+
}
612+
} else {
613+
setSelectedColumns(CompactSelection.fromSingleSelection([start, end + 1]));
614+
}
571615
}
572616
},
573617
[
@@ -586,6 +630,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
586630
appendRow,
587631
lastRowSticky,
588632
selectedColumns,
633+
mangledCols,
589634
]
590635
);
591636

@@ -1424,11 +1469,11 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
14241469
[rowMarkerOffset, verticalBorder]
14251470
);
14261471

1427-
const mangledFreezeColumns = freezeColumns + (hasRowMarkers ? 1 : 0);
14281472
return (
14291473
<ThemeProvider theme={mergedTheme}>
14301474
<DataGridSearch
14311475
{...rest}
1476+
enableGroups={enableGroups}
14321477
canvasRef={canvasRef}
14331478
cellXOffset={cellXOffset}
14341479
cellYOffset={cellYOffset}

core/src/data-grid-dnd/data-grid-dnd.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ const DataGridDnd: React.FunctionComponent<DataGridDndProps> = p => {
6868
setResizeColStartX(bounds.x);
6969
setResizeCol(columns.length - 1);
7070
}
71-
} else if ((args.kind === "header" || args.kind === "cell") && col >= freezeColumns) {
71+
} else if (args.kind === "header" && col >= freezeColumns) {
7272
if (args.isEdge) {
7373
shouldFireEvent = false;
7474
setResizeColStartX(args.bounds.x);

core/src/data-grid/data-grid-lib.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export function getRowIndexForY(
100100
translateY: number,
101101
lastRowSticky: boolean
102102
): number | undefined {
103+
if (targetY <= headerHeight / 2) return -2;
103104
if (targetY <= headerHeight) return -1;
104105

105106
const lastRowHeight = typeof rowHeight === "number" ? rowHeight : rowHeight(rows - 1);

0 commit comments

Comments
 (0)