Skip to content

Commit

Permalink
[Table] scrollToRegion instance method (#1496)
Browse files Browse the repository at this point in the history
* Delete commented code

* Add method stub

* Initial implementation

* [TEMPORARY?] Add instance to the window object

* Make auto-scrolling work; fix syncViewportPosition bug

* Handling for frozen rows/columns

* Fix scroll-correction logic

* Add instance method to TableQuadrantStack

* Refactor logic into new scrollUtils.ts

* Create new common/internal folder

* Fix scroll misalignment bug

* Don't correct if scroll is disabled

* Prevent programmatic scrolling when scrolling disabled

* Remove need for Grid in scrollUtils file

* Fix off-by-one bug

* Add scrollTo controls to the table example

* Fix lint

* Delete animated param for now

* Delete window instance

* Delete console.logs

* Revert changes RE: prereqStateKeyValue

* Delete unintentional cnewline

* Write tests for scrollUtils

* Fix lint again

* Write tests in table too

* Added docs, new 'Instance methods' section

* Update stuff per CR feedback

* Oops, revert changes to table example

* Fix stuff per bdwyer CR
  • Loading branch information
cmslewis authored Aug 28, 2017
1 parent cf4b2a6 commit 95d431d
Show file tree
Hide file tree
Showing 9 changed files with 705 additions and 69 deletions.
116 changes: 116 additions & 0 deletions packages/table/preview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import * as React from "react";
import * as ReactDOM from "react-dom";

import {
Button,
Classes,
FocusStyleManager,
Intent,
Menu,
MenuDivider,
MenuItem,
Expand Down Expand Up @@ -75,6 +77,9 @@ interface IMutableTableState {
numFrozenCols?: number;
numFrozenRows?: number;
numRows?: number;
scrollToColumnIndex?: number;
scrollToRegionType?: RegionCardinality;
scrollToRowIndex?: number;
selectedFocusStyle?: FocusStyle;
showCallbackLogs?: boolean;
showCellsLoading?: boolean;
Expand Down Expand Up @@ -112,6 +117,13 @@ const ROW_COUNTS = [
const FROZEN_COLUMN_COUNTS = [0, 1, 2, 5, 20, 100, 1000];
const FROZEN_ROW_COUNTS = [0, 1, 2, 5, 20, 100, 1000];

const REGION_CARDINALITIES = [
RegionCardinality.CELLS,
RegionCardinality.FULL_ROWS,
RegionCardinality.FULL_COLUMNS,
RegionCardinality.FULL_TABLE,
];

enum CellContent {
EMPTY,
CELL_NAMES,
Expand Down Expand Up @@ -155,6 +167,12 @@ const CELL_CONTENT_GENERATORS = {
class MutableTable extends React.Component<{}, IMutableTableState> {
private store = new DenseGridMutableStore<string>();

private tableInstance: Table;

private refHandlers = {
table: (ref: Table) => this.tableInstance = ref,
};

public constructor(props: any, context?: any) {
super(props, context);

Expand All @@ -179,6 +197,9 @@ class MutableTable extends React.Component<{}, IMutableTableState> {
numFrozenCols: FROZEN_COLUMN_COUNTS[FROZEN_COLUMN_COUNT_DEFAULT_INDEX],
numFrozenRows: FROZEN_ROW_COUNTS[FROZEN_ROW_COUNT_DEFAULT_INDEX],
numRows: ROW_COUNTS[ROW_COUNT_DEFAULT_INDEX],
scrollToColumnIndex: 0,
scrollToRegionType: RegionCardinality.CELLS,
scrollToRowIndex: 0,
selectedFocusStyle: FocusStyle.TAB,
showCallbackLogs: false,
showCellsLoading: false,
Expand Down Expand Up @@ -226,6 +247,7 @@ class MutableTable extends React.Component<{}, IMutableTableState> {
onVisibleCellsChange={this.onVisibleCellsChange}
onRowHeightChanged={this.onRowHeightChanged}
onRowsReordered={this.onRowsReordered}
ref={this.refHandlers.table}
renderBodyContextMenu={this.renderBodyContextMenu}
renderMode={renderMode}
renderRowHeader={this.renderRowHeader}
Expand Down Expand Up @@ -440,6 +462,8 @@ class MutableTable extends React.Component<{}, IMutableTableState> {
{this.renderSwitch("Callback logs", "showCallbackLogs")}
{this.renderSwitch("Full-table selection", "enableFullTableSelection")}
{this.renderSwitch("Multi-selection", "enableMultiSelection")}
<h6>Scroll to</h6>
{this.renderScrollToSection()}

<h4>Columns</h4>
<h6>Display</h6>
Expand Down Expand Up @@ -487,6 +511,66 @@ class MutableTable extends React.Component<{}, IMutableTableState> {
);
}

private renderScrollToSection() {
const { scrollToRegionType } = this.state;

const scrollToRegionTypeSelectMenu = this.renderSelectMenu(
"Region type",
"scrollToRegionType",
REGION_CARDINALITIES,
this.getRegionCardinalityLabel,
this.handleRegionCardinalityChange,
);
const scrollToRowSelectMenu = this.renderSelectMenu(
"Row",
"scrollToRowIndex",
Utils.times(this.state.numRows, (rowIndex) => rowIndex),
(rowIndex) => `${rowIndex + 1}`,
this.handleNumberStateChange,
);
const scrollToColumnSelectMenu = this.renderSelectMenu(
"Column",
"scrollToColumnIndex",
Utils.times(this.state.numCols, (columnIndex) => columnIndex),
(columnIndex) => this.store.getColumnName(columnIndex),
this.handleNumberStateChange,
);

const ROW_MENU_CARDINALITIES = [RegionCardinality.CELLS, RegionCardinality.FULL_ROWS];
const COLUMN_MENU_CARDINALITIES = [RegionCardinality.CELLS, RegionCardinality.FULL_COLUMNS];

const shouldShowRowSelectMenu = contains(ROW_MENU_CARDINALITIES, scrollToRegionType);
const shouldShowColumnSelectMenu = contains(COLUMN_MENU_CARDINALITIES, scrollToRegionType);

return (
<div>
{scrollToRegionTypeSelectMenu}
<div className="sidebar-indented-group">
{shouldShowRowSelectMenu ? scrollToRowSelectMenu : undefined}
{shouldShowColumnSelectMenu ? scrollToColumnSelectMenu : undefined}
</div>
<Button intent={Intent.PRIMARY} className={Classes.FILL} onClick={this.handleScrollToButtonClick}>
Scroll
</Button>
</div>
);
}

private getRegionCardinalityLabel(cardinality: RegionCardinality) {
switch (cardinality) {
case RegionCardinality.CELLS:
return "Cell";
case RegionCardinality.FULL_ROWS:
return "Row";
case RegionCardinality.FULL_COLUMNS:
return "Column";
case RegionCardinality.FULL_TABLE:
return "Full table";
default:
return "";
}
}

private renderSwitch(
label: string,
stateKey: keyof IMutableTableState,
Expand Down Expand Up @@ -693,6 +777,30 @@ class MutableTable extends React.Component<{}, IMutableTableState> {
this.store.setColumnName(columnIndex, value);
}

private handleScrollToButtonClick = () => {
const { scrollToRowIndex, scrollToColumnIndex, scrollToRegionType } = this.state;

let region: IRegion;
switch (scrollToRegionType) {
case RegionCardinality.CELLS:
region = Regions.cell(scrollToRowIndex, scrollToColumnIndex);
break;
case RegionCardinality.FULL_ROWS:
region = Regions.row(scrollToRowIndex);
break;
case RegionCardinality.FULL_COLUMNS:
region = Regions.column(scrollToColumnIndex);
break;
case RegionCardinality.FULL_TABLE:
region = Regions.table();
break;
default:
return;
}

this.tableInstance.scrollToRegion(region);
}

// State updates
// =============

Expand Down Expand Up @@ -734,6 +842,10 @@ class MutableTable extends React.Component<{}, IMutableTableState> {
return handleNumberChange((value) => this.setState({ [stateKey]: value }));
}

private handleRegionCardinalityChange = (stateKey: keyof IMutableTableState) => {
return handleNumberChange((value) => this.setState({ [stateKey]: value }));
}

private updateFocusStyleState = () => {
return handleStringChange((value: string) => {
const selectedFocusStyle = value === "tab" ? FocusStyle.TAB : FocusStyle.TAB_OR_CLICK;
Expand Down Expand Up @@ -831,3 +943,7 @@ function getRandomInteger(min: number, max: number) {
// min and max are inclusive
return Math.floor(min + (Math.random() * (max - min + 1)));
}

function contains(arr: any[], value: any) {
return arr.indexOf(value) >= 0;
}
5 changes: 4 additions & 1 deletion packages/table/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ export { Grid } from "./grid";
export { Rect, AnyRect } from "./rect";
export { RoundSize } from "./roundSize";
export { Utils } from "./utils";
// NOTE: Errors is not exported in public API

// NOTE: The following are not exported in the public API:
// - Errors
// - internal/
67 changes: 67 additions & 0 deletions packages/table/src/common/internal/scrollUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Copyright 2017 Palantir Technologies, Inc. All rights reserved.
* Licensed under the BSD-3 License as modified (the “License”); you may obtain a copy
* of the license at https://github.com/palantir/blueprint/blob/master/LICENSE
* and https://github.com/palantir/blueprint/blob/master/PATENTS
*/

import { IRegion, RegionCardinality, Regions } from "../../regions";

export function getScrollPositionForRegion(
region: IRegion,
currScrollLeft: number,
currScrollTop: number,
getLeftOffset: (columnIndex: number) => number,
getTopOffset: (rowIndex: number) => number,
numFrozenRows: number = 0,
numFrozenColumns: number = 0,
) {
const cardinality = Regions.getRegionCardinality(region);

let scrollTop = currScrollTop;
let scrollLeft = currScrollLeft;

// if these were max-frozen-index values, we would have added 1 before passing to the get*Offset
// functions, but the counts are already 1-indexed, so we can just pass those.
const frozenColumnsCumulativeWidth = getLeftOffset(numFrozenColumns);
const frozenRowsCumulativeHeight = getTopOffset(numFrozenRows);

switch (cardinality) {
case RegionCardinality.CELLS: {
// scroll to the top-left corner of the block of cells
const topOffset = getTopOffset(region.rows[0]);
const leftOffset = getLeftOffset(region.cols[0]);
scrollTop = getClampedScrollPosition(topOffset, frozenRowsCumulativeHeight);
scrollLeft = getClampedScrollPosition(leftOffset, frozenColumnsCumulativeWidth);
break;
}
case RegionCardinality.FULL_ROWS: {
// scroll to the top of the row block
const topOffset = getTopOffset(region.rows[0]);
scrollTop = getClampedScrollPosition(topOffset, frozenRowsCumulativeHeight);
break;
}
case RegionCardinality.FULL_COLUMNS: {
// scroll to the left side of the column block
const leftOffset = getLeftOffset(region.cols[0]);
scrollLeft = getClampedScrollPosition(leftOffset, frozenColumnsCumulativeWidth);
break;
}
default: {
// if it's a FULL_TABLE region, scroll back to the top-left cell of the table
scrollTop = 0;
scrollLeft = 0;
break;
}
}

return { scrollLeft, scrollTop };
}

/**
* Adjust the scroll position to align content just beyond the frozen region, if necessary.
*/
function getClampedScrollPosition(scrollOffset: number, frozenRegionCumulativeSize: number) {
// if the new scroll offset falls within the frozen region, clamp it to 0
return Math.max(scrollOffset - frozenRegionCumulativeSize, 0);
}
19 changes: 19 additions & 0 deletions packages/table/src/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,25 @@ components are available in the __@blueprintjs/table__ package.
The top-level component of the table is `Table`. You must at least define the
number of rows (`numRows` prop) as well as a set of `Column` children.

@#### Instance methods

- `resizeRowsByTallestCell(columnIndices?: number | number[]): void` &ndash; Resizes all rows in the
table to the height of the tallest visible cell in the specified columns. If no indices are
provided, defaults to using the tallest visible cell from all columns in view.
- `scrollToRegion(region: IRegion): void` &ndash; Scrolls the table to the target region in a
fashion appropriate to the target region's cardinality:
- `CELLS`: Scroll the top-left cell in the target region to the top-left corner of the viewport.
- `FULL_ROWS`: Scroll the top-most row in the target region to the top of the viewport.
- `FULL_COLUMNS`: Scroll the left-most column in the target region to the left side of the viewport.
- `FULL_TABLE`: Scroll the top-left cell in the table to the top-left corner of the viewport.

If there are active frozen rows and/or columns, the target region will be positioned in the top-left
corner of the non-frozen area (unless the target region itself is in the frozen area).

If the target region is close to the bottom-right corner of the table, this function will simply
scroll the target region as close to the top-left as possible until the bottom-right corner is
reached.

@interface ITableProps

@### Column
Expand Down
11 changes: 11 additions & 0 deletions packages/table/src/quadrants/tableQuadrantStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,17 @@ export class TableQuadrantStack extends AbstractComponent<ITableQuadrantStackPro
this.throttledHandleWheel = CoreUtils.throttleReactEventCallback(this.handleWheel, { preventDefault: true });
}

/**
* Scroll the main quadrant to the specified scroll offset, keeping all other quadrants in sync.
*/
public scrollToPosition(scrollLeft: number, scrollTop: number) {
const { scrollContainer } = this.quadrantRefs[QuadrantType.MAIN];
this.wasMainQuadrantScrollChangedFromOtherOnWheelCallback = false;
// this will trigger the main quadrant's scroll callback below
scrollContainer.scrollLeft = scrollLeft;
scrollContainer.scrollTop = scrollTop;
}

public componentDidMount() {
this.emitRefs();
this.syncQuadrantSizes();
Expand Down
Loading

1 comment on commit 95d431d

@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] scrollToRegion instance method (#1496)

Preview: documentation
Coverage: core | datetime

Please sign in to comment.