Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Table] Add column reorder handle when interaction bar enabled #1250

Merged
merged 29 commits into from
Jun 22, 2017
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
43049f3
Add Utils.some
cmslewis Jun 16, 2017
2706ee7
[Temporary] Change feature flags in Table example
cmslewis Jun 16, 2017
aafe2c1
Add new classes for reorder handle
cmslewis Jun 16, 2017
1e3017c
Add ignoredSelector prop to DragSelectable
cmslewis Jun 16, 2017
db7e2e9
Add preliminary styles for reorder handle
cmslewis Jun 16, 2017
3f9880e
Add reorder handle to column interaction bar
cmslewis Jun 16, 2017
1e498e8
Ignore reorder handle selector
cmslewis Jun 16, 2017
6056224
Reorder handle works
cmslewis Jun 16, 2017
8497731
Renable drag-reordering on rows
cmslewis Jun 16, 2017
f5f5334
Delete commented code
cmslewis Jun 16, 2017
e81c766
Show column grab cursor only over reorder handle
cmslewis Jun 16, 2017
1355ab1
Move selection only if it existed before reorder
cmslewis Jun 16, 2017
e257c2e
Don't disable drage selection for column headers
cmslewis Jun 16, 2017
1c80928
Different handle colors on :hover and :active
cmslewis Jun 16, 2017
82fea25
Delete console.logs
cmslewis Jun 19, 2017
4fa5c91
Select clicked handle's column if not selected
cmslewis Jun 19, 2017
9b8a3e7
Reenable old reordering interaction if useInteractionBar={false}
cmslewis Jun 19, 2017
069245c
Delete reorderHandle.tsx, put Orientation back in resizeHandle.tsx
cmslewis Jun 19, 2017
78e4e77
Code cleanups
cmslewis Jun 19, 2017
82d8cd6
Rename to isEntireCellTargetReorderable
cmslewis Jun 19, 2017
f9d45e4
Delete orientation.ts
cmslewis Jun 19, 2017
0d043e5
Have Header apply the TABLE_HEADER_REORDERABLE class
cmslewis Jun 19, 2017
5fff7e1
Reply to CR feedback
cmslewis Jun 19, 2017
fe966e9
Fix tests
cmslewis Jun 19, 2017
2d3de11
Write new tests, fix bug
cmslewis Jun 20, 2017
84c3b2b
Add deprecation warning to HeaderCell#isReorderable
cmslewis Jun 20, 2017
58fdf67
Fix lint
cmslewis Jun 20, 2017
07a062f
Fix deprecation version
cmslewis Jun 20, 2017
3e8988a
Fix deprecation message
cmslewis Jun 20, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/core/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,10 @@ export function throttleEvent(target: EventTarget, eventName: string, newEventNa
target.addEventListener(eventName, func);
return func;
};

/**
* Returns true if at least one item in the array satifies the predicate.
*/
export function some(a: any[] = [], predicate: (item: any) => boolean) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

-.-

return a.filter(predicate).length > 0;
}
7 changes: 7 additions & 0 deletions packages/core/test/common/utilsTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,11 @@ describe("Utils", () => {
assert.strictEqual(Utils.clamp(40, 0, 20), 20, "value above max");
assert.throws(() => Utils.clamp(0, 20, 10), /less than/);
});

it("some", () => {
assert.strictEqual(Utils.some([1, 2, 3], (item) => item > 2), true, "number values");
assert.strictEqual(Utils.some(["1", "2", "3"], (item) => item === "3"), true, "string values");
assert.strictEqual(Utils.some([1, 2, 3], (item) => item > 3), false, "no elements satisfy predicate");
assert.strictEqual(Utils.some([], (item) => item > 3), false, "empty array");
});
});
2 changes: 2 additions & 0 deletions packages/table/src/common/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export const TABLE_OVERLAY_LAYER = "bp-table-overlay-layer";
export const TABLE_POPOVER_WHITESPACE_NORMAL = "bp-table-popover-whitespace-normal";
export const TABLE_POPOVER_WHITESPACE_PRE = "bp-table-popover-whitespace-pre";
export const TABLE_REGION = "bp-table-region";
export const TABLE_REORDER_HANDLE = "bp-table-reorder-handle";
export const TABLE_REORDER_HANDLE_TARGET = "bp-table-reorder-handle-target";
export const TABLE_REORDERING = "bp-table-reordering";
export const TABLE_RESIZE_GUIDES = "bp-table-resize-guides";
export const TABLE_RESIZE_HANDLE = "bp-table-resize-handle";
Expand Down
5 changes: 4 additions & 1 deletion packages/table/src/headers/columnHeaderCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,10 @@ export class ColumnHeaderCell extends AbstractComponent<IColumnHeaderCellProps,
if (useInteractionBar) {
return (
<div className={Classes.TABLE_COLUMN_NAME} title={name}>
<div className={Classes.TABLE_INTERACTION_BAR}>{dropdownMenu}</div>
<div className={Classes.TABLE_INTERACTION_BAR}>
{this.props.reorderHandle}
{dropdownMenu}
</div>
<HorizontalCellDivider />
<div className={Classes.TABLE_COLUMN_NAME_TEXT}>{nameComponent}</div>
</div>
Expand Down
111 changes: 74 additions & 37 deletions packages/table/src/headers/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* and https://github.com/palantir/blueprint/blob/master/PATENTS
*/

import { Classes as CoreClasses } from "@blueprintjs/core";
import * as classNames from "classnames";
import * as React from "react";

Expand Down Expand Up @@ -312,64 +313,98 @@ export class Header extends React.Component<IInternalHeaderProps, IHeaderState>
const { getIndexClass, onSelection, selectedRegions } = this.props;

const cell = this.props.renderHeaderCell(index);
const className = classNames(extremaClasses, {
[Classes.TABLE_DRAGGABLE]: onSelection != null,
}, this.props.getCellIndexClass(index), cell.props.className);

const isLoading = cell.props.loading != null ? cell.props.loading : this.props.loading;
const isSelected = this.props.isCellSelected(index);
const isCurrentlyReorderable = this.isCellCurrentlyReorderable(isSelected);
const isEntireCellTargetReorderable = this.isEntireCellTargetReorderable(cell, isSelected);

const className = classNames(extremaClasses, {
[Classes.TABLE_DRAGGABLE]: onSelection != null,
[Classes.TABLE_HEADER_REORDERABLE]: isEntireCellTargetReorderable,
}, this.props.getCellIndexClass(index), cell.props.className);

const cellProps: IHeaderCellProps = {
className,
index,
[this.props.headerCellIsSelectedPropName]: isSelected,
[this.props.headerCellIsReorderablePropName]: isCurrentlyReorderable,
[this.props.headerCellIsReorderablePropName]: isEntireCellTargetReorderable,
loading: isLoading,
reorderHandle: this.maybeRenderReorderHandle(cell, index),
};

const modifiedHandleSizeChanged = (size: number) => this.props.handleSizeChanged(index, size);
const modifiedHandleResizeEnd = (size: number) => this.props.handleResizeEnd(index, size);
const modifiedHandleResizeHandleDoubleClick = () => this.props.handleResizeDoubleClick(index);

const baseChildren = (
<DragSelectable
allowMultipleSelection={this.props.allowMultipleSelection}
disabled={isEntireCellTargetReorderable}
ignoredSelectors={[`.${Classes.TABLE_REORDER_HANDLE}`]}
key={getIndexClass(index)}
locateClick={this.locateClick}
locateDrag={this.locateDragForSelection}
onFocus={this.props.onFocus}
onSelection={this.handleDragSelectableSelection}
onSelectionEnd={this.handleDragSelectableSelectionEnd}
selectedRegions={selectedRegions}
selectedRegionTransform={this.props.selectedRegionTransform}
>
<Resizable
isResizable={this.props.isResizable}
maxSize={this.props.maxSize}
minSize={this.props.minSize}
onDoubleClick={modifiedHandleResizeHandleDoubleClick}
onLayoutLock={this.props.onLayoutLock}
onResizeEnd={modifiedHandleResizeEnd}
onSizeChanged={modifiedHandleSizeChanged}
orientation={this.props.resizeOrientation}
size={this.props.getCellSize(index)}
>
{React.cloneElement(cell, cellProps)}
</Resizable>
</DragSelectable>
);

return this.isReorderHandleEnabled(cell)
? baseChildren // reordering will be handled by interacting with the reorder handle
: this.wrapInDragReorderable(index, baseChildren, !isEntireCellTargetReorderable);
}

private isReorderHandleEnabled(cell: JSX.Element) {
// the reorder handle can only appear in the column interaction bar
return this.isColumnHeader() && cell.props.useInteractionBar;
}

private maybeRenderReorderHandle(cell: JSX.Element, index: number) {
return !this.isReorderHandleEnabled(cell)
? undefined
: this.wrapInDragReorderable(index,
<div className={Classes.TABLE_REORDER_HANDLE_TARGET}>
<div className={Classes.TABLE_REORDER_HANDLE}>
<span className={CoreClasses.iconClass("drag-handle-vertical")} />
</div>
</div>);
}

private isColumnHeader() {
return this.props.fullRegionCardinality === RegionCardinality.FULL_COLUMNS;
}

private wrapInDragReorderable(index: number, children: JSX.Element, disabled?: boolean) {
return (
<DragReorderable
disabled={!isCurrentlyReorderable}
key={getIndexClass(index)}
disabled={disabled}
key={this.props.getIndexClass(index)}
locateClick={this.locateClick}
locateDrag={this.locateDragForReordering}
onReordered={this.props.onReordered}
onReordering={this.props.onReordering}
onSelection={onSelection}
selectedRegions={selectedRegions}
onSelection={this.props.onSelection}
selectedRegions={this.props.selectedRegions}
toRegion={this.props.toRegion}
>
<DragSelectable
allowMultipleSelection={this.props.allowMultipleSelection}
disabled={isCurrentlyReorderable}
key={getIndexClass(index)}
locateClick={this.locateClick}
locateDrag={this.locateDragForSelection}
onFocus={this.props.onFocus}
onSelection={this.handleDragSelectableSelection}
onSelectionEnd={this.handleDragSelectableSelectionEnd}
selectedRegions={selectedRegions}
selectedRegionTransform={this.props.selectedRegionTransform}
>
<Resizable
isResizable={this.props.isResizable}
maxSize={this.props.maxSize}
minSize={this.props.minSize}
onDoubleClick={modifiedHandleResizeHandleDoubleClick}
onLayoutLock={this.props.onLayoutLock}
onResizeEnd={modifiedHandleResizeEnd}
onSizeChanged={modifiedHandleSizeChanged}
orientation={this.props.resizeOrientation}
size={this.props.getCellSize(index)}
>
{React.cloneElement(cell, cellProps)}
</Resizable>
</DragSelectable>
{children}
</DragReorderable>
);
}
Expand All @@ -384,7 +419,7 @@ export class Header extends React.Component<IInternalHeaderProps, IHeaderState>
this.setState({ hasSelectionEnded: true });
}

private isCellCurrentlyReorderable = (isSelected: boolean) => {
private isEntireCellTargetReorderable = (cell: JSX.Element, isSelected: boolean) => {
const { selectedRegions } = this.props;
// although reordering may be generally enabled for this row/column (via props.isReorderable), the
// row/column shouldn't actually become reorderable from a user perspective until a few other
Expand All @@ -400,7 +435,9 @@ export class Header extends React.Component<IInternalHeaderProps, IHeaderState>
// add a final check to make sure we don't enable reordering until the selection
// interaction is complete. this prevents one click+drag interaction from triggering
// both selection and reordering behavior.
&& selectedRegions.length === 1;
&& selectedRegions.length === 1
// columns are reordered via a reorder handle, so drag-selection needn't be disabled
&& !this.isReorderHandleEnabled(cell);
}
}

Expand Down
9 changes: 6 additions & 3 deletions packages/table/src/headers/headerCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,12 @@ export interface IHeaderCellProps extends IProps {
renderMenu?: (index?: number) => JSX.Element;

/**
* A `ResizeHandle` React component that allows users to drag-resize the
* header.
* A `ReorderHandle` React component that allows users to drag-reorder the column header.
*/
reorderHandle?: JSX.Element;

/**
* A `ResizeHandle` React component that allows users to drag-resize the header.
*/
resizeHandle?: ResizeHandle;

Expand Down Expand Up @@ -114,7 +118,6 @@ export class HeaderCell extends React.Component<IInternalHeaderCellProps, IHeade
public render() {
const classes = classNames(Classes.TABLE_HEADER, {
[Classes.TABLE_HEADER_ACTIVE]: this.props.isActive || this.state.isActive,
[Classes.TABLE_HEADER_REORDERABLE]: this.props.isReorderable,
[Classes.TABLE_HEADER_SELECTED]: this.props.isSelected,
[CoreClasses.LOADING]: this.props.loading,
}, this.props.className);
Expand Down
21 changes: 21 additions & 0 deletions packages/table/src/interactions/_interactions.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,27 @@ $resize-handle-dragging-color: $pt-intent-primary !default;
}
}

.bp-table-reorder-handle-target {
@include grabbable();
position: absolute;
top: 0;
left: 0;
color: $gray3;

&:hover {
color: $pt-text-color-muted;
}

&:active {
color: $pt-text-color;
}

.bp-table-reorder-handle {
padding: 3px 6px 3px 1px;
line-height: 14px;
}
}

// The wide, transparent, clickable target that contains the
// thin resize handle bar
.bp-table-resize-handle-target {
Expand Down
33 changes: 19 additions & 14 deletions packages/table/src/interactions/reorderable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,22 +112,27 @@ export class DragReorderable extends React.Component<IDragReorderable, {}> {
const { selectedRegions } = this.props;

const selectedRegionIndex = Regions.findContainingRegion(selectedRegions, region);
if (selectedRegionIndex < 0) {
return false;
}

const selectedRegion = selectedRegions[selectedRegionIndex];
if (Regions.getRegionCardinality(selectedRegion) !== cardinality) {
// ignore FULL_TABLE selections
return false;
if (selectedRegionIndex >= 0) {
const selectedRegion = selectedRegions[selectedRegionIndex];
if (Regions.getRegionCardinality(selectedRegion) !== cardinality) {
// ignore FULL_TABLE selections
return false;
}

// cache for easy access later in the lifecycle
const selectedInterval = isRowHeader ? selectedRegion.rows : selectedRegion.cols;
this.selectedRegionStartIndex = selectedInterval[0];
// add 1 to correct for the fencepost
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick -- can you be a little bit more descriptive here? A "because selected interval is inclusive" or "includes its endpoints" or something would be ok; just looking for a why-the-fencepost.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep

this.selectedRegionLength = selectedInterval[1] - selectedInterval[0] + 1;
} else {
// select the new region to avoid complex and unintuitive UX w/r/t the existing selection
this.props.onSelection([region]);

const regionRange = isRowHeader ? region.rows : region.cols;
this.selectedRegionStartIndex = regionRange[0];
this.selectedRegionLength = regionRange[1] - regionRange[0] + 1;
}

const selectedInterval = isRowHeader ? selectedRegion.rows : selectedRegion.cols;

// cache for easy access later in the lifecycle
this.selectedRegionStartIndex = selectedInterval[0];
this.selectedRegionLength = selectedInterval[1] - selectedInterval[0] + 1; // add 1 to correct for the fencepost

return true;
}

Expand Down
19 changes: 14 additions & 5 deletions packages/table/src/interactions/selectable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* and https://github.com/palantir/blueprint/blob/master/PATENTS
*/

import { Utils as BlueprintUtils } from "@blueprintjs/core";
import { Utils as CoreUtils } from "@blueprintjs/core";
import * as PureRender from "pure-render-decorator";
import * as React from "react";
import { IFocusedCellCoordinates } from "../common/cell";
Expand Down Expand Up @@ -62,6 +62,11 @@ export interface ISelectableProps {
}

export interface IDragSelectableProps extends ISelectableProps {
/**
* A list of CSS selectors that should _not_ trigger selection when a `mousedown` occurs inside of them.
*/
ignoredSelectors?: string[];

/**
* Whether the selection behavior is disabled.
* @default false
Expand Down Expand Up @@ -176,7 +181,7 @@ export class DragSelectable extends React.Component<IDragSelectableProps, {}> {
return;
}
this.maybeInvokeSelectionCallback(nextSelectedRegions);
BlueprintUtils.safeInvoke(this.props.onSelectionEnd, nextSelectedRegions);
CoreUtils.safeInvoke(this.props.onSelectionEnd, nextSelectedRegions);
this.finishInteraction();
}

Expand All @@ -190,7 +195,7 @@ export class DragSelectable extends React.Component<IDragSelectableProps, {}> {

if (!Regions.isValid(region)) {
this.maybeInvokeSelectionCallback([]);
BlueprintUtils.safeInvoke(this.props.onSelectionEnd, []);
CoreUtils.safeInvoke(this.props.onSelectionEnd, []);
return false;
}

Expand All @@ -210,7 +215,7 @@ export class DragSelectable extends React.Component<IDragSelectableProps, {}> {
}

this.maybeInvokeSelectionCallback(nextSelectedRegions);
BlueprintUtils.safeInvoke(this.props.onSelectionEnd, nextSelectedRegions);
CoreUtils.safeInvoke(this.props.onSelectionEnd, nextSelectedRegions);
this.finishInteraction();
return false;
}
Expand All @@ -220,7 +225,11 @@ export class DragSelectable extends React.Component<IDragSelectableProps, {}> {
}

private shouldIgnoreMouseDown(event: MouseEvent) {
return !Utils.isLeftClick(event) || this.props.disabled;
const { ignoredSelectors = [] } = this.props;
const element = event.target as HTMLElement;
return !Utils.isLeftClick(event)
|| this.props.disabled
|| CoreUtils.some(ignoredSelectors, (selector: string) => element.closest(selector) != null);
}

private getDragSelectedRegions(event: MouseEvent, coords: ICoordinateData) {
Expand Down