Skip to content

Commit

Permalink
feat: [sc-63907] allow skipping scrollToIndex
Browse files Browse the repository at this point in the history
  • Loading branch information
blambillotte committed Dec 20, 2024
1 parent 593c04d commit 7e9ec31
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 13 deletions.
13 changes: 11 additions & 2 deletions src/components/Table/GridTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import memoizeOne from "memoize-one";
import { runInAction } from "mobx";
import React, { MutableRefObject, ReactElement, useEffect, useMemo, useRef, useState } from "react";
import { Components, Virtuoso, VirtuosoHandle } from "react-virtuoso";
import { Components, ListRange, Virtuoso, VirtuosoHandle } from "react-virtuoso";
import { getTableRefWidthStyles, Loader } from "src/components";
import { DiscriminateUnion, GridRowKind } from "src/components/index";
import { PresentationFieldProps, PresentationProvider } from "src/components/PresentationContext";
Expand Down Expand Up @@ -231,14 +231,16 @@ export function GridTable<R extends Kinded, X extends Only<GridTableXss, X> = an

// We only use this in as=virtual mode, but keep this here for rowLookup to use
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
// Stores the current rendered range of rows from virtuoso (used for determining if we can skip re-scrolling to a row if already in view)
const virtuosoRangeRef = useRef<ListRange | null>(null);
// Use this ref to watch for changes in the GridTable's container and resize columns accordingly.
const resizeRef = useRef<HTMLDivElement>(null);

const api = useMemo<GridTableApiImpl<R>>(
() => {
// Let the user pass in their own api handle, otherwise make our own
const api = (props.api as GridTableApiImpl<R>) ?? new GridTableApiImpl();
api.init(persistCollapse, virtuosoRef);
api.init(persistCollapse, virtuosoRef, virtuosoRangeRef);
api.setActiveRowId(activeRowId);
api.setActiveCellId(activeCellId);
// Push the initial columns directly into tableState, b/c that is what
Expand Down Expand Up @@ -505,6 +507,7 @@ export function GridTable<R extends Kinded, X extends Only<GridTableXss, X> = an
stickyHeader,
xss,
virtuosoRef,
virtuosoRangeRef,
tableHeadRows,
stickyOffset,
infiniteScroll,
Expand Down Expand Up @@ -532,6 +535,7 @@ function renderDiv<R extends Kinded>(
stickyHeader: boolean,
xss: any,
_virtuosoRef: MutableRefObject<VirtuosoHandle | null>,
_virtuosoRangeRef: MutableRefObject<ListRange | null>,
tableHeadRows: ReactElement[],
stickyOffset: number,
_infiniteScroll?: InfiniteScroll,
Expand Down Expand Up @@ -592,6 +596,7 @@ function renderTable<R extends Kinded>(
stickyHeader: boolean,
xss: any,
_virtuosoRef: MutableRefObject<VirtuosoHandle | null>,
_virtuosoRangeRef: MutableRefObject<ListRange | null>,
tableHeadRows: ReactElement[],
stickyOffset: number,
_infiniteScroll?: InfiniteScroll,
Expand Down Expand Up @@ -659,6 +664,7 @@ function renderVirtual<R extends Kinded>(
stickyHeader: boolean,
xss: any,
virtuosoRef: MutableRefObject<VirtuosoHandle | null>,
virtuosoRangeRef: MutableRefObject<ListRange | null>,
tableHeadRows: ReactElement[],
_stickyOffset: number,
infiniteScroll?: InfiniteScroll,
Expand Down Expand Up @@ -737,6 +743,9 @@ function renderVirtual<R extends Kinded>(
// Lastly render the table body rows
return visibleDataRows[index];
}}
rangeChanged={(newRange) => {
virtuosoRangeRef.current = newRange;
}}
totalCount={tableHeadRows.length + (firstRowMessage ? 1 : 0) + visibleDataRows.length + keptSelectedRows.length}
// When implementing infinite scroll, default the bottom `increaseViewportBy` to 500px. This creates the "infinite"
// effect such that the next page of data is (hopefully) loaded before the user reaches the true bottom
Expand Down
27 changes: 21 additions & 6 deletions src/components/Table/GridTableApi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { comparer } from "mobx";
import { computedFn } from "mobx-utils";
import { MutableRefObject, useMemo } from "react";
import { VirtuosoHandle } from "react-virtuoso";
import { ListRange, VirtuosoHandle } from "react-virtuoso";
import {
applyRowFn,
createRowLookup,
Expand All @@ -10,6 +10,7 @@ import {
isGridCellContent,
isJSX,
MaybeFn,
shouldSkipScrollTo,
} from "src/components/index";
import { GridDataRow } from "src/components/Table/components/Row";
import { DiscriminateUnion, Kinded } from "src/components/Table/types";
Expand Down Expand Up @@ -104,6 +105,7 @@ export class GridTableApiImpl<R extends Kinded> implements GridTableApi<R> {
// This is public to GridTable but not exported outside of Beam
readonly tableState: TableState<R> = new TableState(this);
virtuosoRef: MutableRefObject<VirtuosoHandle | null> = { current: null };
virtuosoRangeRef: MutableRefObject<ListRange | null> = { current: null };
lookup!: GridRowLookup<R>;

constructor() {
Expand All @@ -118,16 +120,29 @@ export class GridTableApiImpl<R extends Kinded> implements GridTableApi<R> {
}

/** Called once by the GridTable when it takes ownership of this api instance. */
init(persistCollapse: string | undefined, virtuosoRef: MutableRefObject<VirtuosoHandle | null>) {
init(
persistCollapse: string | undefined,
virtuosoRef: MutableRefObject<VirtuosoHandle | null>,
virtuosoRangeRef: MutableRefObject<ListRange | null>,
) {
// Technically this drives both row-collapse and column-expanded
if (persistCollapse) this.tableState.loadCollapse(persistCollapse);
this.virtuosoRef = virtuosoRef;
this.lookup = createRowLookup(this, virtuosoRef);
this.virtuosoRangeRef = virtuosoRangeRef;
this.lookup = createRowLookup(this, virtuosoRef, virtuosoRangeRef);
}

public scrollToIndex(index: GridTableScrollOptions): void {
this.virtuosoRef.current &&
this.virtuosoRef.current.scrollToIndex(typeof index === "number" ? { index, behavior: "smooth" } : index);
public scrollToIndex(indexOrOptions: GridTableScrollOptions): void {
if (!this.virtuosoRef.current) return;

const { forceRescroll = true, ...scrollToOpts } =
typeof indexOrOptions === "number"
? { index: indexOrOptions, behavior: "smooth" as const, forceRescroll: false }
: indexOrOptions;

if (shouldSkipScrollTo(scrollToOpts.index, this.virtuosoRangeRef, forceRescroll)) return;

this.virtuosoRef.current.scrollToIndex(scrollToOpts);
}

public getSelectedRowIds(kind?: string): string[] {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export { cardStyle, condensedStyle, defaultStyle, getTableStyles } from "src/com
export type { GridStyle, RowStyle, RowStyles } from "src/components/Table/TableStyles";
export * from "src/components/Table/types";
export * from "src/components/Table/utils/columns";
export { createRowLookup } from "src/components/Table/utils/GridRowLookup";
export { createRowLookup, shouldSkipScrollTo } from "src/components/Table/utils/GridRowLookup";
export type { GridRowLookup } from "src/components/Table/utils/GridRowLookup";
export { simpleDataRows, simpleHeader } from "src/components/Table/utils/simpleHelpers";
export type { SimpleHeaderAndData } from "src/components/Table/utils/simpleHelpers";
Expand Down
2 changes: 2 additions & 0 deletions src/components/Table/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export type GridTableScrollOptions =
* How to position the row in the viewport
*/
align?: "start" | "center" | "end";
/** Determines if the row should be re-scrolled to even if it's already in view (scrolled to top) */
forceRescroll?: boolean;
};

/**
Expand Down
33 changes: 29 additions & 4 deletions src/components/Table/utils/GridRowLookup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MutableRefObject } from "react";
import { VirtuosoHandle } from "react-virtuoso";
import { ListRange, VirtuosoHandle } from "react-virtuoso";
import { GridDataRow } from "src/components/Table/components/Row";
import { GridTableApiImpl } from "src/components/Table/GridTableApi";
import { DiscriminateUnion, GridColumnWithId, Kinded, nonKindGridColumnKeys } from "src/components/Table/types";
Expand All @@ -22,8 +22,11 @@ export interface GridRowLookup<R extends Kinded> {
/** Returns the list of currently filtered/sorted rows, without headers. */
currentList(): readonly GridDataRow<R>[];

/** Scroll's to the row with the given kind + id. Requires using `as=virtual`. */
scrollTo(kind: R["kind"], id: string): void;
/**
* Scroll's to the row with the given kind + id. Requires using `as=virtual`.
* `forceRescroll` controls if an element should be re-scrolled to the top even if it's already visible. (Default false)
*/
scrollTo(kind: R["kind"], id: string, forceRescroll?: boolean): void;
}

interface NextPrev<R extends Kinded> {
Expand All @@ -34,15 +37,20 @@ interface NextPrev<R extends Kinded> {
export function createRowLookup<R extends Kinded>(
api: GridTableApiImpl<R>,
virtuosoRef: MutableRefObject<VirtuosoHandle | null>,
virtuosoRangeRef: MutableRefObject<ListRange | null>,
): GridRowLookup<R> {
return {
scrollTo(kind, id) {
scrollTo(kind, id, forceRescroll = true) {
if (virtuosoRef.current === null) {
// In theory we could support as=div and as=table by finding the DOM
// element and calling .scrollIntoView, just not doing that yet.
throw new Error("scrollTo is only supported for as=virtual");
}

const index = api.tableState.visibleRows.findIndex((r) => r && r.kind === kind && r.row.id === id);

if (shouldSkipScrollTo(index, virtuosoRangeRef, forceRescroll)) return;

virtuosoRef.current.scrollToIndex({ index, behavior: "smooth" });
},
currentList() {
Expand Down Expand Up @@ -83,3 +91,20 @@ export function getKinds<R extends Kinded>(columns: GridColumnWithId<R>[]): R[]
(key) => !nonKindGridColumnKeys.includes(key),
) as any;
}

/** Optionally takes into consideration if a row is already in view before attempting to scroll to it. */
export function shouldSkipScrollTo(
index: number,
virtuosoRangeRef: MutableRefObject<ListRange | null>,
forceRescroll: boolean,
) {
if (!virtuosoRangeRef.current || forceRescroll) return false;

const isAlreadyInView =
// Add 1 on each end to account for "overscan" where the next out of view row is usually already rendered
index >= virtuosoRangeRef.current.startIndex - 1 && index <= virtuosoRangeRef.current.endIndex + 1;

console.log({ index, virtuosoRangeRef: virtuosoRangeRef.current, isAlreadyInView });

return isAlreadyInView;
}

0 comments on commit 7e9ec31

Please sign in to comment.