Skip to content

Commit

Permalink
Implement auto-sizing of columns
Browse files Browse the repository at this point in the history
  • Loading branch information
jassmith committed Feb 21, 2022
1 parent fbfdec0 commit 18c21f7
Show file tree
Hide file tree
Showing 22 changed files with 127 additions and 29 deletions.
14 changes: 4 additions & 10 deletions packages/core/src/data-editor/data-editor-beautiful.stories.tsx
Expand Up @@ -225,7 +225,6 @@ function createTextColumnInfo(index: number, group: boolean): GridColumnWithMock
return {
title: `Column ${index}`,
group: group ? `Group ${Math.round(index / 3)}` : undefined,
width: 120,
icon: GridColumnIcon.HeaderString,
hasMenu: false,
getContent: () => {
Expand All @@ -247,7 +246,6 @@ function getResizableColumns(amount: number, group: boolean): GridColumnWithMock
{
title: "First name",
group: group ? "Name" : undefined,
width: 120,
icon: GridColumnIcon.HeaderString,
hasMenu: false,
getContent: () => {
Expand All @@ -264,7 +262,6 @@ function getResizableColumns(amount: number, group: boolean): GridColumnWithMock
{
title: "Last name",
group: group ? "Name" : undefined,
width: 120,
icon: GridColumnIcon.HeaderString,
hasMenu: false,
getContent: () => {
Expand All @@ -280,7 +277,6 @@ function getResizableColumns(amount: number, group: boolean): GridColumnWithMock
},
{
title: "Avatar",
width: 120,
group: group ? "Info" : undefined,
icon: GridColumnIcon.HeaderImage,
hasMenu: false,
Expand All @@ -298,7 +294,6 @@ function getResizableColumns(amount: number, group: boolean): GridColumnWithMock
},
{
title: "Email",
width: 120,
group: group ? "Info" : undefined,
icon: GridColumnIcon.HeaderString,
hasMenu: false,
Expand All @@ -315,7 +310,6 @@ function getResizableColumns(amount: number, group: boolean): GridColumnWithMock
},
{
title: "Title",
width: 120,
group: group ? "Info" : undefined,
icon: GridColumnIcon.HeaderString,
hasMenu: false,
Expand All @@ -332,14 +326,13 @@ function getResizableColumns(amount: number, group: boolean): GridColumnWithMock
},
{
title: "More Info",
width: 120,
group: group ? "Info" : undefined,
icon: GridColumnIcon.HeaderUri,
hasMenu: false,
getContent: () => {
const url = faker.internet.url();
return {
kind: GridCellKind.Markdown,
kind: GridCellKind.Uri,
displayData: url,
data: url,
allowOverlay: true,
Expand Down Expand Up @@ -1219,7 +1212,7 @@ export const RearrangeColumns: React.VFC = () => {
// you should track indexes properly
const [sortableCols, setSortableCols] = React.useState(() => {
num = 200;
return cols.map(c => ({ ...c, width: c.width + (rand() % 100) }));
return cols.map(c => ({ ...c, width: (c.width ?? 150) + (rand() % 100) }));
});

const onColMoved = React.useCallback((startIndex: number, endIndex: number): void => {
Expand Down Expand Up @@ -2389,7 +2382,7 @@ a new line char ""more quotes"" plus a tab ." https://google.com`}
};

export const FreezeColumns: React.VFC = () => {
const { cols, getCellContent } = useMockDataGenerator(100);
const { cols, getCellContent, getCellsForSelection } = useMockDataGenerator(100);

return (
<BeautifulWrapper
Expand All @@ -2405,6 +2398,7 @@ export const FreezeColumns: React.VFC = () => {
rowMarkers="both"
freezeColumns={1}
getCellContent={getCellContent}
getCellsForSelection={getCellsForSelection}
columns={cols}
verticalBorder={c => c > 0}
rows={1_000}
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/data-editor/data-editor.test.tsx
Expand Up @@ -1194,7 +1194,7 @@ describe("data-editor", () => {

if (scroller !== null) {
jest.spyOn(scroller, "scrollWidth", "get").mockImplementation(() =>
basicProps.columns.map(c => c.width).reduce((pv, cv) => pv + cv, 0)
basicProps.columns.map(c => c.width ?? 150).reduce((pv, cv) => pv + cv, 0)
);
jest.spyOn(scroller, "scrollHeight", "get").mockImplementation(() => 1000 * 32 + 36);
jest.spyOn(scroller, "scrollLeft", "get").mockImplementation(() => 0);
Expand All @@ -1206,7 +1206,7 @@ describe("data-editor", () => {

if (scroller !== null) {
jest.spyOn(scroller, "scrollWidth", "get").mockImplementation(() =>
basicProps.columns.map(c => c.width).reduce((pv, cv) => pv + cv, 0)
basicProps.columns.map(c => c.width ?? 150).reduce((pv, cv) => pv + cv, 0)
);
jest.spyOn(scroller, "scrollHeight", "get").mockImplementation(() => 1000 * 32 + 36);
jest.spyOn(scroller, "scrollLeft", "get").mockImplementation(() => 0);
Expand Down Expand Up @@ -1236,7 +1236,7 @@ describe("data-editor", () => {

if (scroller !== null) {
jest.spyOn(scroller, "scrollWidth", "get").mockImplementation(() =>
basicProps.columns.map(c => c.width).reduce((pv, cv) => pv + cv, 0)
basicProps.columns.map(c => c.width ?? 150).reduce((pv, cv) => pv + cv, 0)
);
jest.spyOn(scroller, "scrollHeight", "get").mockImplementation(() => 1000 * 32 + 36);
jest.spyOn(scroller, "scrollLeft", "get").mockImplementation(() => 55);
Expand All @@ -1248,7 +1248,7 @@ describe("data-editor", () => {

if (scroller !== null) {
jest.spyOn(scroller, "scrollWidth", "get").mockImplementation(() =>
basicProps.columns.map(c => c.width).reduce((pv, cv) => pv + cv, 0)
basicProps.columns.map(c => c.width ?? 150).reduce((pv, cv) => pv + cv, 0)
);
jest.spyOn(scroller, "scrollHeight", "get").mockImplementation(() => 1000 * 32 + 36);
jest.spyOn(scroller, "scrollLeft", "get").mockImplementation(() => 0);
Expand Down
19 changes: 13 additions & 6 deletions packages/core/src/data-editor/data-editor.tsx
Expand Up @@ -26,6 +26,7 @@ import {
GridMouseCellEventArgs,
GridMouseHeaderEventArgs,
GridMouseGroupHeaderEventArgs,
GridColumn,
} from "../data-grid/data-grid-types";
import DataGridSearch, { DataGridSearchProps } from "../data-grid-search/data-grid-search";
import { browserIsOSX } from "../common/browser-detect";
Expand All @@ -37,6 +38,7 @@ import { useEventListener } from "../common/utils";
import { CellRenderers } from "../data-grid/cells";
import { isGroupEqual } from "../data-grid/data-grid-lib";
import { GroupRename } from "./group-rename";
import { useCellSizer } from "./use-cell-sizer";

interface MouseState {
readonly previousSelection?: GridSelection;
Expand All @@ -49,6 +51,7 @@ type Props = Omit<
| "cellXOffset"
| "cellYOffset"
| "className"
| "columns"
| "disabledRows"
| "drawCustomCell"
| "enableGroups"
Expand Down Expand Up @@ -118,6 +121,8 @@ export interface DataEditorProps extends Props {
readonly onGroupHeaderContextMenu?: (colIndex: number, event: GroupHeaderClickedEventArgs) => void;
readonly onCellContextMenu?: (cell: readonly [number, number], event: CellClickedEventArgs) => void;

readonly columns: readonly GridColumn[];

readonly trailingRowOptions?: {
readonly tint?: boolean;
readonly hint?: string;
Expand Down Expand Up @@ -230,7 +235,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
} = p;

const {
columns,
columns: columnsIn,
rows,
getCellContent,
onCellClicked,
Expand Down Expand Up @@ -397,6 +402,13 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
[rowMarkerOffset, setSelectedColumnsOuter]
);

const theme = useTheme();
const mergedTheme = React.useMemo(() => {
return { ...getDataEditorTheme(), ...theme };
}, [theme]);

const columns = useCellSizer(columnsIn, rows, getCellsForSelection, mergedTheme);

const enableGroups = React.useMemo(() => {
return columns.some(c => c.group !== undefined);
}, [columns]);
Expand Down Expand Up @@ -1904,11 +1916,6 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
return CompactSelection.empty();
}, [mangledRows, showTrailingBlankRow, trailingRowOptions?.tint]);

const theme = useTheme();
const mergedTheme = React.useMemo(() => {
return { ...getDataEditorTheme(), ...theme };
}, [theme]);

const mangledVerticalBorder = React.useCallback(
(col: number) => {
return typeof verticalBorder === "boolean"
Expand Down
71 changes: 71 additions & 0 deletions packages/core/src/data-editor/use-cell-sizer.ts
@@ -0,0 +1,71 @@
import { GridCell, GridCellKind, GridColumn, SizedGridColumn } from "../data-grid/data-grid-types";
import { DataEditorProps } from "./data-editor";
import * as React from "react";
import { CellRenderers } from "../data-grid/cells";
import { Theme } from "../common/styles";

function measureCell(ctx: CanvasRenderingContext2D, cell: GridCell): number {
if (cell.kind === GridCellKind.Custom) return 150;

const r = CellRenderers[cell.kind];
return r?.measure(ctx, cell) ?? 150;
}

export function useCellSizer(
columns: readonly GridColumn[],
rows: number,
getCellsForSelection: DataEditorProps["getCellsForSelection"],
theme: Theme
): readonly SizedGridColumn[] {
return React.useMemo(() => {
if (!columns.some(c => c.width === undefined)) {
return columns as SizedGridColumn[];
}

const offscreen = document.createElement("canvas");
const ctx = offscreen.getContext("2d", {
alpha: false,
});
if (ctx === null) {
return columns.map(c => {
if (c.width !== undefined) return c;

return {
...c,
width: 150,
};
}) as SizedGridColumn[];
}

ctx.font = `${theme.baseFontStyle} ${theme.fontFamily}`;

const cells = getCellsForSelection?.({
x: 0,
y: 0,
width: columns.length,
height: Math.min(rows, 10),
});

return columns.map((c, colIndex) => {
if (c.width !== undefined) return c;

if (cells === undefined) {
return {
...c,
width: 150,
};
}

const sizes = cells.map(row => row[colIndex]).map(cell => measureCell(ctx, cell));
sizes.push(ctx.measureText(c.title).width + 16 + (c.icon === undefined ? 0 : 28));
const average = sizes.reduce((a, b) => a + b) / sizes.length;
const biggest = sizes.reduce((a, acc) => (a > average * 2 ? acc : Math.max(acc, a)));

return {
...c,
width: biggest,
};
}) as SizedGridColumn[];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [columns]);
}
1 change: 1 addition & 0 deletions packages/core/src/data-grid/cells/boolean-cell.tsx
Expand Up @@ -8,6 +8,7 @@ export const booleanCellRenderer: InternalCellRenderer<BooleanCell> = {
needsHover: true,
useLabel: false,
needsHoverPosition: true,
measure: () => 50,
render: a => drawBoolean(a, a.cell.data, a.cell.allowEdit),
onDelete: c => ({
...c,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/data-grid/cells/bubble-cell.tsx
Expand Up @@ -11,6 +11,7 @@ export const bubbleCellRenderer: InternalCellRenderer<BubbleCell> = {
needsHover: false,
useLabel: false,
needsHoverPosition: false,
measure: (ctx, cell) => cell.data.reduce((acc, data) => ctx.measureText(data).width + acc, 0) + 16,
render: a => drawBubbles(a, a.cell.data),
getEditor: () => p => {
const { onKeyDown, value } = p;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/data-grid/cells/cell-types.ts
Expand Up @@ -54,6 +54,7 @@ export interface InternalCellRenderer<T extends InnerGridCell> {
readonly needsHover: boolean;
readonly needsHoverPosition: boolean;
readonly useLabel?: boolean;
readonly measure: (ctx: CanvasRenderingContext2D, cell: T) => number;
readonly onClick?: (cell: T, posX: number, posY: number, bounds: Rectangle) => T | undefined;
readonly onDelete?: (cell: T) => T | undefined;
readonly getAccessibilityString: (cell: T) => string;
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/data-grid/cells/drilldown-cell.tsx
Expand Up @@ -11,6 +11,14 @@ export const drilldownCellRenderer: InternalCellRenderer<DrilldownCell> = {
needsHover: false,
useLabel: false,
needsHoverPosition: false,
measure: (ctx, cell) => {
return (
cell.data.reduce(
(acc, data) => ctx.measureText(data.text).width + (data.img === undefined ? 0 : 20) + acc,
0
) + 16
);
},
render: a => drawDrilldownCell(a, a.cell.data),
getEditor: () => p => {
const { onKeyDown, value } = p;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/data-grid/cells/image-cell.tsx
Expand Up @@ -12,6 +12,7 @@ export const imageCellRenderer: InternalCellRenderer<ImageCell> = {
useLabel: false,
needsHoverPosition: false,
render: a => drawImage(a, a.cell.displayData ?? a.cell.data),
measure: (_ctx, cell) => cell.data.length * 50,
onDelete: c => ({
...c,
data: [],
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/data-grid/cells/loading-cell.tsx
Expand Up @@ -7,5 +7,6 @@ export const loadingCellRenderer: InternalCellRenderer<LoadingCell> = {
needsHover: false,
useLabel: false,
needsHoverPosition: false,
measure: () => 150,
render: () => undefined,
};
1 change: 1 addition & 0 deletions packages/core/src/data-grid/cells/markdown-cell.tsx
Expand Up @@ -11,6 +11,7 @@ export const markdownCellRenderer: InternalCellRenderer<MarkdownCell> = {
needsHover: false,
needsHoverPosition: false,
renderPrep: prepTextCell,
measure: () => 200,
render: a => drawTextCell(a, a.cell.data),
onDelete: c => ({
...c,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/data-grid/cells/marker-cell.tsx
Expand Up @@ -8,5 +8,6 @@ export const markerCellRenderer: InternalCellRenderer<MarkerCell> = {
needsHover: true,
needsHoverPosition: false,
renderPrep: prepMarkerRowCell,
measure: () => 44,
render: a => drawMarkerRowCell(a, a.cell.row, a.cell.checked, a.cell.markerKind),
};
1 change: 1 addition & 0 deletions packages/core/src/data-grid/cells/new-row-cell.tsx
Expand Up @@ -7,5 +7,6 @@ export const newRowCellRenderer: InternalCellRenderer<NewRowCell> = {
kind: InnerGridCellKind.NewRow,
needsHover: true,
needsHoverPosition: false,
measure: () => 200,
render: a => drawNewRowCell(a, a.cell.hint, a.cell.icon),
};
1 change: 1 addition & 0 deletions packages/core/src/data-grid/cells/number-cell.tsx
Expand Up @@ -13,6 +13,7 @@ export const numberCellRenderer: InternalCellRenderer<NumberCell> = {
useLabel: true,
renderPrep: prepTextCell,
render: a => drawTextCell(a, a.cell.displayData),
measure: (ctx, cell) => ctx.measureText(cell.displayData).width + 16,
onDelete: c => ({
...c,
data: undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/data-grid/cells/protected-cell.tsx
Expand Up @@ -4,6 +4,7 @@ import { InternalCellRenderer } from "./cell-types";

export const protectedCellRenderer: InternalCellRenderer<ProtectedCell> = {
getAccessibilityString: () => "",
measure: () => 150,
kind: GridCellKind.Protected,
needsHover: false,
needsHoverPosition: false,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/data-grid/cells/row-id-cell.tsx
Expand Up @@ -9,4 +9,5 @@ export const rowIDCellRenderer: InternalCellRenderer<RowIDCell> = {
needsHoverPosition: false,
renderPrep: a => prepTextCell(a, a.theme.textLight),
render: a => drawTextCell(a, a.cell.data),
measure: (ctx, cell) => ctx.measureText(cell.data).width + 16,
};
1 change: 1 addition & 0 deletions packages/core/src/data-grid/cells/text-cell.tsx
Expand Up @@ -13,6 +13,7 @@ export const textCellRenderer: InternalCellRenderer<TextCell> = {
renderPrep: prepTextCell,
useLabel: true,
render: a => drawTextCell(a, a.cell.displayData),
measure: (ctx, cell) => ctx.measureText(cell.displayData).width + 16,
onDelete: c => ({
...c,
data: "",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/data-grid/cells/uri-cell.tsx
Expand Up @@ -13,6 +13,7 @@ export const uriCellRenderer: InternalCellRenderer<UriCell> = {
useLabel: true,
renderPrep: prepTextCell,
render: a => drawTextCell(a, a.cell.data),
measure: (ctx, cell) => ctx.measureText(cell.data).width + 16,
onDelete: c => ({
...c,
data: "",
Expand Down

0 comments on commit 18c21f7

Please sign in to comment.