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

Fix: Table not keeping focused cell visible #6882

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions packages/table/changelog/@unreleased/pr-6882.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
type: fix
fix:
description: |-
`Table` and `Table2` now keep the focused cell cell visible if the focused cell is changed by:

- The `focusedCell` prop (provided that `enableFocusedCell` is `true`), OR
- The built-in navigation key handlers for arrow, tab and enter keys
links:
- https://github.com/palantir/blueprint/pull/6882
8 changes: 8 additions & 0 deletions packages/table/src/common/TableHeaderDimensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* !
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
*/

export interface TableHeaderDimensions {
readonly columnHeaderHeight: number;
readonly rowHeaderWidth: number;
}
45 changes: 26 additions & 19 deletions packages/table/src/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import * as FocusedCellUtils from "./common/internal/focusedCellUtils";
import * as ScrollUtils from "./common/internal/scrollUtils";
import { Rect } from "./common/rect";
import { RenderMode } from "./common/renderMode";
import type { TableHeaderDimensions } from "./common/TableHeaderDimensions";
import { Utils } from "./common/utils";
import { ColumnHeader } from "./headers/columnHeader";
import { ColumnHeaderCell, type ColumnHeaderCellProps } from "./headers/columnHeaderCell";
Expand Down Expand Up @@ -303,6 +304,7 @@ export class Table extends AbstractComponent<TableProps, TableState, TableSnapsh

this.hotkeysImpl = new TableHotkeys(props, this.state, {
getEnabledSelectionHandler: this.getEnabledSelectionHandler,
getHeaderDimensions: this.getHeaderDimensions,
handleFocus: this.handleFocus,
handleSelection: this.handleSelection,
syncViewportPosition: this.syncViewportPosition,
Expand Down Expand Up @@ -792,18 +794,15 @@ export class Table extends AbstractComponent<TableProps, TableState, TableSnapsh
// =============

private shouldDisableVerticalScroll() {
const { enableColumnHeader, enableGhostCells } = this.props;
const { enableGhostCells } = this.props;
const { viewportRect } = this.state;

if (this.grid === null || viewportRect === undefined) {
return false;
}

const columnHeaderHeight = enableColumnHeader
? this.columnHeaderElement?.clientHeight ?? Grid.MIN_COLUMN_HEADER_HEIGHT
: 0;
const rowIndices = this.grid.getRowIndicesInRect({
columnHeaderHeight,
columnHeaderHeight: this.getColumnHeaderHeight(),
includeGhostCells: enableGhostCells,
rect: viewportRect,
});
Expand Down Expand Up @@ -1412,30 +1411,21 @@ export class Table extends AbstractComponent<TableProps, TableState, TableSnapsh
private syncViewportPosition = ({ nextScrollLeft, nextScrollTop }: TableSnapshot) => {
const { viewportRect } = this.state;

if (this.scrollContainerElement == null || this.columnHeaderElement == null || viewportRect === undefined) {
if (this.scrollContainerElement == null || viewportRect === undefined) {
return;
}

if (nextScrollLeft !== undefined || nextScrollTop !== undefined) {
// 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).
if (nextScrollTop !== undefined) {
const topCorrection = this.shouldDisableVerticalScroll() ? 0 : this.columnHeaderElement.clientHeight;
this.scrollContainerElement.scrollTop = nextScrollTop + topCorrection;
this.scrollContainerElement.scrollTop = nextScrollTop;
}
if (nextScrollLeft !== undefined) {
const leftCorrection =
this.shouldDisableHorizontalScroll() || this.rowHeaderElement == null
? 0
: this.rowHeaderElement.clientWidth;

this.scrollContainerElement.scrollLeft = nextScrollLeft + leftCorrection;
this.scrollContainerElement.scrollLeft = nextScrollLeft;
}

const nextViewportRect = new Rect(
nextScrollLeft ?? 0,
nextScrollTop ?? 0,
nextScrollLeft ?? viewportRect.left,
nextScrollTop ?? viewportRect.top,
viewportRect.width,
viewportRect.height,
);
Expand Down Expand Up @@ -1558,4 +1548,21 @@ export class Table extends AbstractComponent<TableProps, TableState, TableSnapsh
private handleRowResizeGuide = (horizontalGuides: number[]) => {
this.setState({ horizontalGuides });
};

private getHeaderDimensions = (): TableHeaderDimensions => {
return {
columnHeaderHeight: this.getColumnHeaderHeight(),
rowHeaderWidth: this.getRowHeaderWidth(),
};
};

private getColumnHeaderHeight = (): number => {
return this.props.enableColumnHeader
? this.columnHeaderElement?.clientHeight ?? Grid.MIN_COLUMN_HEADER_HEIGHT
: 0;
};

private getRowHeaderWidth = (): number => {
return this.props.enableRowHeader ? this.rowHeaderElement?.clientWidth ?? Grid.MIN_ROW_HEADER_WIDTH : 0;
};
}
55 changes: 27 additions & 28 deletions packages/table/src/table2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import * as ScrollUtils from "./common/internal/scrollUtils";
import { Rect } from "./common/rect";
import { RenderMode } from "./common/renderMode";
import { ScrollDirection } from "./common/scrollDirection";
import type { TableHeaderDimensions } from "./common/TableHeaderDimensions";
import { Utils } from "./common/utils";
import { ColumnHeader } from "./headers/columnHeader";
import { ColumnHeaderCell, type ColumnHeaderCellProps } from "./headers/columnHeaderCell";
Expand Down Expand Up @@ -231,15 +232,13 @@ export class Table2 extends AbstractComponent<Table2Props, TableState, TableSnap
private refHandlers = {
cellContainer: (ref: HTMLElement | null) => (this.cellContainerElement = ref),
columnHeader: (ref: HTMLElement | null) => {
this.columnHeaderElement = ref;
if (ref != null) {
this.columnHeaderHeight = Math.max(ref.clientHeight, Grid.MIN_COLUMN_HEADER_HEIGHT);
}
},
quadrantStack: (ref: TableQuadrantStack) => (this.quadrantStackInstance = ref),
rootTable: (ref: HTMLElement | null) => (this.rootTableElement = ref),
rowHeader: (ref: HTMLElement | null) => {
this.rowHeaderElement = ref;
if (ref != null) {
this.rowHeaderWidth = ref.clientWidth;
}
Expand All @@ -249,16 +248,12 @@ export class Table2 extends AbstractComponent<Table2Props, TableState, TableSnap

private cellContainerElement?: HTMLElement | null;

private columnHeaderElement?: HTMLElement | null;

private columnHeaderHeight = Grid.MIN_COLUMN_HEADER_HEIGHT;

private quadrantStackInstance?: TableQuadrantStack;

private rootTableElement?: HTMLElement | null;

private rowHeaderElement?: HTMLElement | null;

private rowHeaderWidth = Grid.MIN_ROW_HEADER_WIDTH;

private scrollContainerElement?: HTMLElement | null;
Expand Down Expand Up @@ -328,6 +323,7 @@ export class Table2 extends AbstractComponent<Table2Props, TableState, TableSnap

this.hotkeysImpl = new TableHotkeys(props, this.state, {
getEnabledSelectionHandler: this.getEnabledSelectionHandler,
getHeaderDimensions: this.getHeaderDimensions,
handleFocus: this.handleFocus,
handleSelection: this.handleSelection,
syncViewportPosition: this.syncViewportPosition,
Expand Down Expand Up @@ -726,15 +722,15 @@ export class Table2 extends AbstractComponent<Table2Props, TableState, TableSnap
// =============

private shouldDisableVerticalScroll() {
const { enableColumnHeader, enableGhostCells } = this.props;
const { enableGhostCells } = this.props;
const { viewportRect } = this.state;

if (this.grid === null || viewportRect === undefined) {
return false;
}

const rowIndices = this.grid.getRowIndicesInRect({
columnHeaderHeight: enableColumnHeader ? this.columnHeaderHeight : 0,
columnHeaderHeight: this.getColumnHeaderHeight(),
includeGhostCells: enableGhostCells!,
rect: viewportRect,
});
Expand Down Expand Up @@ -870,7 +866,6 @@ export class Table2 extends AbstractComponent<Table2Props, TableState, TableSnap
enableGhostCells,
enableColumnReordering,
enableColumnResizing,
enableRowHeader,
loadingOptions,
maxColumnWidth,
minColumnWidth,
Expand All @@ -893,7 +888,7 @@ export class Table2 extends AbstractComponent<Table2Props, TableState, TableSnap
// if we have horizontal overflow or exact fit, no need to render ghost columns
// (this avoids problems like https://github.com/palantir/blueprint/issues/5027)
const hasHorizontalOverflowOrExactFit = this.locator.hasHorizontalOverflowOrExactFit(
enableRowHeader ? this.rowHeaderWidth : 0,
this.getRowHeaderWidth(),
viewportRect,
);
const columnIndices = this.grid.getColumnIndicesInRect(
Expand Down Expand Up @@ -948,7 +943,6 @@ export class Table2 extends AbstractComponent<Table2Props, TableState, TableSnap
const { focusedCell, selectedRegions, viewportRect } = this.state;
const {
defaultRowHeight,
enableColumnHeader,
enableMultipleSelection,
enableGhostCells,
enableRowReordering,
Expand Down Expand Up @@ -976,7 +970,7 @@ export class Table2 extends AbstractComponent<Table2Props, TableState, TableSnap
// if we have vertical overflow or exact fit, no need to render ghost rows
// (this avoids problems like https://github.com/palantir/blueprint/issues/5027)
const hasVerticalOverflowOrExactFit = this.locator.hasVerticalOverflowOrExactFit(
enableColumnHeader ? this.columnHeaderHeight : 0,
this.getColumnHeaderHeight(),
viewportRect,
);
const rowIndices = this.grid.getRowIndicesInRect({
Expand Down Expand Up @@ -1060,7 +1054,6 @@ export class Table2 extends AbstractComponent<Table2Props, TableState, TableSnap
enableMultipleSelection,
enableColumnHeader,
enableGhostCells,
enableRowHeader,
loadingOptions,
bodyContextMenuRenderer,
selectedRegionTransform,
Expand All @@ -1077,7 +1070,7 @@ export class Table2 extends AbstractComponent<Table2Props, TableState, TableSnap
viewportRect,
);
const hasHorizontalOverflowOrExactFit = this.locator.hasHorizontalOverflowOrExactFit(
enableRowHeader ? this.rowHeaderWidth : 0,
this.getRowHeaderWidth(),
viewportRect,
);
const rowIndices = this.grid.getRowIndicesInRect({
Expand Down Expand Up @@ -1478,30 +1471,21 @@ export class Table2 extends AbstractComponent<Table2Props, TableState, TableSnap
private syncViewportPosition = ({ nextScrollLeft, nextScrollTop }: TableSnapshot) => {
const { viewportRect } = this.state;

if (this.scrollContainerElement == null || this.columnHeaderElement == null || viewportRect === undefined) {
if (this.scrollContainerElement == null || viewportRect === undefined) {
return;
}

if (nextScrollLeft !== undefined || nextScrollTop !== undefined) {
// 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).
Comment on lines -1486 to -1488
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 is not true. scrollTop should not be dependent on header size here:

scrolltop.mov

Copy link
Contributor

Choose a reason for hiding this comment

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

if that's the case then I'd expect the viewportRect to not include space for the headers, re https://github.com/palantir/blueprint/pull/6882/files#diff-c2ca442c1f8367c203de64884a8526876c32365d9dd5f071887fb16183ada632R100

Copy link
Contributor Author

Choose a reason for hiding this comment

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

viewportRect does include space for the headers (it contains headers, frozen cells, and the visible "scrollable cells"). The absolute placement of the scrollable cells within the scrollable element S is such that S.scrollTop need not take frozen cell or header height into account. Hopefully the diagram in #6882 (comment) helps explain this a bit better.

if (nextScrollTop !== undefined) {
const topCorrection = this.shouldDisableVerticalScroll() ? 0 : this.columnHeaderElement.clientHeight;
this.scrollContainerElement.scrollTop = nextScrollTop + topCorrection;
this.scrollContainerElement.scrollTop = nextScrollTop;
}
if (nextScrollLeft !== undefined) {
const leftCorrection =
this.shouldDisableHorizontalScroll() || this.rowHeaderElement == null
? 0
: this.rowHeaderElement.clientWidth;

this.scrollContainerElement.scrollLeft = nextScrollLeft + leftCorrection;
this.scrollContainerElement.scrollLeft = nextScrollLeft;
Copy link
Contributor

Choose a reason for hiding this comment

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

the change here removing the "correction" looks like the scrollContainer and viewportRect will have the same scroll top/left now - which seems to be different than what https://github.com/palantir/blueprint/pull/6882/files#diff-8cd23c90b1959192d311f6112803042130c4c7709017e6cac6fd991ea89221beR313-R318 is saying

}

const nextViewportRect = new Rect(
nextScrollLeft ?? 0,
nextScrollTop ?? 0,
nextScrollLeft ?? viewportRect.left,
nextScrollTop ?? viewportRect.top,
Comment on lines -1503 to +1488
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unless both nextScrollLeft and nextScrollTop were used, we would set one value to 0 here. However, viewportRect's top-left point is the value by which the scrollable cells have been scrolled. So, we would be scrolling this back to the first row / column (likely causing batcher to schedule the rendering of cells in that region before being corrected by some other prop change).

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 this change makes sense. In getSnapshotBeforeUpdate I'm seeing

// these will only be defined if they differ from viewportRect
return { nextScrollLeft, nextScrollTop };

so using the current viewportRect top/width instead of 0 sounds like a better default


I think we could merge this now if you'd like to split this little bit off

viewportRect.width,
viewportRect.height,
);
Expand Down Expand Up @@ -1624,4 +1608,19 @@ export class Table2 extends AbstractComponent<Table2Props, TableState, TableSnap
private handleRowResizeGuide = (horizontalGuides: number[]) => {
this.setState({ horizontalGuides });
};

private getHeaderDimensions = (): TableHeaderDimensions => {
return {
columnHeaderHeight: this.getColumnHeaderHeight(),
rowHeaderWidth: this.getRowHeaderWidth(),
};
};

private getColumnHeaderHeight = (): number => {
Copy link
Contributor

Choose a reason for hiding this comment

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

my preference would be to merge all no-op refactors like this in a setup PR so we can really be reviewing a minimal diff here

Copy link
Contributor Author

@filipbalucha filipbalucha Aug 22, 2024

Choose a reason for hiding this comment

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

I actually tried putting up a PR for noops but they are so minimal they should hardly be a distractor. For instance, getColumnHeaderHeight here just encapsulates the implementation from elsewhere in the class.

return this.props.enableColumnHeader ? this.columnHeaderHeight : 0;
};

private getRowHeaderWidth = (): number => {
return this.props.enableRowHeader ? this.rowHeaderWidth : 0;
};
}
Loading