Skip to content
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 { ViewportComponentAttributes } from "./types";
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 @@ export const VGrid = forwardRef<VGridHandle, VGridProps>(
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 @@ export const VGrid = forwardRef<VGridHandle, VGridProps>(
}
);
resizer._observeRoot(root);
scroller._observe(root);
scroller._observe(root, container);
return () => {
unsubscribeVStore();
unsubscribeHStore();
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 { useStatic } from "./useStatic";
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 @@ export interface VirtualizerProps {
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 @@ export const Virtualizer = forwardRef<VirtualizerHandle, VirtualizerProps>(
shift,
horizontal: horizontalProp,
cache,
startMargin,
startOffset,
ssrCount,
as: Element = "div",
item: ItemElement = "div",
Expand Down Expand Up @@ -207,13 +213,12 @@ export const Virtualizer = forwardRef<VirtualizerHandle, VirtualizerProps>(
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 @@ export const Virtualizer = forwardRef<VirtualizerHandle, VirtualizerProps>(
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 Down
2 changes: 1 addition & 1 deletion src/solid/Virtualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export const Virtualizer = <T,>(props: VirtualizerProps<T>): JSX.Element => {

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