Skip to content

Commit b5f4659

Browse files
authored
Initial scroll position improvements (#902)
* WIP * Naming * Fix build * Add story
1 parent 4bdfe38 commit b5f4659

File tree

4 files changed

+190
-68
lines changed

4 files changed

+190
-68
lines changed

packages/core/src/data-editor/data-editor.tsx

Lines changed: 26 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import {
4444
mergeAndRealizeTheme,
4545
} from "../common/styles.js";
4646
import type { DataGridRef } from "../internal/data-grid/data-grid.js";
47-
import { getScrollBarWidth, useEventListener, useStateWithReactiveInput, whenDefined } from "../common/utils.js";
47+
import { getScrollBarWidth, useEventListener, whenDefined } from "../common/utils.js";
4848
import {
4949
isGroupEqual,
5050
itemsAreEqual,
@@ -83,6 +83,8 @@ import { type Keybinds, useKeybindingsWithDefaults } from "./data-editor-keybind
8383
import type { Highlight } from "../internal/data-grid/render/data-grid-render.cells.js";
8484
import { useRowGroupingInner, type RowGroupingOptions } from "./row-grouping.js";
8585
import { useRowGrouping } from "./row-grouping-api.js";
86+
import { useInitialScrollOffset } from "./use-initial-scroll-offset.js";
87+
import type { VisibleRegion } from "./visible-region.js";
8688

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

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

1094-
const [visibleRegionY, visibleRegionTy] = React.useMemo(() => {
1095-
return [
1096-
scrollOffsetY !== undefined && typeof rowHeight === "number" ? Math.floor(scrollOffsetY / rowHeight) : 0,
1097-
scrollOffsetY !== undefined && typeof rowHeight === "number" ? -(scrollOffsetY % rowHeight) : 0,
1098-
];
1099-
}, [scrollOffsetY, rowHeight]);
1100-
1101-
type VisibleRegion = Rectangle & {
1102-
/** value in px */
1103-
tx?: number;
1104-
/** value in px */
1105-
ty?: number;
1106-
extras?: {
1107-
selected?: Item;
1108-
/**
1109-
* @deprecated
1110-
*/
1111-
freezeRegion?: Rectangle;
1112-
1113-
/**
1114-
* All visible freeze regions
1115-
*/
1116-
freezeRegions?: readonly Rectangle[];
1117-
};
1118-
};
1119-
11201095
const visibleRegionRef = React.useRef<VisibleRegion>({
11211096
height: 1,
11221097
width: 1,
11231098
x: 0,
11241099
y: 0,
11251100
});
1126-
const visibleRegionInput = React.useMemo<VisibleRegion>(
1127-
() => ({
1128-
x: visibleRegionRef.current.x,
1129-
y: visibleRegionY,
1130-
width: visibleRegionRef.current.width ?? 1,
1131-
height: visibleRegionRef.current.height ?? 1,
1132-
// tx: 'TODO',
1133-
ty: visibleRegionTy,
1134-
}),
1135-
[visibleRegionTy, visibleRegionY]
1136-
);
11371101

11381102
const hasJustScrolled = React.useRef(false);
11391103

1140-
const [visibleRegion, setVisibleRegion, empty] = useStateWithReactiveInput<VisibleRegion>(visibleRegionInput);
1141-
visibleRegionRef.current = visibleRegion;
1142-
1143-
const vScrollReady = (visibleRegion.height ?? 1) > 1;
1144-
React.useLayoutEffect(() => {
1145-
if (scrollOffsetY !== undefined && scrollRef.current !== null && vScrollReady) {
1146-
if (scrollRef.current.scrollTop === scrollOffsetY) return;
1147-
scrollRef.current.scrollTop = scrollOffsetY;
1148-
if (scrollRef.current.scrollTop !== scrollOffsetY) {
1149-
empty();
1150-
}
1151-
hasJustScrolled.current = true;
1152-
}
1153-
}, [scrollOffsetY, vScrollReady, empty]);
1104+
const { setVisibleRegion, visibleRegion, scrollRef } = useInitialScrollOffset(
1105+
scrollOffsetX,
1106+
scrollOffsetY,
1107+
rowHeight,
1108+
visibleRegionRef,
1109+
() => (hasJustScrolled.current = true)
1110+
);
11541111

1155-
const hScrollReady = (visibleRegion.width ?? 1) > 1;
1156-
React.useLayoutEffect(() => {
1157-
if (scrollOffsetX !== undefined && scrollRef.current !== null && hScrollReady) {
1158-
if (scrollRef.current.scrollLeft === scrollOffsetX) return;
1159-
scrollRef.current.scrollLeft = scrollOffsetX;
1160-
if (scrollRef.current.scrollLeft !== scrollOffsetX) {
1161-
empty();
1162-
}
1163-
hasJustScrolled.current = true;
1164-
}
1165-
}, [scrollOffsetX, hScrollReady, empty]);
1112+
visibleRegionRef.current = visibleRegion;
11661113

11671114
const cellXOffset = visibleRegion.x + rowMarkerOffset;
11681115
const cellYOffset = visibleRegion.y;
@@ -1494,7 +1441,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
14941441
forceEditMode: true,
14951442
});
14961443
},
1497-
[getMangledCellContent, setOverlaySimple]
1444+
[getMangledCellContent, scrollRef, setOverlaySimple]
14981445
);
14991446

15001447
const scrollTo = React.useCallback<ScrollToFn>(
@@ -1637,6 +1584,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
16371584
rowMarkerOffset,
16381585
freezeTrailingRows,
16391586
rowMarkerWidth,
1587+
scrollRef,
16401588
totalHeaderHeight,
16411589
freezeColumns,
16421590
columns,
@@ -3590,6 +3538,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
35903538
getMangledCellContent,
35913539
gridSelection,
35923540
keybindings.paste,
3541+
scrollRef,
35933542
mangledCols.length,
35943543
mangledOnCellsEdited,
35953544
mangledRows,
@@ -3697,7 +3646,16 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
36973646
}
36983647
}
36993648
},
3700-
[columnsIn, getCellsForSelection, gridSelection, keybindings.copy, rowMarkerOffset, rows, copyHeaders]
3649+
[
3650+
columnsIn,
3651+
getCellsForSelection,
3652+
gridSelection,
3653+
keybindings.copy,
3654+
rowMarkerOffset,
3655+
scrollRef,
3656+
rows,
3657+
copyHeaders,
3658+
]
37013659
);
37023660

37033661
useEventListener("copy", onCopy, safeWindow, false, false);
@@ -3728,7 +3686,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
37283686
deleteRange(effectiveSelection.current.range);
37293687
}
37303688
},
3731-
[deleteRange, gridSelection, keybindings.cut, onCopy, onDelete]
3689+
[deleteRange, gridSelection, keybindings.cut, onCopy, scrollRef, onDelete]
37323690
);
37333691

37343692
useEventListener("cut", onCut, safeWindow, false, false);
@@ -3912,7 +3870,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
39123870
}
39133871
},
39143872
}),
3915-
[appendRow, normalSizeColumn, onCopy, onKeyDown, onPasteInternal, rowMarkerOffset, scrollTo]
3873+
[appendRow, normalSizeColumn, scrollRef, onCopy, onKeyDown, onPasteInternal, rowMarkerOffset, scrollTo]
39163874
);
39173875

39183876
const [selCol, selRow] = currentCell ?? [];
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import * as React from "react";
2+
import type { VisibleRegion } from "./visible-region.js";
3+
import type { DataEditorCoreProps } from "../index.js";
4+
import { useStateWithReactiveInput } from "../common/utils.js";
5+
6+
function useCallbackRef<T>(initialValue: T, callback: (newVal: T) => void) {
7+
const realRef = React.useRef<T>(initialValue);
8+
const cbRef = React.useRef(callback);
9+
cbRef.current = callback;
10+
11+
return React.useMemo(
12+
() => ({
13+
get current() {
14+
return realRef.current;
15+
},
16+
set current(value: T) {
17+
if (realRef.current !== value) {
18+
realRef.current = value;
19+
cbRef.current(value);
20+
}
21+
},
22+
}),
23+
[]
24+
);
25+
}
26+
27+
export function useInitialScrollOffset(
28+
scrollOffsetX: number | undefined,
29+
scrollOffsetY: number | undefined,
30+
rowHeight: NonNullable<DataEditorCoreProps["rowHeight"]>,
31+
visibleRegionRef: React.MutableRefObject<VisibleRegion>,
32+
onDidScroll: () => void
33+
) {
34+
const [visibleRegionY, visibleRegionTy] = React.useMemo(() => {
35+
return [
36+
scrollOffsetY !== undefined && typeof rowHeight === "number" ? Math.floor(scrollOffsetY / rowHeight) : 0,
37+
scrollOffsetY !== undefined && typeof rowHeight === "number" ? -(scrollOffsetY % rowHeight) : 0,
38+
];
39+
}, [scrollOffsetY, rowHeight]);
40+
41+
const visibleRegionInput = React.useMemo<VisibleRegion>(
42+
() => ({
43+
x: visibleRegionRef.current.x,
44+
y: visibleRegionY,
45+
width: visibleRegionRef.current.width ?? 1,
46+
height: visibleRegionRef.current.height ?? 1,
47+
// tx: 'TODO',
48+
ty: visibleRegionTy,
49+
}),
50+
[visibleRegionRef, visibleRegionTy, visibleRegionY]
51+
);
52+
53+
const [visibleRegion, setVisibleRegion, empty] = useStateWithReactiveInput<VisibleRegion>(visibleRegionInput);
54+
55+
const onDidScrollRef = React.useRef(onDidScroll);
56+
onDidScrollRef.current = onDidScroll;
57+
58+
const scrollRef = useCallbackRef<HTMLDivElement | null>(null, newVal => {
59+
if (newVal !== null && scrollOffsetY !== undefined) {
60+
newVal.scrollTop = scrollOffsetY;
61+
} else if (newVal !== null && scrollOffsetX !== undefined) {
62+
newVal.scrollLeft = scrollOffsetX;
63+
}
64+
});
65+
66+
const vScrollReady = (visibleRegion.height ?? 1) > 1;
67+
React.useLayoutEffect(() => {
68+
if (scrollOffsetY !== undefined && scrollRef.current !== null && vScrollReady) {
69+
if (scrollRef.current.scrollTop === scrollOffsetY) return;
70+
scrollRef.current.scrollTop = scrollOffsetY;
71+
if (scrollRef.current.scrollTop !== scrollOffsetY) {
72+
empty();
73+
}
74+
onDidScrollRef.current();
75+
}
76+
}, [scrollOffsetY, vScrollReady, empty, scrollRef]);
77+
78+
const hScrollReady = (visibleRegion.width ?? 1) > 1;
79+
React.useLayoutEffect(() => {
80+
if (scrollOffsetX !== undefined && scrollRef.current !== null && hScrollReady) {
81+
if (scrollRef.current.scrollLeft === scrollOffsetX) return;
82+
scrollRef.current.scrollLeft = scrollOffsetX;
83+
if (scrollRef.current.scrollLeft !== scrollOffsetX) {
84+
empty();
85+
}
86+
onDidScrollRef.current();
87+
}
88+
}, [scrollOffsetX, hScrollReady, empty, scrollRef]);
89+
90+
return {
91+
visibleRegion,
92+
setVisibleRegion,
93+
scrollRef,
94+
};
95+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { type Rectangle, type Item } from "../internal/data-grid/data-grid-types.js";
2+
3+
export type VisibleRegion = Rectangle & {
4+
/** value in px */
5+
tx?: number;
6+
/** value in px */
7+
ty?: number;
8+
extras?: {
9+
selected?: Item;
10+
/**
11+
* @deprecated
12+
*/
13+
freezeRegion?: Rectangle;
14+
15+
/**
16+
* All visible freeze regions
17+
*/
18+
freezeRegions?: readonly Rectangle[];
19+
};
20+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from "react";
2+
import { DataEditorAll as DataEditor } from "../../data-editor-all.js";
3+
import {
4+
BeautifulWrapper,
5+
Description,
6+
PropName,
7+
useMockDataGenerator,
8+
defaultProps,
9+
} from "../../data-editor/stories/utils.js";
10+
import { SimpleThemeWrapper } from "../../stories/story-utils.js";
11+
import _ from "lodash";
12+
13+
export default {
14+
title: "Glide-Data-Grid/DataEditor Demos",
15+
16+
decorators: [
17+
(Story: React.ComponentType) => (
18+
<SimpleThemeWrapper>
19+
<BeautifulWrapper
20+
title="Scroll Offset"
21+
description={
22+
<Description>
23+
The <PropName>rowGrouping</PropName> prop can be used to group and even fold rows.
24+
</Description>
25+
}>
26+
<Story />
27+
</BeautifulWrapper>
28+
</SimpleThemeWrapper>
29+
),
30+
],
31+
};
32+
33+
export const ScrollOffset: React.VFC<any> = () => {
34+
const { cols, getCellContent } = useMockDataGenerator(100);
35+
const rows = 1000;
36+
37+
return (
38+
<DataEditor
39+
{...defaultProps}
40+
height="100%"
41+
rowMarkers="both"
42+
scrollOffsetY={400}
43+
getCellContent={getCellContent}
44+
columns={cols}
45+
// verticalBorder={false}
46+
rows={rows}
47+
/>
48+
);
49+
};

0 commit comments

Comments
 (0)