Skip to content

Commit

Permalink
[Table] Redux: resizeRowsByApproximateHeight instance method (#1718)
Browse files Browse the repository at this point in the history
* Prototype: resizeRowsByApproximateHeight

* Better implementation

* Better implementation

* Add margin between consecutive buttons in dev preview

* Fix import paths

* Write basic test

* Use an object param instead

* Make the code more concise

* Fix type error
  • Loading branch information
cmslewis authored Oct 16, 2017
1 parent 7d88ba4 commit 2b5bb23
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 7 deletions.
4 changes: 4 additions & 0 deletions packages/table/preview/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions packages/table/preview/mutableTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})}

<h4>Cells</h4>
<h6>Display</h6>
Expand Down Expand Up @@ -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
// =============

Expand Down
2 changes: 1 addition & 1 deletion packages/table/src/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
125 changes: 124 additions & 1 deletion packages/table/src/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<number>;

/**
* Approximate height (in pixels) of an average line of text.
*/
getApproximateLineHeight?: number | ICellMapper<number>;

/**
* Sum of horizontal paddings (in pixels) from the left __and__ right sides
* of the cell.
*/
getCellHorizontalPadding?: number | ICellMapper<number>;

/**
* Number of extra lines to add in case the calculation is imperfect.
*/
getNumBufferLines?: number | ICellMapper<number>;
}

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
Expand Down Expand Up @@ -396,6 +426,18 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {
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<keyof ITableProps>;
Expand Down Expand Up @@ -490,6 +532,63 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {
// 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<string>,
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.
Expand Down Expand Up @@ -2023,6 +2122,30 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {
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) {
Expand Down
5 changes: 1 addition & 4 deletions packages/table/test/formatsTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -183,7 +184,3 @@ describe("Formats", () => {
});
});
});

function createStringOfLength(length: number) {
return new Array(length).fill("a").join("");
}
4 changes: 4 additions & 0 deletions packages/table/test/mocks/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
49 changes: 48 additions & 1 deletion packages/table/test/tableTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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("<Table>", () => {
const COLUMN_HEADER_SELECTOR = `.${Classes.TABLE_QUADRANT_MAIN} .${Classes.TABLE_COLUMN_HEADERS} .${Classes.TABLE_HEADER}`;
Expand Down Expand Up @@ -146,6 +146,53 @@ describe("<Table>", () => {
});

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 <Cell wrapText={true}>{getCellText(rowIndex)}</Cell>;
};

let table: Table;
const saveTable = (t: Table) => (table = t);

beforeEach(() => {
harness.mount(
<Table ref={saveTable} numRows={NUM_ROWS}>
<Column name="Column0" renderCell={renderCell} />
<Column name="Column1" renderCell={renderCell} />
</Table>,
);
});

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;
Expand Down

1 comment on commit 2b5bb23

@blueprint-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Table] Redux: `resizeRowsByApproximateHeight` instance method (#1718)

Preview: documentation
Coverage: core | datetime

Please sign in to comment.