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 { 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,
|
||||
};
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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'
|
||||
>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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))}
|
||||
|
|
|
@ -21,7 +21,6 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
|
|||
<ScrollableList
|
||||
isLoading={isFetching}
|
||||
style={{ height: 320 }}
|
||||
useWindowScroll={false}
|
||||
parentRef={parentRef}
|
||||
>
|
||||
{data.map((suggestion) => (
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -54,7 +54,6 @@ const FavouritesModal: React.FC<BaseModalProps & FavouritesModalProps> = ({ onCl
|
|||
onLoadMore={handleLoadMore}
|
||||
hasMore={!!next}
|
||||
estimatedSize={42}
|
||||
useWindowScroll={false}
|
||||
parentRef={modalRef}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
|
|
|
@ -337,8 +337,8 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
|||
<Thread
|
||||
status={status}
|
||||
withMedia={false}
|
||||
useWindowScroll={false}
|
||||
itemClassName='px-4'
|
||||
isModal
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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) =>
|
||||
|
|
Loading…
Reference in a new issue