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

Support automatic start offset calculation #339

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const App = () => {
return (
<div style={{ overflowY: "auto", height: 800 }}>
<div style={{ height: 40 }}>header</div>
<Virtualizer startMargin={40}>
<Virtualizer startOffset="static">
{Array.from({ length: 1000 }).map((_, i) => (
<div
key={i}
Expand Down
98 changes: 62 additions & 36 deletions src/core/scroller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
ACTION_BEFORE_MANUAL_SMOOTH_SCROLL,
ACTION_START_OFFSET_CHANGE,
} from "./store";
import { type ScrollToIndexOpts } from "./types";
import { type ScrollToIndexOpts, StartOffsetType } from "./types";
import { debounce, timeout, clamp, microtask } from "./utils";

/**
Expand All @@ -34,6 +34,32 @@ const normalizeOffset = (offset: number, isHorizontal: boolean): number => {
}
};

const calcOffsetToViewport = (
node: HTMLElement,
viewport: HTMLElement,
isHorizontal: boolean,
offset: number = 0
): number => {
// TODO calc offset only when it changes (maybe impossible)
const offsetSum =
offset +
(isHorizontal && isRTLDocument()
? viewport.offsetWidth - node.offsetLeft - node.offsetWidth
: node[isHorizontal ? "offsetLeft" : "offsetTop"]);

const parent = node.offsetParent;
if (node === viewport || !parent) {
return offsetSum;
}

return calcOffsetToViewport(
parent as HTMLElement,
viewport,
isHorizontal,
offsetSum
);
};

const createScrollObserver = (
store: VirtualStore,
viewport: HTMLElement | Window,
Expand Down Expand Up @@ -121,6 +147,10 @@ const createScrollObserver = (
}
};

if (getStartOffset) {
store._update(ACTION_START_OFFSET_CHANGE, getStartOffset());
}

viewport.addEventListener("scroll", onScroll);
viewport.addEventListener("wheel", onWheel, { passive: true });
viewport.addEventListener("touchstart", onTouchStart, { passive: true });
Expand Down Expand Up @@ -159,7 +189,10 @@ type ScrollObserver = ReturnType<typeof createScrollObserver>;
* @internal
*/
export type Scroller = {
_observe: (viewportElement: HTMLElement) => void;
_observe: (
viewportElement: HTMLElement,
containerElement: HTMLElement
) => void;
_dispose(): void;
_scrollTo: (offset: number) => void;
_scrollBy: (offset: number) => void;
Expand All @@ -172,7 +205,8 @@ export type Scroller = {
*/
export const createScroller = (
store: VirtualStore,
isHorizontal: boolean
isHorizontal: boolean,
startOffset?: StartOffsetType
): Scroller => {
let viewportElement: HTMLElement | undefined;
let scrollObserver: ScrollObserver | undefined;
Expand Down Expand Up @@ -264,9 +298,24 @@ export const createScroller = (
};

return {
_observe(viewport) {
_observe(viewport, container) {
viewportElement = viewport;

let getStartOffset: (() => number) | undefined;
if (startOffset === "dynamic") {
getStartOffset = () =>
calcOffsetToViewport(container, viewport, isHorizontal);
} else if (startOffset === "static") {
const staticStartOffset = calcOffsetToViewport(
container,
viewport,
isHorizontal
);
getStartOffset = () => staticStartOffset;
} else if (typeof startOffset === "number") {
getStartOffset = () => startOffset;
}

scrollObserver = createScrollObserver(
store,
viewport,
Expand All @@ -293,7 +342,8 @@ export const createScroller = (
} else {
viewport[scrollOffsetKey] += jump;
}
}
},
getStartOffset
);
},
_dispose() {
Expand Down Expand Up @@ -371,33 +421,6 @@ export const createWindowScroller = (
const window = getCurrentWindow(document);
const documentBody = document.body;

const calcOffsetToViewport = (
node: HTMLElement,
viewport: HTMLElement,
isHorizontal: boolean,
offset: number = 0
): number => {
// TODO calc offset only when it changes (maybe impossible)
const offsetKey = isHorizontal ? "offsetLeft" : "offsetTop";
const offsetSum =
offset +
(isHorizontal && isRTLDocument()
? window.innerWidth - node[offsetKey] - node.offsetWidth
: node[offsetKey]);

const parent = node.offsetParent;
if (node === viewport || !parent) {
return offsetSum;
}

return calcOffsetToViewport(
parent as HTMLElement,
viewport,
isHorizontal,
offsetSum
);
};

scrollObserver = createScrollObserver(
store,
window,
Expand Down Expand Up @@ -429,7 +452,10 @@ export const createWindowScroller = (
* @internal
*/
export type GridScroller = {
_observe: (viewportElement: HTMLElement) => void;
_observe: (
viewportElement: HTMLElement,
containerElement: HTMLElement
) => void;
_dispose(): void;
_scrollTo: (offsetX: number, offsetY: number) => void;
_scrollBy: (offsetX: number, offsetY: number) => void;
Expand All @@ -447,9 +473,9 @@ export const createGridScroller = (
const vScroller = createScroller(vStore, false);
const hScroller = createScroller(hStore, true);
return {
_observe(viewportElement) {
vScroller._observe(viewportElement);
hScroller._observe(viewportElement);
_observe(viewportElement, containerElement) {
vScroller._observe(viewportElement, containerElement);
hScroller._observe(viewportElement, containerElement);
},
_dispose() {
vScroller._dispose();
Expand Down
10 changes: 5 additions & 5 deletions src/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,13 @@ export const createVirtualStore = (
itemSize: number = 40,
ssrCount: number = 0,
cacheSnapshot?: CacheSnapshot | undefined,
shouldAutoEstimateItemSize: boolean = false,
startSpacerSize: number = 0
shouldAutoEstimateItemSize: boolean = false
): VirtualStore => {
let isSSR = !!ssrCount;
let stateVersion: StateVersion = [];
let viewportSize = 0;
let scrollOffset = 0;
let startOffset = 0;
let jumpCount = 0;
let jump = 0;
let pendingJump = 0;
Expand All @@ -165,7 +165,7 @@ export const createVirtualStore = (
cacheSnapshot as unknown as InternalCacheSnapshot | undefined
);
const subscribers = new Set<[number, Subscriber]>();
const getRelativeScrollOffset = () => scrollOffset - startSpacerSize;
const getRelativeScrollOffset = () => scrollOffset - startOffset;
const getRange = (offset: number) => {
return computeRange(cache, offset, _prevRange[0], viewportSize);
};
Expand Down Expand Up @@ -240,7 +240,7 @@ export const createVirtualStore = (
return viewportSize;
},
_getStartSpacerSize() {
return startSpacerSize;
return startOffset;
},
_getTotalSize: getTotalSize,
_getJumpCount() {
Expand Down Expand Up @@ -418,7 +418,7 @@ export const createVirtualStore = (
break;
}
case ACTION_START_OFFSET_CHANGE: {
startSpacerSize = payload;
startOffset = payload;
break;
}
case ACTION_MANUAL_SCROLL: {
Expand Down
2 changes: 2 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ export interface ScrollToIndexOpts {
*/
offset?: number;
}

export type StartOffsetType = "dynamic" | "static" | number;
5 changes: 4 additions & 1 deletion src/react/VGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import { flushSync } from "react-dom";
import { isRTLDocument } from "../core/environment";
import { useRerender } from "./useRerender";

const genKey = (i: number, j: number) => `${i}-${j}`;

/**
Expand Down Expand Up @@ -250,9 +251,11 @@
const height = getScrollSize(vStore);
const width = getScrollSize(hStore);
const rootRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);

useIsomorphicLayoutEffect(() => {
const root = rootRef[refKey]!;
const container = containerRef[refKey]!;
// store must be subscribed first because others may dispatch update on init depending on implementation
const unsubscribeVStore = vStore._subscribe(
UPDATE_VIRTUAL_STATE,
Expand All @@ -275,7 +278,7 @@
}
);
resizer._observeRoot(root);
scroller._observe(root);
scroller._observe(root, container);
return () => {
unsubscribeVStore();
unsubscribeHStore();
Expand Down Expand Up @@ -315,7 +318,7 @@
scrollBy: scroller._scrollBy,
};
},
[]

Check warning on line 321 in src/react/VGrid.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useImperativeHandle has missing dependencies: 'hStore', 'scroller._scrollBy', 'scroller._scrollTo', 'scroller._scrollToIndex', and 'vStore'. Either include them or remove the dependency array
);

const render = useMemo(() => {
Expand Down
22 changes: 14 additions & 8 deletions src/react/Virtualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
import { useLatestRef } from "./useLatestRef";
import { createResizer } from "../core/resizer";
import { ListItem } from "./ListItem";
import { CacheSnapshot, ScrollToIndexOpts } from "../core/types";
import {
CacheSnapshot,
ScrollToIndexOpts,
StartOffsetType,
} from "../core/types";
import { flushSync } from "react-dom";
import { useRerender } from "./useRerender";
import { useChildren } from "./useChildren";
Expand Down Expand Up @@ -120,8 +124,10 @@
cache?: CacheSnapshot;
/**
* If you put an element before virtualizer, you have to define its height with this prop.
*
* TODO
*/
startMargin?: number;
startOffset?: StartOffsetType;
/**
* A prop for SSR. If set, the specified amount of items will be mounted in the initial rendering regardless of the container size until hydrated.
*/
Expand Down Expand Up @@ -178,7 +184,7 @@
shift,
horizontal: horizontalProp,
cache,
startMargin,
startOffset,
ssrCount,
as: Element = "div",
item: ItemElement = "div",
Expand Down Expand Up @@ -207,13 +213,12 @@
itemSize,
ssrCount,
cache,
!itemSize,
startMargin
!itemSize
);
return [
_store,
createResizer(_store, _isHorizontal),
createScroller(_store, _isHorizontal),
createScroller(_store, _isHorizontal, startOffset),
_isHorizontal,
];
});
Expand Down Expand Up @@ -281,15 +286,16 @@
onScrollEnd[refKey] && onScrollEnd[refKey]();
}
);
const container = containerRef[refKey]!;
const assignScrollableElement = (e: HTMLElement) => {
resizer._observeRoot(e);
scroller._observe(e);
scroller._observe(e, container);
};
if (scrollRef) {
// parent's ref doesn't exist when useLayoutEffect is called
microtask(() => assignScrollableElement(scrollRef[refKey]!));
} else {
assignScrollableElement(containerRef[refKey]!.parentElement!);
assignScrollableElement(container.parentElement!);
}

return () => {
Expand All @@ -309,7 +315,7 @@
if (!onRangeChangeProp) return;

onRangeChangeProp(startIndex, endIndex);
}, [startIndex, endIndex]);

Check warning on line 318 in src/react/Virtualizer.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useEffect has a missing dependency: 'onRangeChangeProp'. Either include it or remove the dependency array. If 'onRangeChangeProp' changes too often, find the parent component that defines it and wrap that definition in useCallback

useImperativeHandle(
ref,
Expand All @@ -333,7 +339,7 @@
scrollBy: scroller._scrollBy,
};
},
[]

Check warning on line 342 in src/react/Virtualizer.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useImperativeHandle has missing dependencies: 'scroller._scrollBy', 'scroller._scrollTo', 'scroller._scrollToIndex', and 'store'. Either include them or remove the dependency array
);

for (let i = overscanedRangeStart, j = overscanedRangeEnd; i <= j; i++) {
Expand Down
2 changes: 1 addition & 1 deletion src/solid/Virtualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
onScroll: _onScroll,
onScrollEnd: _onScrollEnd,
onRangeChange: _onRangeChange,
} = props;

Check warning on line 148 in src/solid/Virtualizer.tsx

View workflow job for this annotation

GitHub Actions / check

The reactive variable 'props' should be used within JSX, a tracked scope (like createEffect), or inside an event handler function, or else changes will be ignored

const store = createVirtualStore(
props.data.length,
Expand Down Expand Up @@ -225,7 +225,7 @@

const scrollable = containerRef!.parentElement!;
resizer._observeRoot(scrollable);
scroller._observe(scrollable);
scroller._observe(scrollable, containerRef!);

onCleanup(() => {
if (props.ref) {
Expand Down
3 changes: 2 additions & 1 deletion src/svelte/VList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,9 @@
);

onMount(() => {
const container = containerRef!;
const root = containerRef.parentElement!;
virtualizer[ON_MOUNT](root);
virtualizer[ON_MOUNT](root, container);
});
onDestroy(() => {
virtualizer[ON_UN_MOUNT]();
Expand Down
4 changes: 2 additions & 2 deletions src/svelte/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ export const createVirtualizer = (
);

return {
[ON_MOUNT]: (scrollable: HTMLElement) => {
[ON_MOUNT]: (scrollable: HTMLElement, container: HTMLElement) => {
resizer._observeRoot(scrollable);
scroller._observe(scrollable);
scroller._observe(scrollable, container);
},
[ON_UN_MOUNT]: () => {
unsubscribeStore();
Expand Down
Loading
Loading