Skip to content

Commit

Permalink
Controllable selection
Browse files Browse the repository at this point in the history
Adds "selectedRegions" prop to Table to allow to user to take over
control of selection. If defined, the user will be responsible for
updating the selection and should probable add a "onSelection"
callback.

Adds "selectedRegionTransform" prop to table to allow users to
transform the selection region. This allows users to, for example,
select a whole row when the user clicks on a single cell. Using
a transform is advantageous because we can still handle interaction
semantics like multiselect, meta-click, and click-to-deselect.
  • Loading branch information
themadcreator committed Nov 28, 2016
1 parent 9ae2871 commit d0b3faa
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 13 deletions.
4 changes: 4 additions & 0 deletions packages/table/preview/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ <h4>All Selection Modes, Body Context Menu</h4>
<div id="table-2"></div>
<br /><br />

<h4>Row Transformed Controlled Selection</h4>
<div id="table-select-rows"></div>
<br /><br />

<h4>Ghost Inline</h4>
<div id="table-inline-ghost" style="display: inline-block;"></div>
<br /><br />
Expand Down
56 changes: 55 additions & 1 deletion packages/table/preview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -20,6 +27,7 @@ import {
EditableName,
IColumnHeaderCellProps,
IColumnProps,
ICoordinateData,
HorizontalCellDivider,
IMenuContext,
IRegion,
Expand Down Expand Up @@ -257,6 +265,52 @@ ReactDOM.render(
document.getElementById("table-big")
);

class RowSelectableTable extends React.Component<{}, {}> {
public state = {
selectedRegions: [ Regions.row(2) ],
};

public render() {
return (<div>
<Table
numRows={7}
isRowHeaderShown={false}
onSelection={this.handleSelection}
selectedRegions={this.state.selectedRegions}
selectedRegionTransform={this.selectedRegionTransform}
>
<Column name="Cells" />
<Column name="Select" />
<Column name="Rows" />
</Table>
<br/>
<Button onClick={this.handleClear} intent={Intent.PRIMARY}>Clear Selection</Button>
</div>);
}

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(
<RowSelectableTable/>,
document.getElementById("table-select-rows")
);

document.getElementById("table-ledger").classList.add("bp-table-striped");

ReactDOM.render(
Expand Down
2 changes: 2 additions & 0 deletions packages/table/src/headers/columnHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export class ColumnHeader extends React.Component<IColumnHeaderProps, {}> {
onResizeGuide,
onSelection,
selectedRegions,
selectedRegionTransform,
} = this.props;

const rect = grid.getColumnRect(columnIndex);
Expand Down Expand Up @@ -162,6 +163,7 @@ export class ColumnHeader extends React.Component<IColumnHeaderProps, {}> {
locateDrag={this.locateDrag}
onSelection={onSelection}
selectedRegions={selectedRegions}
selectedRegionTransform={selectedRegionTransform}
>
<Resizable
isResizable={isResizable}
Expand Down
2 changes: 2 additions & 0 deletions packages/table/src/headers/rowHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export class RowHeader extends React.Component<IRowHeaderProps, {}> {
onSelection,
renderRowHeader,
selectedRegions,
selectedRegionTransform,
} = this.props;

const rect = grid.getRowRect(rowIndex);
Expand Down Expand Up @@ -155,6 +156,7 @@ export class RowHeader extends React.Component<IRowHeaderProps, {}> {
locateDrag={this.locateDrag}
onSelection={onSelection}
selectedRegions={selectedRegions}
selectedRegionTransform={selectedRegionTransform}
>
<Resizable
isResizable={isResizable}
Expand Down
5 changes: 5 additions & 0 deletions packages/table/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export {
IMenuContext
} from "./interactions/menus";

export {
IClientCoordinates,
ICoordinateData,
} from "./interactions/draggable";

export {
ILockableLayout,
IResizeHandleProps,
Expand Down
38 changes: 31 additions & 7 deletions packages/table/src/interactions/selectable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { DragEvents } from "../interactions/dragEvents";
import { Draggable, ICoordinateData, IDraggableProps } from "../interactions/draggable";
import { IRegion, Regions } from "../regions";

export type ISelectedRegionTransform = (region: IRegion, event: MouseEvent, coords?: ICoordinateData) => IRegion;

export interface ISelectableProps {
/**
* If `false`, only a single region of a single column/row/cell may be
Expand All @@ -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 {
Expand Down Expand Up @@ -76,12 +88,16 @@ export class DragSelectable extends React.Component<IDragSelectableProps, {}> {
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
Expand All @@ -107,14 +123,18 @@ export class DragSelectable extends React.Component<IDragSelectableProps, {}> {
}

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);

if (!Regions.isValid(region)) {
return;
}

if (this.props.selectedRegionTransform != null) {
region = this.props.selectedRegionTransform(region, event, coords);
}

this.props.onSelection(Regions.update(this.props.selectedRegions, region));
}

Expand All @@ -123,13 +143,17 @@ export class DragSelectable extends React.Component<IDragSelectableProps, {}> {
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 {
Expand Down
58 changes: 54 additions & 4 deletions packages/table/src/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -254,16 +278,26 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {
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<React.ReactElement<IColumnProps>>;

// Try to maintain widths of columns by looking up the width of the
Expand All @@ -286,12 +320,15 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {
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,
});
}

Expand Down Expand Up @@ -414,6 +451,7 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {
isColumnResizable,
maxColumnWidth,
minColumnWidth,
selectedRegionTransform,
} = this.props;
const classes = classNames("bp-table-column-headers", {
"bp-table-selection-enabled": this.isSelectionModeEnabled(RegionCardinality.FULL_COLUMNS),
Expand All @@ -435,6 +473,7 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {
onResizeGuide={this.handleColumnResizeGuide}
onSelection={this.getEnabledSelectionHandler(RegionCardinality.FULL_COLUMNS)}
selectedRegions={selectedRegions}
selectedRegionTransform={selectedRegionTransform}
viewportRect={viewportRect}
{...columnIndices}
>
Expand All @@ -456,6 +495,7 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {
maxRowHeight,
minRowHeight,
renderRowHeader,
selectedRegionTransform,
} = this.props;
const classes = classNames("bp-table-row-headers", {
"bp-table-selection-enabled": this.isSelectionModeEnabled(RegionCardinality.FULL_ROWS),
Expand All @@ -479,6 +519,7 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {
onSelection={this.getEnabledSelectionHandler(RegionCardinality.FULL_ROWS)}
renderRowHeader={renderRowHeader}
selectedRegions={selectedRegions}
selectedRegionTransform={selectedRegionTransform}
viewportRect={viewportRect}
{...rowIndices}
/>
Expand All @@ -494,7 +535,12 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {

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();
Expand Down Expand Up @@ -528,6 +574,7 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {
onSelection={this.getEnabledSelectionHandler(RegionCardinality.CELLS)}
renderBodyContextMenu={renderBodyContextMenu}
selectedRegions={selectedRegions}
selectedRegionTransform={selectedRegionTransform}
viewportRect={viewportRect}
{...rowIndices}
{...columnIndices}
Expand Down Expand Up @@ -741,7 +788,10 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {
}

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) {
Expand Down
Loading

1 comment on commit d0b3faa

@blueprint-bot
Copy link

Choose a reason for hiding this comment

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

Controllable selection

Preview: docs | table Coverage: core | datetime

Please sign in to comment.