Skip to content

Commit

Permalink
Initial scroll position improvements (#902)
Browse files Browse the repository at this point in the history
* WIP

* Naming

* Fix build

* Add story
  • Loading branch information
jassmith authored Feb 10, 2024
1 parent 4bdfe38 commit b5f4659
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 68 deletions.
94 changes: 26 additions & 68 deletions packages/core/src/data-editor/data-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
mergeAndRealizeTheme,
} from "../common/styles.js";
import type { DataGridRef } from "../internal/data-grid/data-grid.js";
import { getScrollBarWidth, useEventListener, useStateWithReactiveInput, whenDefined } from "../common/utils.js";
import { getScrollBarWidth, useEventListener, whenDefined } from "../common/utils.js";
import {
isGroupEqual,
itemsAreEqual,
Expand Down Expand Up @@ -83,6 +83,8 @@ import { type Keybinds, useKeybindingsWithDefaults } from "./data-editor-keybind
import type { Highlight } from "../internal/data-grid/render/data-grid-render.cells.js";
import { useRowGroupingInner, type RowGroupingOptions } from "./row-grouping.js";
import { useRowGrouping } from "./row-grouping-api.js";
import { useInitialScrollOffset } from "./use-initial-scroll-offset.js";
import type { VisibleRegion } from "./visible-region.js";

const DataGridOverlayEditor = React.lazy(
async () => await import("../internal/data-grid-overlay-editor/data-grid-overlay-editor.js")
Expand Down Expand Up @@ -732,7 +734,6 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
const searchInputRef = React.useRef<HTMLInputElement | null>(null);
const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
const [mouseState, setMouseState] = React.useState<MouseState>();
const scrollRef = React.useRef<HTMLDivElement | null>(null);
const lastSent = React.useRef<[number, number]>();

const safeWindow = typeof window === "undefined" ? null : window;
Expand Down Expand Up @@ -1091,78 +1092,24 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
];
}, [rowMarkers, columns, rowMarkerWidth, rowMarkerTheme, rowMarkerCheckboxStyle, rowMarkerChecked]);

const [visibleRegionY, visibleRegionTy] = React.useMemo(() => {
return [
scrollOffsetY !== undefined && typeof rowHeight === "number" ? Math.floor(scrollOffsetY / rowHeight) : 0,
scrollOffsetY !== undefined && typeof rowHeight === "number" ? -(scrollOffsetY % rowHeight) : 0,
];
}, [scrollOffsetY, rowHeight]);

type VisibleRegion = Rectangle & {
/** value in px */
tx?: number;
/** value in px */
ty?: number;
extras?: {
selected?: Item;
/**
* @deprecated
*/
freezeRegion?: Rectangle;

/**
* All visible freeze regions
*/
freezeRegions?: readonly Rectangle[];
};
};

const visibleRegionRef = React.useRef<VisibleRegion>({
height: 1,
width: 1,
x: 0,
y: 0,
});
const visibleRegionInput = React.useMemo<VisibleRegion>(
() => ({
x: visibleRegionRef.current.x,
y: visibleRegionY,
width: visibleRegionRef.current.width ?? 1,
height: visibleRegionRef.current.height ?? 1,
// tx: 'TODO',
ty: visibleRegionTy,
}),
[visibleRegionTy, visibleRegionY]
);

const hasJustScrolled = React.useRef(false);

const [visibleRegion, setVisibleRegion, empty] = useStateWithReactiveInput<VisibleRegion>(visibleRegionInput);
visibleRegionRef.current = visibleRegion;

const vScrollReady = (visibleRegion.height ?? 1) > 1;
React.useLayoutEffect(() => {
if (scrollOffsetY !== undefined && scrollRef.current !== null && vScrollReady) {
if (scrollRef.current.scrollTop === scrollOffsetY) return;
scrollRef.current.scrollTop = scrollOffsetY;
if (scrollRef.current.scrollTop !== scrollOffsetY) {
empty();
}
hasJustScrolled.current = true;
}
}, [scrollOffsetY, vScrollReady, empty]);
const { setVisibleRegion, visibleRegion, scrollRef } = useInitialScrollOffset(
scrollOffsetX,
scrollOffsetY,
rowHeight,
visibleRegionRef,
() => (hasJustScrolled.current = true)
);

const hScrollReady = (visibleRegion.width ?? 1) > 1;
React.useLayoutEffect(() => {
if (scrollOffsetX !== undefined && scrollRef.current !== null && hScrollReady) {
if (scrollRef.current.scrollLeft === scrollOffsetX) return;
scrollRef.current.scrollLeft = scrollOffsetX;
if (scrollRef.current.scrollLeft !== scrollOffsetX) {
empty();
}
hasJustScrolled.current = true;
}
}, [scrollOffsetX, hScrollReady, empty]);
visibleRegionRef.current = visibleRegion;

const cellXOffset = visibleRegion.x + rowMarkerOffset;
const cellYOffset = visibleRegion.y;
Expand Down Expand Up @@ -1494,7 +1441,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
forceEditMode: true,
});
},
[getMangledCellContent, setOverlaySimple]
[getMangledCellContent, scrollRef, setOverlaySimple]
);

const scrollTo = React.useCallback<ScrollToFn>(
Expand Down Expand Up @@ -1637,6 +1584,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
rowMarkerOffset,
freezeTrailingRows,
rowMarkerWidth,
scrollRef,
totalHeaderHeight,
freezeColumns,
columns,
Expand Down Expand Up @@ -3590,6 +3538,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
getMangledCellContent,
gridSelection,
keybindings.paste,
scrollRef,
mangledCols.length,
mangledOnCellsEdited,
mangledRows,
Expand Down Expand Up @@ -3697,7 +3646,16 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
}
}
},
[columnsIn, getCellsForSelection, gridSelection, keybindings.copy, rowMarkerOffset, rows, copyHeaders]
[
columnsIn,
getCellsForSelection,
gridSelection,
keybindings.copy,
rowMarkerOffset,
scrollRef,
rows,
copyHeaders,
]
);

useEventListener("copy", onCopy, safeWindow, false, false);
Expand Down Expand Up @@ -3728,7 +3686,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
deleteRange(effectiveSelection.current.range);
}
},
[deleteRange, gridSelection, keybindings.cut, onCopy, onDelete]
[deleteRange, gridSelection, keybindings.cut, onCopy, scrollRef, onDelete]
);

useEventListener("cut", onCut, safeWindow, false, false);
Expand Down Expand Up @@ -3912,7 +3870,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
}
},
}),
[appendRow, normalSizeColumn, onCopy, onKeyDown, onPasteInternal, rowMarkerOffset, scrollTo]
[appendRow, normalSizeColumn, scrollRef, onCopy, onKeyDown, onPasteInternal, rowMarkerOffset, scrollTo]
);

const [selCol, selRow] = currentCell ?? [];
Expand Down
95 changes: 95 additions & 0 deletions packages/core/src/data-editor/use-initial-scroll-offset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as React from "react";
import type { VisibleRegion } from "./visible-region.js";
import type { DataEditorCoreProps } from "../index.js";
import { useStateWithReactiveInput } from "../common/utils.js";

function useCallbackRef<T>(initialValue: T, callback: (newVal: T) => void) {
const realRef = React.useRef<T>(initialValue);
const cbRef = React.useRef(callback);
cbRef.current = callback;

return React.useMemo(
() => ({
get current() {
return realRef.current;
},
set current(value: T) {
if (realRef.current !== value) {
realRef.current = value;
cbRef.current(value);
}
},
}),
[]
);
}

export function useInitialScrollOffset(
scrollOffsetX: number | undefined,
scrollOffsetY: number | undefined,
rowHeight: NonNullable<DataEditorCoreProps["rowHeight"]>,
visibleRegionRef: React.MutableRefObject<VisibleRegion>,
onDidScroll: () => void
) {
const [visibleRegionY, visibleRegionTy] = React.useMemo(() => {
return [
scrollOffsetY !== undefined && typeof rowHeight === "number" ? Math.floor(scrollOffsetY / rowHeight) : 0,
scrollOffsetY !== undefined && typeof rowHeight === "number" ? -(scrollOffsetY % rowHeight) : 0,
];
}, [scrollOffsetY, rowHeight]);

const visibleRegionInput = React.useMemo<VisibleRegion>(
() => ({
x: visibleRegionRef.current.x,
y: visibleRegionY,
width: visibleRegionRef.current.width ?? 1,
height: visibleRegionRef.current.height ?? 1,
// tx: 'TODO',
ty: visibleRegionTy,
}),
[visibleRegionRef, visibleRegionTy, visibleRegionY]
);

const [visibleRegion, setVisibleRegion, empty] = useStateWithReactiveInput<VisibleRegion>(visibleRegionInput);

const onDidScrollRef = React.useRef(onDidScroll);
onDidScrollRef.current = onDidScroll;

const scrollRef = useCallbackRef<HTMLDivElement | null>(null, newVal => {
if (newVal !== null && scrollOffsetY !== undefined) {
newVal.scrollTop = scrollOffsetY;
} else if (newVal !== null && scrollOffsetX !== undefined) {
newVal.scrollLeft = scrollOffsetX;
}
});

const vScrollReady = (visibleRegion.height ?? 1) > 1;
React.useLayoutEffect(() => {
if (scrollOffsetY !== undefined && scrollRef.current !== null && vScrollReady) {
if (scrollRef.current.scrollTop === scrollOffsetY) return;
scrollRef.current.scrollTop = scrollOffsetY;
if (scrollRef.current.scrollTop !== scrollOffsetY) {
empty();
}
onDidScrollRef.current();
}
}, [scrollOffsetY, vScrollReady, empty, scrollRef]);

const hScrollReady = (visibleRegion.width ?? 1) > 1;
React.useLayoutEffect(() => {
if (scrollOffsetX !== undefined && scrollRef.current !== null && hScrollReady) {
if (scrollRef.current.scrollLeft === scrollOffsetX) return;
scrollRef.current.scrollLeft = scrollOffsetX;
if (scrollRef.current.scrollLeft !== scrollOffsetX) {
empty();
}
onDidScrollRef.current();
}
}, [scrollOffsetX, hScrollReady, empty, scrollRef]);

return {
visibleRegion,
setVisibleRegion,
scrollRef,
};
}
20 changes: 20 additions & 0 deletions packages/core/src/data-editor/visible-region.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { type Rectangle, type Item } from "../internal/data-grid/data-grid-types.js";

export type VisibleRegion = Rectangle & {
/** value in px */
tx?: number;
/** value in px */
ty?: number;
extras?: {
selected?: Item;
/**
* @deprecated
*/
freezeRegion?: Rectangle;

/**
* All visible freeze regions
*/
freezeRegions?: readonly Rectangle[];
};
};
49 changes: 49 additions & 0 deletions packages/core/src/docs/examples/scroll-offset.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from "react";
import { DataEditorAll as DataEditor } from "../../data-editor-all.js";
import {
BeautifulWrapper,
Description,
PropName,
useMockDataGenerator,
defaultProps,
} from "../../data-editor/stories/utils.js";
import { SimpleThemeWrapper } from "../../stories/story-utils.js";
import _ from "lodash";

export default {
title: "Glide-Data-Grid/DataEditor Demos",

decorators: [
(Story: React.ComponentType) => (
<SimpleThemeWrapper>
<BeautifulWrapper
title="Scroll Offset"
description={
<Description>
The <PropName>rowGrouping</PropName> prop can be used to group and even fold rows.
</Description>
}>
<Story />
</BeautifulWrapper>
</SimpleThemeWrapper>
),
],
};

export const ScrollOffset: React.VFC<any> = () => {
const { cols, getCellContent } = useMockDataGenerator(100);
const rows = 1000;

return (
<DataEditor
{...defaultProps}
height="100%"
rowMarkers="both"
scrollOffsetY={400}
getCellContent={getCellContent}
columns={cols}
// verticalBorder={false}
rows={rows}
/>
);
};

0 comments on commit b5f4659

Please sign in to comment.