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 LoadMore from './load-more';
import { Card, Spinner } from './ui'; import { Card, Spinner } from './ui';
type IScrollableListWindowScroll = { interface IScrollableListBase {
/** 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 {
/** Pagination callback when the end of the list is reached. */ /** Pagination callback when the end of the list is reached. */
onLoadMore?: () => void; onLoadMore?: () => void;
/** Whether the data is currently being fetched. */ /** Whether the data is currently being fetched. */
@ -42,8 +33,6 @@ interface IScrollableList {
placeholderComponent?: React.ComponentType | React.NamedExoticComponent; placeholderComponent?: React.ComponentType | React.NamedExoticComponent;
/** Number of placeholders to render while loading. */ /** Number of placeholders to render while loading. */
placeholderCount?: number; placeholderCount?: number;
/** Extra class names on the parent element. */
className?: string;
/** Extra class names on the list element. */ /** Extra class names on the list element. */
listClassName?: string; listClassName?: string;
/** Class names on each item container. */ /** Class names on each item container. */
@ -52,8 +41,6 @@ interface IScrollableList {
loadMoreClassName?: string; loadMoreClassName?: string;
/** `id` attribute on the parent element. */ /** `id` attribute on the parent element. */
id?: string; id?: string;
/** CSS styles on the parent element. */
style?: React.CSSProperties;
/** Initial item index to scroll to. */ /** Initial item index to scroll to. */
initialIndex?: number; initialIndex?: number;
/** Estimated size for items. */ /** Estimated size for items. */
@ -62,8 +49,20 @@ interface IScrollableList {
alignToBottom?: boolean; alignToBottom?: boolean;
} }
const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList & IScrollableListWindowScroll>(({ interface IScrollableListWithContainer extends IScrollableListBase {
prepend = null, /** 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, alwaysPrepend,
children, children,
isLoading, isLoading,
@ -72,7 +71,6 @@ const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList &
showLoading, showLoading,
onScroll, onScroll,
onLoadMore, onLoadMore,
className,
listClassName, listClassName,
itemClassName, itemClassName,
loadMoreClassName, loadMoreClassName,
@ -81,15 +79,12 @@ const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList &
placeholderComponent: Placeholder, placeholderComponent: Placeholder,
placeholderCount = 0, placeholderCount = 0,
initialIndex, initialIndex,
style = {},
estimatedSize = 300, estimatedSize = 300,
alignToBottom, alignToBottom,
...props ...props
}, ref) => { }, ref) => {
const { autoloadMore } = useSettings(); const { autoloadMore } = useSettings();
const parentRef = React.useRef<HTMLDivElement>(null);
/** Normalized children. */ /** Normalized children. */
const elements = Array.from(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 data = showPlaceholder ? Array(placeholderCount).fill('') : elements;
const virtualizer = props.useWindowScroll === false ? useVirtualizer({ const virtualizer = 'parentRef' in props ? useVirtualizer({
count: data.length + (hasMore ? 1 : 0), count: data.length + (hasMore ? 1 : 0),
overscan: 3, overscan: 3,
estimateSize: () => estimatedSize, estimateSize: () => estimatedSize,
getScrollElement: () => props.parentRef.current || parentRef.current, getScrollElement: () => props.parentRef.current,
}) : useWindowVirtualizer({ }) : useWindowVirtualizer({
count: data.length + (hasMore ? 1 : 0), count: data.length + (hasMore ? 1 : 0),
overscan: 3, overscan: 3,
@ -170,14 +165,9 @@ const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList &
const virtualItems = virtualizer.getVirtualItems(); const virtualItems = virtualizer.getVirtualItems();
return ( const body = (
<div
ref={parentRef}
id={id}
className={clsx(className, 'w-full')}
style={style}
>
<div <div
id={'parentRef' in props ? id : undefined}
className={listClassName} className={listClassName}
style={{ style={{
height: !showLoading && data.length ? virtualizer.getTotalSize() : undefined, height: !showLoading && data.length ? virtualizer.getTotalSize() : undefined,
@ -206,8 +196,24 @@ const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList &
</> </>
) : renderEmpty()} ) : renderEmpty()}
</div> </div>
);
if ('parentRef' in props) return body;
return (
<div
id={id}
className={clsx(props.className, 'w-full')}
style={props.style}
>
{body}
</div> </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 { FormattedMessage } from 'react-intl';
import LoadGap from 'pl-fe/components/load-gap'; 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 StatusContainer from 'pl-fe/containers/status-container';
import FeedSuggestions from 'pl-fe/features/feed-suggestions/feed-suggestions'; import FeedSuggestions from 'pl-fe/features/feed-suggestions/feed-suggestions';
import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status'; 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 { Stack, Text } from './ui';
import type { OrderedSet as ImmutableOrderedSet } from 'immutable'; 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. */ /** Unique key to preserve the scroll position when navigating back. */
scrollKey: string; scrollKey: string;
/** List of status IDs to display. */ /** List of status IDs to display. */

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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