pl-fe: further actually improve virtual scrolling behavior

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-10-04 00:15:31 +02:00
parent ea3c70f37c
commit 93e0311984
13 changed files with 74 additions and 79 deletions

View file

@ -8,16 +8,7 @@ import { useSettings } from 'pl-fe/hooks';
import LoadMore from './load-more';
import { Card, Spinner } from './ui';
type IScrollableListWindowScroll = {
/** Whether to use the window to scroll the content instead of the container. */
useWindowScroll?: true;
} | {
/** Whether to use the window to scroll the content instead of the container. */
useWindowScroll: false;
parentRef: React.RefObject<HTMLElement>;
};
interface IScrollableList {
interface IScrollableListBase {
/** Pagination callback when the end of the list is reached. */
onLoadMore?: () => void;
/** Whether the data is currently being fetched. */
@ -42,8 +33,6 @@ interface IScrollableList {
placeholderComponent?: React.ComponentType | React.NamedExoticComponent;
/** Number of placeholders to render while loading. */
placeholderCount?: number;
/** Extra class names on the parent element. */
className?: string;
/** Extra class names on the list element. */
listClassName?: string;
/** Class names on each item container. */
@ -52,8 +41,6 @@ interface IScrollableList {
loadMoreClassName?: string;
/** `id` attribute on the parent element. */
id?: string;
/** CSS styles on the parent element. */
style?: React.CSSProperties;
/** Initial item index to scroll to. */
initialIndex?: number;
/** Estimated size for items. */
@ -62,8 +49,20 @@ interface IScrollableList {
alignToBottom?: boolean;
}
const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList & IScrollableListWindowScroll>(({
prepend = null,
interface IScrollableListWithContainer extends IScrollableListBase {
/** Extra class names on the container element. */
className?: string;
/** CSS styles on the container element. */
style?: React.CSSProperties;
}
interface IScrollableListWithoutContainer extends IScrollableListBase {
parentRef: React.RefObject<HTMLElement>;
}
type IScrollableList = IScrollableListWithContainer | IScrollableListWithoutContainer;
const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList>(({ prepend = null,
alwaysPrepend,
children,
isLoading,
@ -72,7 +71,6 @@ const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList &
showLoading,
onScroll,
onLoadMore,
className,
listClassName,
itemClassName,
loadMoreClassName,
@ -81,15 +79,12 @@ const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList &
placeholderComponent: Placeholder,
placeholderCount = 0,
initialIndex,
style = {},
estimatedSize = 300,
alignToBottom,
...props
}, ref) => {
const { autoloadMore } = useSettings();
const parentRef = React.useRef<HTMLDivElement>(null);
/** Normalized children. */
const elements = Array.from(children || []);
@ -97,11 +92,11 @@ const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList &
const data = showPlaceholder ? Array(placeholderCount).fill('') : elements;
const virtualizer = props.useWindowScroll === false ? useVirtualizer({
const virtualizer = 'parentRef' in props ? useVirtualizer({
count: data.length + (hasMore ? 1 : 0),
overscan: 3,
estimateSize: () => estimatedSize,
getScrollElement: () => props.parentRef.current || parentRef.current,
getScrollElement: () => props.parentRef.current,
}) : useWindowVirtualizer({
count: data.length + (hasMore ? 1 : 0),
overscan: 3,
@ -170,44 +165,55 @@ const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList &
const virtualItems = virtualizer.getVirtualItems();
const body = (
<div
id={'parentRef' in props ? id : undefined}
className={listClassName}
style={{
height: !showLoading && data.length ? virtualizer.getTotalSize() : undefined,
width: '100%',
position: 'relative',
}}
>
{(!showLoading || showPlaceholder) && data.length ? (
<>
{prepend}
{virtualItems.map((item) => (
<div
className={item.index === data.length ? '' : itemClassName}
key={item.key as number}
data-index={item.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
width: '100%',
transform: `translateY(${item.start - virtualizer.options.scrollMargin}px)`,
}}
>
{renderItem(item.index)}
</div>
))}
</>
) : renderEmpty()}
</div>
);
if ('parentRef' in props) return body;
return (
<div
ref={parentRef}
id={id}
className={clsx(className, 'w-full')}
style={style}
className={clsx(props.className, 'w-full')}
style={props.style}
>
<div
className={listClassName}
style={{
height: !showLoading && data.length ? virtualizer.getTotalSize() : undefined,
width: '100%',
position: 'relative',
}}
>
{(!showLoading || showPlaceholder) && data.length ? (
<>
{prepend}
{virtualItems.map((item) => (
<div
className={item.index === data.length ? '' : itemClassName}
key={item.key as number}
data-index={item.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
width: '100%',
transform: `translateY(${item.start - virtualizer.options.scrollMargin}px)`,
}}
>
{renderItem(item.index)}
</div>
))}
</>
) : renderEmpty()}
</div>
{body}
</div>
);
});
export { type IScrollableList, ScrollableList as default };
export {
type IScrollableList,
type IScrollableListWithContainer,
type IScrollableListWithoutContainer,
ScrollableList as default,
};

View file

@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import LoadGap from 'pl-fe/components/load-gap';
import ScrollableList from 'pl-fe/components/scrollable-list';
import ScrollableList, { type IScrollableListWithContainer } from 'pl-fe/components/scrollable-list';
import StatusContainer from 'pl-fe/containers/status-container';
import FeedSuggestions from 'pl-fe/features/feed-suggestions/feed-suggestions';
import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status';
@ -14,9 +14,8 @@ import { usePlFeConfig } from 'pl-fe/hooks';
import { Stack, Text } from './ui';
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
import type { IScrollableList } from 'pl-fe/components/scrollable-list';
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
interface IStatusList extends Omit<IScrollableListWithContainer, 'onLoadMore' | 'children'> {
/** Unique key to preserve the scroll position when navigating back. */
scrollKey: string;
/** List of status IDs to display. */

View file

@ -59,7 +59,6 @@ const ChatList: React.FC<IChatList> = ({ onClickChat, parentRef, topOffset }) =>
hasMore={hasNextPage}
onLoadMore={handleLoadMore}
estimatedSize={64}
useWindowScroll={false}
parentRef={parentRef}
loadMoreClassName='mx-4 mb-4'
>

View file

@ -196,7 +196,6 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
isLoading={isFetching}
showLoading={isFetching && !isFetchingNextPage}
onLoadMore={handleStartReached}
useWindowScroll={false}
parentRef={parentRef}
>
{cachedChatMessages.map((chatMessage, index) => {

View file

@ -61,7 +61,6 @@ const Results = ({ accountSearchResult, onSelect, parentRef }: IResults) => {
isLoading={isFetching}
hasMore={hasNextPage}
onLoadMore={handleLoadMore}
useWindowScroll={false}
parentRef={parentRef}
>
{(accounts || []).map((chat) => renderAccount(chat))}

View file

@ -21,7 +21,6 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
<ScrollableList
isLoading={isFetching}
style={{ height: 320 }}
useWindowScroll={false}
parentRef={parentRef}
>
{data.map((suggestion) => (

View file

@ -78,14 +78,14 @@ const getDescendantsIds = createSelector([
interface IThread {
status: SelectedStatus;
withMedia?: boolean;
useWindowScroll?: boolean;
isModal?: boolean;
itemClassName?: string;
}
const Thread: React.FC<IThread> = ({
itemClassName,
status,
useWindowScroll = true,
isModal,
withMedia = true,
}) => {
const dispatch = useAppDispatch();
@ -114,7 +114,7 @@ const Thread: React.FC<IThread> = ({
});
let initialIndex = ancestorsIds.size;
if (!useWindowScroll && initialIndex !== 0) initialIndex = ancestorsIds.size + 1;
if (isModal && initialIndex !== 0) initialIndex = ancestorsIds.size + 1;
const node = useRef<HTMLDivElement>(null);
const statusRef = useRef<HTMLDivElement>(null);
@ -234,7 +234,7 @@ const Thread: React.FC<IThread> = ({
};
const _selectChild = (index: number) => {
if (!useWindowScroll) index = index + 1;
if (isModal) index = index + 1;
const selector = `[data-index="${index}"] .focusable`;
const element = node.current?.querySelector<HTMLDivElement>(selector);
@ -341,7 +341,7 @@ const Thread: React.FC<IThread> = ({
<StatusActionBar
status={status}
expandable={!useWindowScroll}
expandable={isModal}
space='lg'
withLabels
/>
@ -356,7 +356,7 @@ const Thread: React.FC<IThread> = ({
const children: JSX.Element[] = [];
if (!useWindowScroll) {
if (isModal) {
// Add padding to the top of the Thread (for Media Modal)
children.push(<div key='padding' className='h-4' />);
}
@ -376,8 +376,8 @@ const Thread: React.FC<IThread> = ({
space={2}
className={
clsx({
'h-full': !useWindowScroll,
'mt-2': useWindowScroll,
'h-full': isModal,
'mt-2': !isModal,
})
}
>
@ -391,7 +391,7 @@ const Thread: React.FC<IThread> = ({
ref={node}
className={
clsx('bg-white black:bg-black dark:bg-primary-900', {
'h-full overflow-auto': !useWindowScroll,
'h-full overflow-auto': isModal,
})
}
>
@ -403,10 +403,9 @@ const Thread: React.FC<IThread> = ({
itemClassName={itemClassName}
listClassName={
clsx({
'h-full': !useWindowScroll,
'h-full': isModal,
})
}
useWindowScroll={useWindowScroll}
parentRef={node}
>
{children}

View file

@ -38,7 +38,6 @@ const FamiliarFollowersModal: React.FC<BaseModalProps & FamiliarFollowersModalPr
itemClassName='pb-3'
style={{ height: 'calc(80vh - 88px)' }}
estimatedSize={42}
useWindowScroll={false}
parentRef={modalRef}
>
{familiarFollowerIds.map(id =>

View file

@ -54,7 +54,6 @@ const FavouritesModal: React.FC<BaseModalProps & FavouritesModalProps> = ({ onCl
onLoadMore={handleLoadMore}
hasMore={!!next}
estimatedSize={42}
useWindowScroll={false}
parentRef={modalRef}
>
{accountIds.map(id =>

View file

@ -337,8 +337,8 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
<Thread
status={status}
withMedia={false}
useWindowScroll={false}
itemClassName='px-4'
isModal
/>
</div>
)}

View file

@ -46,7 +46,6 @@ const MentionsModal: React.FC<BaseModalProps & MentionsModalProps> = ({ onClose,
listClassName='max-w-full'
itemClassName='pb-3'
estimatedSize={42}
useWindowScroll={false}
parentRef={modalRef}
>
{accountIds.map(id =>

View file

@ -95,7 +95,6 @@ const ReactionsModal: React.FC<BaseModalProps & ReactionsModalProps> = ({ onClos
itemClassName='pb-3'
style={{ height: 'calc(80vh - 88px)' }}
estimatedSize={42}
useWindowScroll={false}
parentRef={modalRef}
>
{accounts.map((account) =>

View file

@ -56,7 +56,6 @@ const ReblogsModal: React.FC<BaseModalProps & ReblogsModalProps> = ({ onClose, s
onLoadMore={handleLoadMore}
hasMore={!!next}
estimatedSize={42}
useWindowScroll={false}
parentRef={modalRef}
>
{accountIds.map((id) =>