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

Merged
merged 30 commits into from
Aug 28, 2017
Merged

Conversation

cmslewis
Copy link
Contributor

@cmslewis cmslewis commented Aug 24, 2017

Fixes #1065, Fixes #1430

Checklist

  • Include tests
  • Update documentation

Changes proposed in this pull request:

  • 💎 NEW Table now offers a scrollToRegion(region: IRegion) instance method to programmatically scroll a desired row, column, or cell into view!
    • The target region will be positioned in the top/left corner of the viewport.
      • Or if the table is maximally scrolled, then having the target cell simply in view is enough.
    • If there are active frozen rows and/or columns:
      • Scrolling to a frozen row will move the table back to scrollTop=0.
      • Scrolling to a frozen column will move the table back to scrollLeft=0.
      • Scrolling to a frozen TOP_LEFT cell will move the table back to (0, 0).
      • Scrolling to a non-frozen row/column will scroll the target region to just beyond the frozen regions, keeping it clearly in view.
  • 🖋 Added documentation for both Table instance methods:
    • resizeRowsByTallestCell
    • scrollToRegion
  • Also added some new scrollTo controls on the Table example page.

Followup work (for later)

Reviewers should focus on:

  • How does this API look? Tried to be as simple and general as possible. Ended up not including an animated option for now, because that'd actually be a lot more work.

Screenshot

2017-08-24 12 50 14

image

Copy link
Contributor Author

@cmslewis cmslewis left a comment

Choose a reason for hiding this comment

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

Clarifying comments + self-review.

);

// Table: "Scroll to"
const scrollToRegionTypeSelectMenu = this.renderSelectMenu(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This whole area is a mess now. Wanted to get a preview up quickly, but would like to clean this up before merging.

private renderSwitch(
label: string,
stateKey: keyof IMutableTableState,
prereqStateKey?: keyof IMutableTableState,
prereqStateKeyValue?: any,
prereqStateKeyValues?: any[],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can undo all these changes; ended up going in a different direction.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


// NOTE: The following are not exported in the public API:
// - Errors
// - internal/
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thoughts on this common/internal/ folder to clearly denote things that won't be exported?

Copy link
Contributor

Choose a reason for hiding this comment

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

👍 yes nice

@@ -0,0 +1,68 @@
/**
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Really want to get into the practice of taking utility code out of table.tsx. I'd also like to avoid putting all utility code into one huge Utils file if possible. Thus, here's a new scrollUtils file.

Copy link
Contributor

Choose a reason for hiding this comment

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

that's cool, if you add more do you think it's useful to keep track of an index.ts that re-exports? it's a tradeoff between maintenance of index.ts and simplifying import lines.

Copy link
Contributor Author

@cmslewis cmslewis Aug 26, 2017

Choose a reason for hiding this comment

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

I'd be into that. 👍 Another immediate idea is focusUtils.ts. Happy to introduce a /common/internal/index.ts file when I create that.

getTopOffset: (rowIndex: number) => number,
numFrozenRows: number = 0,
numFrozenColumns: number = 0,
) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A lot of params, but this way I don't have to pass a bloated viewportRect or grid object from <Table>. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

seems legit

Copy link
Contributor

Choose a reason for hiding this comment

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

yeeesh. how many args would passing those props around save? this is an internal method and those args are passed by reference, so no perf penalty.

Copy link
Contributor Author

@cmslewis cmslewis Aug 26, 2017

Choose a reason for hiding this comment

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

It would save two args (get{Top,Left}Offset => grid, currScroll{Top,Left} => viewportRect), but the current args are much easier to mock for unit tests.

Also, we don't really need to pass in the current scroll offsets. We could simply have this function return null for scrollLeft or scrollTop if that respective coordinate doesn't need to change, then the caller can substitute the current value for null in its scope. That reduces the params to:

region: IRegion;
getLeftOffset: (columnIndex: number) => number;
getTopOffset: (rowIndex: number) => number;
numFrozenRows: number = 0;
numFrozenColumns: number = 0;

Thoughts? @themadcreator @giladgray

EDIT: Actually, I guess this isn't quite true given the need for clamping of values within the frozen area. Ignore the above.

const { scrollContainer } = this.quadrantRefs[QuadrantType.MAIN];
this.wasMainQuadrantScrollChangedFromOtherOnWheelCallback = false;
// this will trigger the main quadrant's scroll callback below

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can delete this new line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -368,12 +380,17 @@ export class TableQuadrantStack extends AbstractComponent<ITableQuadrantStackPro
// use the more generic "scroll" event for the main quadrant, which captures both click+dragging
// on the scrollbar and trackpad/mousewheel gestures
private handleMainQuadrantScroll = (event: React.UIEvent<HTMLElement>) => {
console.log("[TableQuadrantStack] handleMainQuadrantScroll");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can delete these console.logs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -1030,48 +1081,31 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {
const rowIndexEnd = showFrozenRowsOnly ? numFrozenRows : rowIndices.rowIndexEnd;

return (
// <div
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Went ahead and deleted this unrelated commented code. Was a relic from the frozen rows/columns refactor.

// we need to modify the body element explicitly for the viewport to shift
// we need to modify the scroll container explicitly for the viewport to shift. in so
// doing, we add the size of the header elements, which are not technically part of the
// "grid" concept (the grid only consists of body cells at present).
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was actually a regression that happened during the frozen rows/columns refactor. But all that broke was auto-scrolling as you use arrow keys to move the focus cell around. Still, works again now. 👍


// NOTE: The following are not exported in the public API:
// - Errors
// - internal/
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 yes nice

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

// Public
// ======
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure these headings are useful... the keyword export gets the point across.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

getTopOffset: (rowIndex: number) => number,
numFrozenRows: number = 0,
numFrozenColumns: number = 0,
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

seems legit

// scroll to the left side of the column block
const leftOffset = getLeftOffset(region.cols[0]);
nextLeft = getAdjustedScrollPosition(leftOffset, frozenColumnsCumulativeWidth);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

this might make more sense as a switch? cuz that's exactly what it is, on enum values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like to avoid switches unless I can return immediately within each case; otherwise I have an extra break; line at the end of every case, which I find bloat-y and unsightly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, TIL if you redeclare variables with the same name within different switch cases (e.g. topOffset and leftOffset), you get this compilation error: [ts] Cannot redeclare block-scoped variable 'leftOffset'.

Copy link
Contributor

Choose a reason for hiding this comment

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

you can always put your switch-case in a block

case 1: {
   const foo = 1;
   break;
}
case 2: {
   const foo = 2;
   break;
}
   

Copy link
Contributor

@themadcreator themadcreator Aug 25, 2017

Choose a reason for hiding this comment

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

breaks are annoying yes, but the benefit of compile-time warnings shouldn't be discarded.

also, you can refactor the switch to a function so you can use returns

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@themadcreator nice, will change

function getAdjustedScrollPosition(scrollOffset: number, frozenRegionCumulativeSize: number) {
return scrollOffset < frozenRegionCumulativeSize
? 0
: scrollOffset - frozenRegionCumulativeSize;
Copy link
Contributor

Choose a reason for hiding this comment

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

equivalent to Math.min(scrollOffset - frozenRegionCumulativeSize, 0), i think.

perhaps, instead of this function, you can just do the subtraction inline and the Math.min once when building the return object?

Copy link
Contributor Author

@cmslewis cmslewis Aug 25, 2017

Choose a reason for hiding this comment

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

Yeah. It's a little clearer to me as is, but I can change it and just add a comment about the intent like // if the new scroll offset falls within the frozen region, clamp it to 0

EDIT: Math.max

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

if (this.wasMainQuadrantScrollChangedFromOtherOnWheelCallback) {
console.log(" this.wasMainQuadrantScrollChangedFromOtherOnWheelCallback == true.");
console.log(" Setting this.wasMainQuadrantScrollChangedFromOtherOnWheelCallback = false. RETURNING");
Copy link
Contributor

Choose a reason for hiding this comment

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

😱 🌳 logs 🌳 😱

* Scroll the left-most column in the target region to the left side of the viewport.
*
* FULL_TABLE:
* Scroll the top-left cell to the top-left corner of the viewport.
Copy link
Contributor

Choose a reason for hiding this comment

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

can cut lines in a third by formatting as list.

- `CELLS`: words words
- `FULL_ROWS`: more words

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One of them was too long to fit and line-wrapped; this format takes more lines but keeps everything looking perfectly consistent. I'll see if I can whittle the words down and go back to a list.

@blueprint-bot
Copy link

Fix lint again

Preview: documentation | table
Coverage: core | datetime

@cmslewis cmslewis force-pushed the cl/table-scroll-to-position branch from a01046f to dfb5a9c Compare August 25, 2017 19:47
@tgreenwatts
Copy link
Contributor

seeing 1 small possible
-focus on c1 and then scroll to G, I lose my focus cell. It's selected but not focused?

@cmslewis
Copy link
Contributor Author

cmslewis commented Aug 25, 2017

@tgreenwatts I can't repro that. When I focus on C1 and then scrollTo column G, I still see the focus cell in C1 when I manually scroll back. What are the exact repro steps for the weirdness you're seeing?

2017-08-25 14 09 36

@blueprint-bot
Copy link

Added docs, new 'Instance methods' section

Preview: documentation | table
Coverage: core | datetime

@blueprint-bot
Copy link

Update stuff per CR feedback

Preview: documentation | table
Coverage: core | datetime

Copy link
Contributor

@llorca llorca left a comment

Choose a reason for hiding this comment

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

👌

@blueprint-bot
Copy link

Oops, revert changes to table example

Preview: documentation | table
Coverage: core | datetime

Copy link
Contributor

@themadcreator themadcreator left a comment

Choose a reason for hiding this comment

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

baller tests. many nits, but nothing blocking.

return;
}

this.tableInstance.scrollToRegion(region);
Copy link
Contributor

Choose a reason for hiding this comment

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

is invoking methods on the table ref the standard way for actions like this? do we have other components/features that do this? is this what users will expect?

Copy link
Contributor

Choose a reason for hiding this comment

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

i guess the resizeRows does this. still, is this the idiomatic blueprint way to invoke actions on react components?

Copy link
Contributor Author

@cmslewis cmslewis Aug 26, 2017

Choose a reason for hiding this comment

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

Yeah, there are a few instance methods around the codebase, each of which operates on a ref. The goal here was to provide a way to briefly control the scroll without forcing the caller to control it forever and always (would be annoying and unnecessary). See:

// Table
// =====

resizeRowsByTallestCell(columnIndices?: number | number[]): void;

// Toast
// =====

clear(): void;
dismiss(key: string): void;
getToasts(): IToastProps[];
show(props: IToastProps): string;
update(key: string, props: IToastProps): void;

// Tree
// ====

getNodeContentElement(nodeId: string | number): HTMLElement | undefined;

@@ -724,6 +832,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 }));
Copy link
Contributor

Choose a reason for hiding this comment

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

man this pattern is so common, i wish react had a shorthand for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

e.g. 4 lines above

@@ -0,0 +1,68 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

that's cool, if you add more do you think it's useful to keep track of an index.ts that re-exports? it's a tradeoff between maintenance of index.ts and simplifying import lines.

getTopOffset: (rowIndex: number) => number,
numFrozenRows: number = 0,
numFrozenColumns: number = 0,
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

yeeesh. how many args would passing those props around save? this is an internal method and those args are passed by reference, so no perf penalty.

const frozenColumnsCumulativeWidth = getLeftOffset(numFrozenColumns);
const frozenRowsCumulativeHeight = getTopOffset(numFrozenRows);

if (cardinality === RegionCardinality.CELLS) {
Copy link
Contributor

Choose a reason for hiding this comment

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

for enums, i think we should use switch statements to make default: lintable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, good thought. Will change.

@@ -435,6 +439,74 @@ export class Table extends AbstractComponent<ITableProps, ITableState> {
};
}

// Instance methods
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: these sort of block comments indicate the file is too big and should be split. "nit" because i know that splitting is extremely difficult at this point.

Copy link
Contributor Author

@cmslewis cmslewis Aug 26, 2017

Choose a reason for hiding this comment

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

Agree. I want to change this later (not this PR).

* 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.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

i wish there were some way to annotate these methods so we could easily use the jsdoc comment in our docs. appreciate your update the docs markdown though

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Samesies.

* corner is reached.
*/
public scrollToRegion(region: IRegion) {
const { left: currScrollLeft, top: currScrollTop } = this.state.viewportRect;
Copy link
Contributor

Choose a reason for hiding this comment

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

holy crap, i didn't know you could rename with destructuring assignment!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I just learned this last month. Super nifty. 👍

this.grid.getCumulativeHeightBefore,
numFrozenRows,
numFrozenColumns,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like passing the objects would only save a couple args =/

Copy link
Contributor

Choose a reason for hiding this comment

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

still, i prefer many args over jamming them into a custom argument object

if (didScrollTopChange) {
this.bodyElement.scrollTop = nextScrollTop;
const topCorrection = this.shouldDisableVerticalScroll() ? 0 : this.columnHeaderElement.clientHeight;
Copy link
Contributor

Choose a reason for hiding this comment

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

i think you use "adjustment" and "correction" interchangeably. choose one

@cmslewis
Copy link
Contributor Author

@giladgray @themadcreator think I addressed all of your immediate comments.

@blueprint-bot
Copy link

Fix stuff per bdwyer CR

Preview: documentation | table
Coverage: core | datetime

@cmslewis cmslewis dismissed giladgray’s stale review August 28, 2017 21:10

Addressed all comments from before

@blueprint-bot
Copy link

Merge branch 'master' into cl/table-scroll-to-position

Preview: documentation | table
Coverage: core | datetime

@cmslewis cmslewis merged commit 95d431d into master Aug 28, 2017
@cmslewis cmslewis deleted the cl/table-scroll-to-position branch August 28, 2017 21:26
@cmslewis cmslewis mentioned this pull request Aug 30, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants