diff --git a/packages/table/preview/index.html b/packages/table/preview/index.html
index 1d4765d5db..d08a8a7a47 100644
--- a/packages/table/preview/index.html
+++ b/packages/table/preview/index.html
@@ -78,6 +78,10 @@
All Selection Modes, Body Context Menu
+ Row Transformed Controlled Selection
+
+
+
Ghost Inline
diff --git a/packages/table/preview/index.tsx b/packages/table/preview/index.tsx
index 7deeee16a8..7f06b339fd 100644
--- a/packages/table/preview/index.tsx
+++ b/packages/table/preview/index.tsx
@@ -9,7 +9,14 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
-import { Intent, Menu, MenuItem, MenuDivider } from "@blueprintjs/core";
+
+import {
+ Button,
+ Intent,
+ Menu,
+ MenuItem,
+ MenuDivider,
+} from "@blueprintjs/core";
import {
Cell,
@@ -20,6 +27,7 @@ import {
EditableName,
IColumnHeaderCellProps,
IColumnProps,
+ ICoordinateData,
HorizontalCellDivider,
IMenuContext,
IRegion,
@@ -257,6 +265,52 @@ ReactDOM.render(
document.getElementById("table-big")
);
+class RowSelectableTable extends React.Component<{}, {}> {
+ public state = {
+ selectedRegions: [ Regions.row(2) ],
+ };
+
+ public render() {
+ return ();
+ }
+
+ private handleClear = () => {
+ this.setState({ selectedRegions: [] });
+ }
+
+ private handleSelection = (selectedRegions: IRegion[]) => {
+ this.setState({ selectedRegions });
+ }
+
+ private selectedRegionTransform = (region: IRegion) => {
+ // convert cell selection to row selection
+ if (Regions.getRegionCardinality(region) === RegionCardinality.CELLS) {
+ return Regions.row(region.rows[0], region.rows[1]);
+ }
+
+ return region;
+ }
+}
+
+ReactDOM.render(
+ ,
+ document.getElementById("table-select-rows")
+);
+
document.getElementById("table-ledger").classList.add("bp-table-striped");
ReactDOM.render(
diff --git a/packages/table/src/headers/columnHeader.tsx b/packages/table/src/headers/columnHeader.tsx
index 75eed4d107..747385971f 100644
--- a/packages/table/src/headers/columnHeader.tsx
+++ b/packages/table/src/headers/columnHeader.tsx
@@ -129,6 +129,7 @@ export class ColumnHeader extends React.Component {
onResizeGuide,
onSelection,
selectedRegions,
+ selectedRegionTransform,
} = this.props;
const rect = grid.getColumnRect(columnIndex);
@@ -162,6 +163,7 @@ export class ColumnHeader extends React.Component {
locateDrag={this.locateDrag}
onSelection={onSelection}
selectedRegions={selectedRegions}
+ selectedRegionTransform={selectedRegionTransform}
>
{
onSelection,
renderRowHeader,
selectedRegions,
+ selectedRegionTransform,
} = this.props;
const rect = grid.getRowRect(rowIndex);
@@ -155,6 +156,7 @@ export class RowHeader extends React.Component {
locateDrag={this.locateDrag}
onSelection={onSelection}
selectedRegions={selectedRegions}
+ selectedRegionTransform={selectedRegionTransform}
>
IRegion;
+
export interface ISelectableProps {
/**
* If `false`, only a single region of a single column/row/cell may be
@@ -20,17 +22,27 @@ export interface ISelectableProps {
*/
allowMultipleSelection: boolean;
+ /**
+ * When the user selects something, this callback is called with a new
+ * array of Regions. This array should be considered the new selection
+ * state for the entire table.
+ */
+ onSelection: (regions: IRegion[]) => void;
+
/**
* An array containing the table's selection Regions.
*/
selectedRegions: IRegion[];
/**
- * When the user selects something, this callback is called with a new
- * array of Regions. This array should be considered the new selection
- * state for the entire table.
+ * An optional transform function that will be applied to the located
+ * `Region`.
+ *
+ * This allows you to, for example, convert cell `Region`s into row
+ * `Region`s while maintaining the existing multi-select and meta-click
+ * functionality.
*/
- onSelection: (regions: IRegion[]) => void;
+ selectedRegionTransform?: ISelectedRegionTransform;
}
export interface IDragSelectableProps extends ISelectableProps {
@@ -76,12 +88,16 @@ export class DragSelectable extends React.Component {
return false;
}
- const region = this.props.locateClick(event);
+ let region = this.props.locateClick(event);
if (!Regions.isValid(region)) {
return false;
}
+ if (this.props.selectedRegionTransform != null) {
+ region = this.props.selectedRegionTransform(region, event);
+ }
+
const foundIndex = Regions.findMatchingRegion(this.props.selectedRegions, region);
if (foundIndex !== -1) {
// If re-clicking on an existing region, we either carefully
@@ -107,7 +123,7 @@ export class DragSelectable extends React.Component {
}
private handleDragMove = (event: MouseEvent, coords: ICoordinateData) => {
- const region = (this.props.allowMultipleSelection) ?
+ let region = (this.props.allowMultipleSelection) ?
this.props.locateDrag(event, coords) :
this.props.locateClick(event);
@@ -115,6 +131,10 @@ export class DragSelectable extends React.Component {
return;
}
+ if (this.props.selectedRegionTransform != null) {
+ region = this.props.selectedRegionTransform(region, event, coords);
+ }
+
this.props.onSelection(Regions.update(this.props.selectedRegions, region));
}
@@ -123,13 +143,17 @@ export class DragSelectable extends React.Component {
return false;
}
- const region = this.props.locateClick(event);
+ let region = this.props.locateClick(event);
if (!Regions.isValid(region)) {
this.props.onSelection([]);
return false;
}
+ if (this.props.selectedRegionTransform != null) {
+ region = this.props.selectedRegionTransform(region, event);
+ }
+
if (this.props.selectedRegions.length > 0) {
this.props.onSelection(Regions.update(this.props.selectedRegions, region));
} else {
diff --git a/packages/table/src/table.tsx b/packages/table/src/table.tsx
index 77f0beb1d5..f8011e8c7b 100644
--- a/packages/table/src/table.tsx
+++ b/packages/table/src/table.tsx
@@ -20,6 +20,7 @@ import { IRowHeaderRenderer, IRowHeights, renderDefaultRowHeader, RowHeader } fr
import { IContextMenuRenderer } from "./interactions/menus";
import { IIndexedResizeCallback } from "./interactions/resizable";
import { ResizeSensor } from "./interactions/resizeSensor";
+import { ISelectedRegionTransform } from "./interactions/selectable";
import { GuideLayer } from "./layers/guides";
import { IRegionStyler, RegionLayer } from "./layers/regions";
import { Locator } from "./locator";
@@ -119,6 +120,29 @@ export interface ITableProps extends IProps, IRowHeights, IColumnWidths {
*/
numRows?: number;
+ /**
+ * If defined, will set the selected regions in the cells. If defined, this
+ * changes table selection to "controlled" mode, meaning you in charge of
+ * setting the selections in response to events in the `onSelection`
+ * callback.
+ *
+ * Note that the `selectionModes` prop controls which types of events are
+ * triggered to the `onSelection` callback, but does not restrict what
+ * selection you can pass to the `selectedRegions` prop. Therefore you can,
+ * for example, convert cell clicks into row selections.
+ */
+ selectedRegions?: IRegion[];
+
+ /**
+ * An optional transform function that will be applied to the located
+ * `Region`.
+ *
+ * This allows you to, for example, convert cell `Region`s into row
+ * `Region`s while maintaining the existing multi-select and meta-click
+ * functionality.
+ */
+ selectedRegionTransform?: ISelectedRegionTransform;
+
/**
* A `SelectionModes` enum value indicating the selection mode. You may
* equivalently provide an array of `RegionCardinality` enum values for
@@ -254,16 +278,26 @@ export class Table extends AbstractComponent {
let newRowHeights = Utils.times(numRows, () => defaultRowHeight);
newRowHeights = Utils.assignSparseValues(newRowHeights, rowHeights);
+ const selectedRegions = (props.selectedRegions == null) ? [] as IRegion[] : props.selectedRegions;
+
this.state = {
columnWidths: newColumnWidths,
isLayoutLocked: false,
rowHeights: newRowHeights,
- selectedRegions: [],
+ selectedRegions,
};
}
public componentWillReceiveProps(nextProps: ITableProps) {
- const { defaultRowHeight, defaultColumnWidth, columnWidths, rowHeights, children, numRows } = nextProps;
+ const {
+ defaultRowHeight,
+ defaultColumnWidth,
+ columnWidths,
+ rowHeights,
+ children,
+ numRows,
+ selectedRegions,
+ } = nextProps;
const newChildArray = React.Children.toArray(children) as Array>;
// Try to maintain widths of columns by looking up the width of the
@@ -286,12 +320,15 @@ export class Table extends AbstractComponent {
newRowHeights = Utils.arrayOfLength(newRowHeights, numRows, defaultRowHeight);
newRowHeights = Utils.assignSparseValues(newRowHeights, rowHeights);
+ const newselectedRegions = (selectedRegions == null) ? this.state.selectedRegions : selectedRegions;
+
this.childrenArray = newChildArray;
this.columnIdToIndex = Table.createColumnIdIndex(this.childrenArray);
this.invalidateGrid();
this.setState({
columnWidths: newColumnWidths,
rowHeights: newRowHeights,
+ selectedRegions: newselectedRegions,
});
}
@@ -414,6 +451,7 @@ export class Table extends AbstractComponent {
isColumnResizable,
maxColumnWidth,
minColumnWidth,
+ selectedRegionTransform,
} = this.props;
const classes = classNames("bp-table-column-headers", {
"bp-table-selection-enabled": this.isSelectionModeEnabled(RegionCardinality.FULL_COLUMNS),
@@ -435,6 +473,7 @@ export class Table extends AbstractComponent {
onResizeGuide={this.handleColumnResizeGuide}
onSelection={this.getEnabledSelectionHandler(RegionCardinality.FULL_COLUMNS)}
selectedRegions={selectedRegions}
+ selectedRegionTransform={selectedRegionTransform}
viewportRect={viewportRect}
{...columnIndices}
>
@@ -456,6 +495,7 @@ export class Table extends AbstractComponent {
maxRowHeight,
minRowHeight,
renderRowHeader,
+ selectedRegionTransform,
} = this.props;
const classes = classNames("bp-table-row-headers", {
"bp-table-selection-enabled": this.isSelectionModeEnabled(RegionCardinality.FULL_ROWS),
@@ -479,6 +519,7 @@ export class Table extends AbstractComponent {
onSelection={this.getEnabledSelectionHandler(RegionCardinality.FULL_ROWS)}
renderRowHeader={renderRowHeader}
selectedRegions={selectedRegions}
+ selectedRegionTransform={selectedRegionTransform}
viewportRect={viewportRect}
{...rowIndices}
/>
@@ -494,7 +535,12 @@ export class Table extends AbstractComponent {
private renderBody() {
const { grid } = this;
- const { allowMultipleSelection, fillBodyWithGhostCells, renderBodyContextMenu } = this.props;
+ const {
+ allowMultipleSelection,
+ fillBodyWithGhostCells,
+ renderBodyContextMenu,
+ selectedRegionTransform,
+ } = this.props;
const { locator, selectedRegions, viewportRect, verticalGuides, horizontalGuides } = this.state;
const style = grid.getRect().sizeStyle();
@@ -528,6 +574,7 @@ export class Table extends AbstractComponent {
onSelection={this.getEnabledSelectionHandler(RegionCardinality.CELLS)}
renderBodyContextMenu={renderBodyContextMenu}
selectedRegions={selectedRegions}
+ selectedRegionTransform={selectedRegionTransform}
viewportRect={viewportRect}
{...rowIndices}
{...columnIndices}
@@ -741,7 +788,10 @@ export class Table extends AbstractComponent {
}
private handleSelection = (selectedRegions: IRegion[]) => {
- this.setState({ selectedRegions } as ITableState);
+ // only set selectedRegions state if not specified in props
+ if (this.props.selectedRegions == null) {
+ this.setState({ selectedRegions } as ITableState);
+ }
const { onSelection } = this.props;
if (onSelection != null) {
diff --git a/packages/table/src/tableBody.tsx b/packages/table/src/tableBody.tsx
index 27f57bda89..26eddc3fdb 100644
--- a/packages/table/src/tableBody.tsx
+++ b/packages/table/src/tableBody.tsx
@@ -141,7 +141,14 @@ export class TableBody extends React.Component {
}
private renderCell = (rowIndex: number, columnIndex: number, extremaClasses: string[]) => {
- const { allowMultipleSelection, grid, cellRenderer, selectedRegions, onSelection } = this.props;
+ const {
+ allowMultipleSelection,
+ grid,
+ cellRenderer,
+ onSelection,
+ selectedRegions,
+ selectedRegionTransform,
+ } = this.props;
const cell = Utils.assignClasses(
cellRenderer(rowIndex, columnIndex),
TableBody.cellClassNames(rowIndex, columnIndex),
@@ -160,6 +167,7 @@ export class TableBody extends React.Component {
locateDrag={this.locateDrag}
onSelection={onSelection}
selectedRegions={selectedRegions}
+ selectedRegionTransform={selectedRegionTransform}
>
{React.cloneElement(cell, { style } as ICellProps)}
diff --git a/packages/table/test/selectionTests.tsx b/packages/table/test/selectionTests.tsx
index 8becb519af..fe26edf604 100644
--- a/packages/table/test/selectionTests.tsx
+++ b/packages/table/test/selectionTests.tsx
@@ -15,6 +15,7 @@ describe("Selection", () => {
let harness = new ReactHarness();
const TH_SELECTOR = ".bp-table-column-headers .bp-table-header";
const ROW_TH_SELECTOR = ".bp-table-row-headers .bp-table-header";
+ const CELL_SELECTOR = ".bp-table-cell-row-2.bp-table-cell-col-0";
afterEach(() => {
harness.unmount();
@@ -96,6 +97,26 @@ describe("Selection", () => {
expect(onSelection.lastCall.args).to.deep.equal([[]], "meta key clear");
});
+ it("Transforms regions on selections", () => {
+ const selectedRegionTransform = () => {
+ return Regions.row(1);
+ };
+ const onSelection = sinon.spy();
+ const table = harness.mount(createTableOfSize(3, 7, {}, {onSelection, selectedRegionTransform}));
+
+ // clicking adds transformed selection
+ table.find(CELL_SELECTOR).mouse("mousedown").mouse("mouseup");
+
+ expect(onSelection.called).to.equal(true);
+ expect(onSelection.lastCall.args).to.deep.equal([[Regions.row(1)]]);
+ });
+
+ it("Accepts controlled selection", () => {
+ const table = harness.mount(createTableOfSize(3, 7, {}, { selectedRegions: [ Regions.row(0) ]}));
+ const selectionRegion = table.find(".bp-table-selection-region");
+ expect(selectionRegion.element).to.exist;
+ });
+
// TODO fix these tests on CircleCI.
//
// These tests pass locally. They are disabled because in the linux