diff --git a/packages/table/preview/index.html b/packages/table/preview/index.html
index e8ccfab06b..795dd13f48 100644
--- a/packages/table/preview/index.html
+++ b/packages/table/preview/index.html
@@ -126,6 +126,10 @@
margin-left: 15px;
}
+ .sidebar > .pt-button + .pt-button {
+ margin-top: 3px;
+ }
+
label.tbl-select-label {
margin-top: -3px;
margin-bottom: 7px;
diff --git a/packages/table/preview/mutableTable.tsx b/packages/table/preview/mutableTable.tsx
index b43caaae6f..79167420b5 100644
--- a/packages/table/preview/mutableTable.tsx
+++ b/packages/table/preview/mutableTable.tsx
@@ -647,6 +647,9 @@ export class MutableTable extends React.Component<{}, IMutableTableState> {
{this.renderButton("Resize rows by tallest cell", {
onClick: this.handleResizeRowsByTallestCellButtonClick,
})}
+ {this.renderButton("Resize rows by approx height", {
+ onClick: this.handleResizeRowsByApproxHeightButtonClick,
+ })}
Cells
Display
@@ -989,6 +992,15 @@ export class MutableTable extends React.Component<{}, IMutableTableState> {
this.tableInstance.resizeRowsByTallestCell();
};
+ private handleResizeRowsByApproxHeightButtonClick = () => {
+ this.tableInstance.resizeRowsByApproximateHeight(this.getCellText);
+ };
+
+ private getCellText = (rowIndex: number, columnIndex: number) => {
+ const content = this.store.get(rowIndex, columnIndex);
+ return this.state.cellContent === CellContent.LARGE_JSON ? JSON.stringify(content) : content;
+ };
+
// State updates
// =============
diff --git a/packages/table/src/locator.ts b/packages/table/src/locator.ts
index 915045e87e..63020df07c 100644
--- a/packages/table/src/locator.ts
+++ b/packages/table/src/locator.ts
@@ -47,7 +47,7 @@ export interface ILocator {
}
export class Locator implements ILocator {
- private static CELL_HORIZONTAL_PADDING = 10;
+ public static CELL_HORIZONTAL_PADDING = 10;
private grid: Grid;
diff --git a/packages/table/src/table.tsx b/packages/table/src/table.tsx
index e9bd02167f..473d3567a5 100644
--- a/packages/table/src/table.tsx
+++ b/packages/table/src/table.tsx
@@ -16,7 +16,7 @@ import * as Classes from "./common/classes";
import { Clipboard } from "./common/clipboard";
import { Direction } from "./common/direction";
import * as Errors from "./common/errors";
-import { Grid, IColumnIndices, IRowIndices } from "./common/grid";
+import { Grid, ICellMapper, IColumnIndices, IRowIndices } from "./common/grid";
import * as FocusedCellUtils from "./common/internal/focusedCellUtils";
import * as ScrollUtils from "./common/internal/scrollUtils";
import * as SelectionUtils from "./common/internal/selectionUtils";
@@ -46,6 +46,36 @@ import {
} from "./regions";
import { TableBody } from "./tableBody";
+export interface IResizeRowsByApproximateHeightOptions {
+ /**
+ * Approximate width (in pixels) of an average character of text.
+ */
+ getApproximateCharWidth?: number | ICellMapper;
+
+ /**
+ * Approximate height (in pixels) of an average line of text.
+ */
+ getApproximateLineHeight?: number | ICellMapper;
+
+ /**
+ * Sum of horizontal paddings (in pixels) from the left __and__ right sides
+ * of the cell.
+ */
+ getCellHorizontalPadding?: number | ICellMapper;
+
+ /**
+ * Number of extra lines to add in case the calculation is imperfect.
+ */
+ getNumBufferLines?: number | ICellMapper;
+}
+
+interface IResizeRowsByApproximateHeightResolvedOptions {
+ getApproximateCharWidth?: number;
+ getApproximateLineHeight?: number;
+ getCellHorizontalPadding?: number;
+ getNumBufferLines?: number;
+}
+
export interface ITableProps extends IProps, IRowHeights, IColumnWidths {
/**
* If `false`, only a single region of a single column/row/cell may be
@@ -396,6 +426,18 @@ export class Table extends AbstractComponent {
selectionModes: SelectionModes.ALL,
};
+ // these default values for `resizeRowsByApproximateHeight` have been
+ // fine-tuned to work well with default Table font styles.
+ private static resizeRowsByApproximateHeightDefaults: Record<
+ keyof IResizeRowsByApproximateHeightOptions,
+ number
+ > = {
+ getApproximateCharWidth: 8,
+ getApproximateLineHeight: 18,
+ getCellHorizontalPadding: 2 * Locator.CELL_HORIZONTAL_PADDING,
+ getNumBufferLines: 1,
+ };
+
private static SHALLOW_COMPARE_PROP_KEYS_BLACKLIST = [
"selectedRegions", // (intentionally omitted; can be deeply compared to save on re-renders in controlled mode)
] as Array;
@@ -490,6 +532,63 @@ export class Table extends AbstractComponent {
// Instance methods
// ================
+ /**
+ * __Experimental!__ Resizes all rows in the table to the approximate
+ * maximum height of wrapped cell content in each row. Works best when each
+ * cell contains plain text of a consistent font style (though font style
+ * may vary between cells). Since this function uses approximate
+ * measurements, results may not be perfect.
+ *
+ * Approximation parameters can be configured for the entire table or on a
+ * per-cell basis. Default values are fine-tuned to work well with default
+ * Table font styles.
+ */
+ public resizeRowsByApproximateHeight(
+ getCellText: ICellMapper,
+ options?: IResizeRowsByApproximateHeightOptions,
+ ) {
+ const { numRows } = this.props;
+ const { columnWidths } = this.state;
+ const numColumns = columnWidths.length;
+
+ const rowHeights: number[] = [];
+
+ for (let rowIndex = 0; rowIndex < numRows; rowIndex++) {
+ let maxCellHeightInRow = 0;
+
+ // iterate through each cell in the row
+ for (let columnIndex = 0; columnIndex < numColumns; columnIndex++) {
+ // resolve all parameters to raw values
+ const {
+ getApproximateCharWidth: approxCharWidth,
+ getApproximateLineHeight: approxLineHeight,
+ getCellHorizontalPadding: horizontalPadding,
+ getNumBufferLines: numBufferLines,
+ } = this.resolveResizeRowsByApproximateHeightOptions(options, rowIndex, columnIndex);
+
+ const cellText = getCellText(rowIndex, columnIndex);
+ const numCharsInCell = cellText == null ? 0 : cellText.length;
+
+ const actualCellWidth = columnWidths[columnIndex];
+ const availableCellWidth = actualCellWidth - horizontalPadding;
+ const approxCharsPerLine = availableCellWidth / approxCharWidth;
+ const approxNumLinesDesired = Math.ceil(numCharsInCell / approxCharsPerLine) + numBufferLines;
+
+ const approxCellHeight = approxNumLinesDesired * approxLineHeight;
+
+ if (approxCellHeight > maxCellHeightInRow) {
+ maxCellHeightInRow = approxCellHeight;
+ }
+ }
+
+ rowHeights.push(maxCellHeightInRow);
+ }
+
+ this.invalidateGrid();
+ this.didUpdateColumnOrRowSizes = true;
+ this.setState({ rowHeights });
+ }
+
/**
* Resize all rows in the table to the height of the tallest visible cell in the specified columns.
* If no indices are provided, default to using the tallest visible cell from all columns in view.
@@ -2023,6 +2122,30 @@ export class Table extends AbstractComponent {
private handleRowResizeGuide = (horizontalGuides: number[]) => {
this.setState({ horizontalGuides });
};
+
+ /**
+ * Returns an object with option keys mapped to their resolved values
+ * (falling back to default values as necessary).
+ */
+ private resolveResizeRowsByApproximateHeightOptions(
+ options: IResizeRowsByApproximateHeightOptions | null | undefined,
+ rowIndex: number,
+ columnIndex: number,
+ ) {
+ const optionKeys = Object.keys(Table.resizeRowsByApproximateHeightDefaults);
+ const optionReducer = (
+ agg: IResizeRowsByApproximateHeightResolvedOptions,
+ key: keyof IResizeRowsByApproximateHeightOptions,
+ ) => {
+ agg[key] =
+ options != null && options[key] != null
+ ? CoreUtils.safeInvokeOrValue(options[key], rowIndex, columnIndex)
+ : Table.resizeRowsByApproximateHeightDefaults[key];
+ return agg;
+ };
+ const resolvedOptions: IResizeRowsByApproximateHeightResolvedOptions = optionKeys.reduce(optionReducer, {});
+ return resolvedOptions;
+ }
}
function clampNumFrozenColumns(props: ITableProps) {
diff --git a/packages/table/test/formatsTests.tsx b/packages/table/test/formatsTests.tsx
index 5dc7fef7ba..4854157783 100644
--- a/packages/table/test/formatsTests.tsx
+++ b/packages/table/test/formatsTests.tsx
@@ -11,6 +11,7 @@ import { JSONFormat } from "../src/cell/formats/jsonFormat";
import { TruncatedFormat, TruncatedPopoverMode } from "../src/cell/formats/truncatedFormat";
import * as Classes from "../src/common/classes";
import { ReactHarness } from "./harness";
+import { createStringOfLength } from "./mocks/table";
describe("Formats", () => {
const harness = new ReactHarness();
@@ -183,7 +184,3 @@ describe("Formats", () => {
});
});
});
-
-function createStringOfLength(length: number) {
- return new Array(length).fill("a").join("");
-}
diff --git a/packages/table/test/mocks/table.tsx b/packages/table/test/mocks/table.tsx
index 5a06e7096e..0f90347a79 100644
--- a/packages/table/test/mocks/table.tsx
+++ b/packages/table/test/mocks/table.tsx
@@ -9,6 +9,10 @@ import * as React from "react";
import { Cell, Column, IColumnProps, ITableProps, RenderMode, Table, Utils } from "../../src";
+export function createStringOfLength(length: number) {
+ return new Array(length).fill("a").join("");
+}
+
export function createTableOfSize(numColumns: number, numRows: number, columnProps?: any, tableProps?: any) {
const columns = Utils.times(numColumns, Utils.toBase26Alpha);
const data = Utils.times(numRows, (row: number) => {
diff --git a/packages/table/test/tableTests.tsx b/packages/table/test/tableTests.tsx
index fd2c33bb10..5aaa636c17 100644
--- a/packages/table/test/tableTests.tsx
+++ b/packages/table/test/tableTests.tsx
@@ -26,7 +26,7 @@ import { IRegion, Regions } from "../src/regions";
import { ITableState } from "../src/table";
import { CellType, expectCellLoading } from "./cellTestUtils";
import { ElementHarness, ReactHarness } from "./harness";
-import { createTableOfSize } from "./mocks/table";
+import { createStringOfLength, createTableOfSize } from "./mocks/table";
describe("", () => {
const COLUMN_HEADER_SELECTOR = `.${Classes.TABLE_QUADRANT_MAIN} .${Classes.TABLE_COLUMN_HEADERS} .${Classes.TABLE_HEADER}`;
@@ -146,6 +146,53 @@ describe("", () => {
});
describe("Instance methods", () => {
+ describe("resizeRowsByApproximateHeight", () => {
+ const STR_LENGTH_SHORT = 10;
+ const STR_LENGTH_LONG = 100;
+ const NUM_ROWS = 4;
+
+ const cellTextShort = createStringOfLength(STR_LENGTH_SHORT);
+ const cellTextLong = createStringOfLength(STR_LENGTH_LONG);
+
+ const getCellText = (rowIndex: number) => {
+ return rowIndex === 0 ? cellTextShort : cellTextLong;
+ };
+ const renderCell = (rowIndex: number) => {
+ return {getCellText(rowIndex)} | ;
+ };
+
+ let table: Table;
+ const saveTable = (t: Table) => (table = t);
+
+ beforeEach(() => {
+ harness.mount(
+ ,
+ );
+ });
+
+ afterEach(() => {
+ table = undefined;
+ });
+
+ it("resizes each row to fit its respective tallest cell", () => {
+ table.resizeRowsByApproximateHeight(getCellText);
+ expect(table.state.rowHeights).to.deep.equal([36, 144, 144, 144]);
+ });
+
+ it("still uses defaults if an empty `options` object is passed", () => {
+ table.resizeRowsByApproximateHeight(getCellText, {});
+ expect(table.state.rowHeights).to.deep.equal([36, 144, 144, 144]);
+ });
+
+ it("can customize options", () => {
+ table.resizeRowsByApproximateHeight(getCellText, { getNumBufferLines: 2 });
+ expect(table.state.rowHeights).to.deep.equal([54, 162, 162, 162]);
+ });
+ });
+
describe("resizeRowsByTallestCell", () => {
it("Gets and sets the tallest cell by columns correctly", () => {
const DEFAULT_RESIZE_HEIGHT = 20;