From 3d605913e8dc7226c02a61aad79d4f9ba49a2a11 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 Jun 2022 18:47:07 -0500 Subject: [PATCH] Preserve scroll position perfectly --- app/soapbox/components/scrollable_list.tsx | 38 +++++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/app/soapbox/components/scrollable_list.tsx b/app/soapbox/components/scrollable_list.tsx index 5fd1843c4..18dd1e987 100644 --- a/app/soapbox/components/scrollable_list.tsx +++ b/app/soapbox/components/scrollable_list.tsx @@ -12,6 +12,11 @@ type Context = { listClassName?: string, } +type SavedScrollPosition = { + index: number, + offset: number, +} + // NOTE: It's crucial to space lists with **padding** instead of margin! // Pass an `itemClassName` like `pb-3`, NOT a `space-y-3` className // https://virtuoso.dev/troubleshooting#list-does-not-scroll-to-the-bottom--items-jump-around @@ -72,10 +77,11 @@ const ScrollableList = React.forwardRef(({ const settings = useSettings(); const autoloadMore = settings.get('autoloadMore'); - // Preserve scroll index - const scrollIndexKey = `soapbox:scrollIndex:${location.pathname}`; - const scrollIndex = Number(sessionStorage.getItem(scrollIndexKey)); - const initialIndex = useRef(scrollIndex); + // Preserve scroll position + const scrollDataKey = `soapbox:scrollData:${location.pathname}`; + const scrollData: SavedScrollPosition | null = JSON.parse(sessionStorage.getItem(scrollDataKey)!); + const topIndex = useRef(scrollData ? scrollData.index : 0); + const topOffset = useRef(scrollData ? scrollData.offset : 0); /** Normalized children */ const elements = Array.from(children || []); @@ -95,8 +101,22 @@ const ScrollableList = React.forwardRef(({ data.push(); } + const handleScroll = () => { + const node = document.querySelector(`[data-virtuoso-scroller] [data-item-index="${topIndex.current}"]`); + if (node) { + topOffset.current = node.getBoundingClientRect().top * -1; + } + }; + useEffect(() => { - sessionStorage.removeItem(scrollIndexKey); + document.addEventListener('scroll', handleScroll); + sessionStorage.removeItem(scrollDataKey); + + return () => { + const data = { index: topIndex.current, offset: topOffset.current }; + sessionStorage.setItem(scrollDataKey, JSON.stringify(data)); + document.removeEventListener('scroll', handleScroll); + }; }, []); /* Render an empty state instead of the scrollable list */ @@ -139,8 +159,8 @@ const ScrollableList = React.forwardRef(({ } }; - const handleRangeChanged = (range: ListRange) => { - sessionStorage.setItem(scrollIndexKey, String(range.startIndex)); + const handleRangeChange = (range: ListRange) => { + topIndex.current = range.startIndex; }; /** Render the actual Virtuoso list */ @@ -155,8 +175,8 @@ const ScrollableList = React.forwardRef(({ endReached={handleEndReached} isScrolling={isScrolling => isScrolling && onScroll && onScroll()} itemContent={renderItem} - initialTopMostItemIndex={showLoading ? 0 : initialTopMostItemIndex || initialIndex.current} - rangeChanged={handleRangeChanged} + initialTopMostItemIndex={showLoading ? 0 : initialTopMostItemIndex || (scrollData ? { align: 'start', index: scrollData.index, offset: scrollData.offset } : 0)} + rangeChanged={handleRangeChange} style={style} context={{ listClassName: className,