pl-fe: further actually improve virtual scrolling behavior
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
ea3c70f37c
commit
93e0311984
13 changed files with 74 additions and 79 deletions
|
@ -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,
|
||||||
|
};
|
||||||
|
|
|
@ -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. */
|
||||||
|
|
|
@ -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'
|
||||||
>
|
>
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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))}
|
||||||
|
|
|
@ -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) => (
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 =>
|
||||||
|
|
|
@ -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 =>
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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 =>
|
||||||
|
|
|
@ -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) =>
|
||||||
|
|
|
@ -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) =>
|
||||||
|
|
Loading…
Reference in a new issue