From a85e5584c83a4a337aadf6d59a66ca48e5219435 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Mon, 17 Feb 2025 20:19:28 +0100 Subject: [PATCH] Preserve scroll position continuous pager on previous chapter load In the horizontal pager loading the previous chapter caused the scroll position to get lost and instead of starting at the end of the previous chapter, the scroll position was at the very start. This happened everytime no matter if the scroll position was at the x 0 or not before the previous chapter got visible. Same issue can be observed in the vertical pager in case the scroll position is at y 0 before the previous chapter has been rendered. --- .../reader/components/viewer/ReaderViewer.tsx | 31 +++--- .../hooks/useReaderPreserveScrollPosition.ts | 94 +++++++++++++++++++ src/modules/reader/utils/Reader.utils.ts | 22 +++++ 3 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 src/modules/reader/hooks/useReaderPreserveScrollPosition.ts diff --git a/src/modules/reader/components/viewer/ReaderViewer.tsx b/src/modules/reader/components/viewer/ReaderViewer.tsx index be6f1f15..1b94ce06 100644 --- a/src/modules/reader/components/viewer/ReaderViewer.tsx +++ b/src/modules/reader/components/viewer/ReaderViewer.tsx @@ -59,12 +59,14 @@ import { useReaderHandlePageSelection } from '@/modules/reader/hooks/useReaderHa import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; import { ReaderChapterViewer } from '@/modules/reader/components/viewer/ReaderChapterViewer.tsx'; import { + getPreviousNextChapterVisibility, getReaderChapterViewerCurrentPageIndex, getReaderChapterViewResumeMode, } from '@/modules/reader/utils/Reader.utils.ts'; import { coerceIn, noOp } from '@/lib/HelperFunctions.ts'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts'; +import { useReaderPreserveScrollPosition } from '@/modules/reader/hooks/useReaderPreserveScrollPosition.ts'; const READING_MODE_TO_IN_VIEWPORT_TYPE: Record = { [ReadingMode.SINGLE_PAGE]: PageInViewportType.X, @@ -254,6 +256,15 @@ const BaseReaderViewer = forwardRef( scrollElementRef, ); useReaderAutoScroll(isOverlayVisible, automaticScrolling); + useReaderPreserveScrollPosition( + scrollElementRef, + currentChapter?.id, + currentChapterIndex, + chaptersToRender, + visibleChapters, + isContinuousReadingModeActive, + readingDirection, + ); useLayoutEffect(() => { chapterViewerSize.current = { minChapterViewWidth: 0, minChapterViewHeight: 0 }; @@ -321,17 +332,11 @@ const BaseReaderViewer = forwardRef( (isLastLeadingChapter && visibleChapters.isLeadingChapterPreloadMode) || (isLastTrailingChapter && visibleChapters.isTrailingChapterPreloadMode); - const isPreviousChapterLoaded = !!chaptersToRender[chapterIndex + 1]; - const isPreviousChapterLastLeadingChapter = chapterIndex + 1 >= chaptersToRender.length - 1; - const isPreviousChapterPreloading = - isPreviousChapterLastLeadingChapter && visibleChapters.isLeadingChapterPreloadMode; - const isPreviousChapterVisible = isPreviousChapterLoaded && !isPreviousChapterPreloading; - - const isNextChapterLoaded = !!chaptersToRender[chapterIndex - 1]; - const isNextChapterLastTrailingChapter = chapterIndex - 1 < 0; - const isNextChapterPreloading = - isNextChapterLastTrailingChapter && visibleChapters.isTrailingChapterPreloadMode; - const isNextChapterVisible = isNextChapterLoaded && !isNextChapterPreloading; + const previousNextChapterVisibility = getPreviousNextChapterVisibility( + chapterIndex, + chaptersToRender, + visibleChapters, + ); return ( , + currentChapterId: ChapterIdInfo['id'] | undefined, + chapterIndex: number, + chaptersToRender: TChapterReader[], + visibleChapters: ReaderStateChapters['visibleChapters'], + isContinuousReadingModeActive: boolean, + readingDirection: ReadingDirection, +) => { + const scrollPosition = useRef({ left: 0, top: 0, scrollWidth: 0, scrollHeight: 0 }); + + useEffect(() => { + const element = scrollElementRef.current; + + if (!element) { + return () => {}; + } + + const onScroll = () => { + scrollPosition.current = { + ...scrollPosition.current, + left: element.scrollLeft, + top: element.scrollTop, + }; + }; + + element.addEventListener('scroll', onScroll); + + return () => element.removeEventListener('scroll', onScroll); + }, []); + + useLayoutEffect(() => { + const scrollElement = scrollElementRef.current; + const { left, top, scrollWidth, scrollHeight } = scrollPosition.current; + + if (!scrollElement || !isContinuousReadingModeActive) { + return; + } + + scrollPosition.current = { + ...scrollPosition.current, + scrollWidth: scrollElement.scrollWidth, + scrollHeight: scrollElement.scrollHeight, + }; + + const themeDirectionForReadingDirection = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection]; + + const previousNextChapterVisibility = getPreviousNextChapterVisibility( + chapterIndex, + chaptersToRender, + visibleChapters, + ); + + const wasScrolledBackwardHorizontal = Math.abs(left) < window.innerWidth * 1.5; + const wasScrolledBackwardVertical = top < window.innerHeight * 1.5; + + const widthOfPrependedContent = scrollElement.scrollWidth - scrollWidth; + const heightOfPrependedContent = scrollElement.scrollHeight - scrollHeight; + + const newLeft = wasScrolledBackwardHorizontal + ? getOptionForDirection( + widthOfPrependedContent, + -widthOfPrependedContent, + themeDirectionForReadingDirection, + ) + left + : left; + const newTop = wasScrolledBackwardVertical ? heightOfPrependedContent + top : top; + + // only relevant when prepending content to the dom due to the resulting layout shift + const isFirstRenderOfPreviousChapter = + !previousNextChapterVisibility.previous && (wasScrolledBackwardHorizontal || wasScrolledBackwardVertical); + if (!isFirstRenderOfPreviousChapter) { + return; + } + + scrollElement.scrollTo(newLeft, newTop); + }, [currentChapterId]); +}; diff --git a/src/modules/reader/utils/Reader.utils.ts b/src/modules/reader/utils/Reader.utils.ts index f20dcf07..2a282df3 100644 --- a/src/modules/reader/utils/Reader.utils.ts +++ b/src/modules/reader/utils/Reader.utils.ts @@ -321,3 +321,25 @@ export const getReaderChapterViewResumeMode = ( return ReaderResumeMode.START; }; + +export const getPreviousNextChapterVisibility = ( + chapterIndex: number, + chaptersToRender: TChapterReader[], + visibleChapters: ReaderStateChapters['visibleChapters'], +): { previous: boolean; next: boolean } => { + const isPreviousChapterLoaded = !!chaptersToRender[chapterIndex + 1]; + const isPreviousChapterLastLeadingChapter = chapterIndex + 1 >= chaptersToRender.length - 1; + const isPreviousChapterPreloading = + isPreviousChapterLastLeadingChapter && visibleChapters.isLeadingChapterPreloadMode; + const isPreviousChapterVisible = isPreviousChapterLoaded && !isPreviousChapterPreloading; + + const isNextChapterLoaded = !!chaptersToRender[chapterIndex - 1]; + const isNextChapterLastTrailingChapter = chapterIndex - 1 < 0; + const isNextChapterPreloading = isNextChapterLastTrailingChapter && visibleChapters.isTrailingChapterPreloadMode; + const isNextChapterVisible = isNextChapterLoaded && !isNextChapterPreloading; + + return { + previous: isPreviousChapterVisible, + next: isNextChapterVisible, + }; +};