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