diff --git a/src/core/store.ts b/src/core/store.ts index af183c377..d964557e9 100644 --- a/src/core/store.ts +++ b/src/core/store.ts @@ -13,6 +13,7 @@ import { import { isIOSWebKit } from "./environment"; import type { CacheSnapshot, + InitialScrollProp, InternalCacheSnapshot, ItemResize, ItemsRange, @@ -141,6 +142,7 @@ export const createVirtualStore = ( ssrCount: number = 0, cacheSnapshot?: CacheSnapshot | undefined, shouldAutoEstimateItemSize: boolean = false, + initialScroll?: InitialScrollProp | undefined, startSpacerSize: number = 0 ): VirtualStore => { let isSSR = !!ssrCount; @@ -189,6 +191,11 @@ export const createVirtualStore = ( } }; + if (initialScroll) { + scrollOffset = + initialScroll.offset ?? getItemOffset(initialScroll.index || 0); + } + return { _getStateVersion() { return stateVersion; diff --git a/src/core/types.ts b/src/core/types.ts index 956c2434e..cb210766d 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -42,3 +42,11 @@ export interface ScrollToIndexOpts { */ offset?: number; } + +export interface InitialScrollProp extends ScrollToIndexOpts { + /** + * Index of item. + * @defaultValue 0 + */ + index?: number; +} diff --git a/src/index.ts b/src/index.ts index 7610dca53..751d88dd2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,5 +5,6 @@ export type { CacheSnapshot, ScrollToIndexAlign, ScrollToIndexOpts, + InitialScrollProp, } from "./core/types"; export * from "./react"; diff --git a/src/react/VList.tsx b/src/react/VList.tsx index 2ceb80a6d..44a69f0e0 100644 --- a/src/react/VList.tsx +++ b/src/react/VList.tsx @@ -23,6 +23,7 @@ export interface VListProps | "itemSize" | "shift" | "horizontal" + | "initialScroll" | "cache" | "ssrCount" | "item" @@ -52,6 +53,7 @@ export const VList = forwardRef( shift, horizontal, reverse, + initialScroll, cache, ssrCount, item, @@ -76,6 +78,7 @@ export const VList = forwardRef( itemSize={itemSize} shift={shift} horizontal={horizontal} + initialScroll={initialScroll} cache={cache} ssrCount={ssrCount} item={item} diff --git a/src/react/Virtualizer.tsx b/src/react/Virtualizer.tsx index 4ff19f6d2..a4003167d 100644 --- a/src/react/Virtualizer.tsx +++ b/src/react/Virtualizer.tsx @@ -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, + InitialScrollProp, + ScrollToIndexOpts, +} from "../core/types"; import { flushSync } from "react-dom"; import { useRerender } from "./useRerender"; import { useChildren } from "./useChildren"; @@ -112,6 +116,10 @@ export interface VirtualizerProps { * If true, rendered as a horizontally scrollable list. Otherwise rendered as a vertically scrollable list. */ horizontal?: boolean; + /** + * TODO + */ + initialScroll?: InitialScrollProp; /** * You can restore cache by passing a {@link CacheSnapshot} on mount. This is useful when you want to restore scroll position after navigation. The snapshot can be obtained from {@link VirtualizerHandle.cache}. * @@ -177,6 +185,7 @@ export const Virtualizer = forwardRef( itemSize, shift, horizontal: horizontalProp, + initialScroll, cache, startMargin, ssrCount, @@ -208,6 +217,7 @@ export const Virtualizer = forwardRef( ssrCount, cache, !itemSize, + initialScroll, startMargin ); return [ @@ -284,6 +294,9 @@ export const Virtualizer = forwardRef( const assignScrollableElement = (e: HTMLElement) => { resizer._observeRoot(e); scroller._observe(e); + if (initialScroll) { + scroller._scrollToIndex(initialScroll.index || 0, initialScroll); + } }; if (scrollRef) { // parent's ref doesn't exist when useLayoutEffect is called diff --git a/src/vue/Virtualizer.tsx b/src/vue/Virtualizer.tsx index 280490e39..e70ed311c 100644 --- a/src/vue/Virtualizer.tsx +++ b/src/vue/Virtualizer.tsx @@ -118,6 +118,7 @@ export const Virtualizer = /*#__PURE__*/ defineComponent({ props.ssrCount, undefined, !props.itemSize, + undefined, props.startMargin ); const resizer = createResizer(store, isHorizontal); diff --git a/stories/react/advanced/Keep offscreen items.stories.tsx b/stories/react/advanced/Keep offscreen items.stories.tsx index 89b3ee879..8b66e8fb2 100644 --- a/stories/react/advanced/Keep offscreen items.stories.tsx +++ b/stories/react/advanced/Keep offscreen items.stories.tsx @@ -152,12 +152,6 @@ export const SelectedIndex: StoryObj = { isPrepend.current = false; }); - useEffect(() => { - if (!ref.current) return; - - ref.current.scrollToIndex(items.length - 1, { align: "end" }); - }, []); - const toggleEditing = useCallback((itemId: number) => { setEditingItemId((currentValue) => itemId === currentValue ? null : itemId @@ -185,6 +179,7 @@ export const SelectedIndex: StoryObj = { ref={ref} style={{ flex: 1 }} reverse + initialScroll={{ index: items.length - 1, align: "end" }} keepMounted={ editingItemId ? [items.findIndex((item) => item.id === editingItemId)] diff --git a/stories/react/basics/VList.stories.tsx b/stories/react/basics/VList.stories.tsx index 6f13ace20..ce8c41c4d 100644 --- a/stories/react/basics/VList.stories.tsx +++ b/stories/react/basics/VList.stories.tsx @@ -106,12 +106,8 @@ export const PaddingAndMargin: StoryObj = { export const Reverse: StoryObj = { render: () => { - const ref = useRef(null); - useEffect(() => { - ref.current?.scrollToIndex(999); - }, []); return ( - + {createRows(1000)} ); @@ -402,10 +398,6 @@ const RestorableList = ({ id }: { id: string }) => { if (!ref.current) return; const handle = ref.current; - if (offset) { - handle.scrollTo(offset); - } - return () => { sessionStorage.setItem( cacheKey, @@ -415,7 +407,12 @@ const RestorableList = ({ id }: { id: string }) => { }, []); return ( - + {createRows(1000)} );