layout: fix overlay scrollbar with virtualized list, closes #1524 (#1530)

This commit is contained in:
Huang Xin
2025-07-05 01:36:13 +08:00
committed by GitHub
parent 8a0216dcf8
commit a03f2c585f
3 changed files with 92 additions and 41 deletions
@@ -15,6 +15,7 @@ import { RxLineHeight } from 'react-icons/rx';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { eventDispatcher } from '@/utils/event';
@@ -45,6 +46,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
}) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { getConfig, setConfig } = useBookDataStore();
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
const { getView, getViewState, getProgress, getViewSettings } = useReaderStore();
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
@@ -55,6 +57,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
const marginIconSize = useResponsiveSize(20);
const view = getView(bookKey);
const config = getConfig(bookKey);
const progress = getProgress(bookKey);
const viewSettings = getViewSettings(bookKey);
const viewState = getViewState(bookKey);
@@ -131,15 +134,17 @@ const FooterBar: React.FC<FooterBarProps> = ({
handleSpeakText();
} else if (tab === 'toc') {
setHoveredBookKey('');
if (viewSettings) {
viewSettings.sideBarTab = 'toc';
if (config && config.viewSettings) {
config.viewSettings.sideBarTab = 'toc';
setConfig(bookKey, config);
}
setSideBarVisible(true);
} else if (tab === 'note') {
setHoveredBookKey('');
setSideBarVisible(true);
if (viewSettings) {
viewSettings.sideBarTab = 'annotations';
if (config && config.viewSettings) {
config.viewSettings.sideBarTab = 'annotations';
setConfig(bookKey, config);
}
}
};
@@ -51,12 +51,16 @@ const SidebarContent: React.FC<{
'font-sans text-base font-normal sm:text-sm',
)}
>
<OverlayScrollbarsComponent options={{ scrollbars: { autoHide: 'scroll' } }} defer>
<OverlayScrollbarsComponent
className='min-h-0 flex-1'
options={{ scrollbars: { autoHide: 'scroll' }, showNativeOverlaidScrollbars: false }}
defer
>
<div
className={clsx(
'scroll-container min-h-0 flex-1 transition-opacity duration-300 ease-in-out',
{ 'opacity-0': fade, 'opacity-100': !fade },
)}
className={clsx('scroll-container h-full transition-opacity duration-300 ease-in-out', {
'opacity-0': fade,
'opacity-100': !fade,
})}
>
{targetTab === 'toc' && bookDoc.toc && (
<TOCView toc={bookDoc.toc} bookKey={sideBarBookKey} />
@@ -1,7 +1,10 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FixedSizeList as VirtualList } from 'react-window';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import 'overlayscrollbars/overlayscrollbars.css';
import { TOCItem } from '@/libs/document';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { findParentPath } from '@/utils/toc';
@@ -32,6 +35,7 @@ const TOCView: React.FC<{
bookKey: string;
toc: TOCItem[];
}> = ({ bookKey, toc }) => {
const { appService } = useEnv();
const { getView, getProgress, getViewState, getViewSettings } = useReaderStore();
const { sideBarBookKey, isSideBarVisible } = useSidebarStore();
const viewSettings = getViewSettings(bookKey)!;
@@ -42,9 +46,41 @@ const TOCView: React.FC<{
const [containerHeight, setContainerHeight] = useState(400);
const containerRef = useRef<HTMLDivElement | null>(null);
const listOuterRef = useRef<HTMLDivElement | null>(null);
const vitualListRef = useRef<VirtualList | null>(null);
const staticListRef = useRef<HTMLDivElement | null>(null);
const [initialize] = useOverlayScrollbars({
defer: true,
options: {
scrollbars: {
autoHide: 'scroll',
},
showNativeOverlaidScrollbars: false,
},
events: {
initialized(osInstance) {
const { viewport } = osInstance.elements();
viewport.style.overflowX = `var(--os-viewport-overflow-x)`;
viewport.style.overflowY = `var(--os-viewport-overflow-y)`;
},
},
});
useEffect(() => {
const { current: root } = containerRef;
const { current: virtualOuter } = listOuterRef;
if (root && virtualOuter) {
initialize({
target: root,
elements: {
viewport: virtualOuter,
},
});
}
}, [initialize]);
useTextTranslation(bookKey, containerRef.current, false, 'translation-target-toc');
useEffect(() => {
@@ -78,8 +114,11 @@ const TOCView: React.FC<{
};
}, []);
const activeHref = useMemo(() => progress?.sectionHref || null, [progress]);
const activeHref = useMemo(() => progress?.sectionHref || null, [progress?.sectionHref]);
const flatItems = useFlattenedTOC(toc, expandedItems);
const activeItemIndex = useMemo(() => {
return flatItems.findIndex((item) => item.item.href === activeHref);
}, [flatItems, activeHref]);
const handleToggleExpand = useCallback((item: TOCItem) => {
const href = item.href || '';
@@ -136,7 +175,7 @@ const TOCView: React.FC<{
const virtualItemSize = useMemo(() => {
return window.innerWidth >= 640 && !viewSettings?.translationEnabled ? 37 : 57;
}, [viewSettings]);
}, [viewSettings?.translationEnabled]);
const virtualListData = useMemo(
() => ({
@@ -163,40 +202,43 @@ const TOCView: React.FC<{
useEffect(() => {
if (flatItems.length > 0) {
setTimeout(scrollToActiveItem, 0);
setTimeout(scrollToActiveItem, appService?.isAndroidApp ? 300 : 0);
}
}, [flatItems, scrollToActiveItem]);
return (
<div className='rounded' ref={containerRef}>
{flatItems.length > 256 ? (
<VirtualList
ref={vitualListRef}
width='100%'
height={containerHeight}
itemCount={flatItems.length}
itemSize={virtualItemSize}
itemData={virtualListData}
overscanCount={20}
>
{VirtualListRow}
</VirtualList>
) : (
<div className='pt-2' ref={staticListRef}>
{flatItems.map((flatItem, index) => (
<StaticListRow
key={`static-row-${index}`}
bookKey={bookKey}
flatItem={flatItem}
activeHref={activeHref}
onToggleExpand={handleToggleExpand}
onItemClick={handleItemClick}
/>
))}
</div>
)}
return flatItems.length > 256 ? (
<div className='rounded' data-overlayscrollbars-initialize='' ref={containerRef}>
<VirtualList
ref={vitualListRef}
outerRef={listOuterRef}
width='100%'
height={containerHeight}
itemCount={flatItems.length}
itemSize={virtualItemSize}
itemData={virtualListData}
overscanCount={20}
initialScrollOffset={
appService?.isAndroidApp && activeItemIndex >= 0
? Math.max(0, activeItemIndex * virtualItemSize - containerHeight / 2)
: undefined
}
>
{VirtualListRow}
</VirtualList>
</div>
) : (
<div className='rounded pt-2' ref={staticListRef}>
{flatItems.map((flatItem, index) => (
<StaticListRow
key={`static-row-${index}`}
bookKey={bookKey}
flatItem={flatItem}
activeHref={activeHref}
onToggleExpand={handleToggleExpand}
onItemClick={handleItemClick}
/>
))}
</div>
);
};
export default TOCView;