Break ScrollableList component into parts (#38059)

This commit is contained in:
diondiondion authored and GitHub committed 2026-03-04 16:18:05 +00:00
1 parent 3fbb7424fa
commit a70079968c
11 files changed
+219 -93

No files matched your search

@@ -0,0 +1,85 @@
import type { ComponentPropsWithoutRef } from 'react';
import { Children, forwardRef } from 'react';
import classNames from 'classnames';
import { LoadingIndicator } from '../loading_indicator';
export const Scrollable = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<'div'> & {
flex?: boolean;
fullscreen?: boolean;
}
>(({ flex = true, fullscreen, className, children, ...otherProps }, ref) => {
return (
<div
className={classNames(
'scrollable',
{ 'scrollable--flex': flex, fullscreen },
className,
)}
ref={ref}
{...otherProps}
>
{children}
</div>
);
});
Scrollable.displayName = 'Scrollable';
export const ItemList = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<'div'> & {
isLoading?: boolean;
emptyMessage?: React.ReactNode;
}
>(({ isLoading, emptyMessage, className, children, ...otherProps }, ref) => {
if (Children.count(children) === 0 && emptyMessage) {
return <div className='empty-column-indicator'>{emptyMessage}</div>;
}
return (
<>
<div
role='feed'
className={classNames('item-list', className)}
ref={ref}
{...otherProps}
>
{!isLoading && children}
</div>
{isLoading && (
<div className='scrollable__append'>
<LoadingIndicator />
</div>
)}
</>
);
});
ItemList.displayName = 'ItemList';
export const Article = forwardRef<
HTMLElement,
ComponentPropsWithoutRef<'article'> & {
focusable?: boolean;
'data-id'?: string;
'aria-posinset': number;
'aria-setsize': number;
}
>(({ focusable, className, children, ...otherProps }, ref) => {
return (
<article
ref={ref}
className={classNames(className, { focusable })}
tabIndex={-1}
{...otherProps}
>
{children}
</article>
);
});
Article.displayName = 'Article';
@@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import { Children, cloneElement, PureComponent } from 'react';
import classNames from 'classnames';
import { useLocation } from 'react-router-dom';
import { List as ImmutableList } from 'immutable';
@@ -12,13 +11,14 @@ import { throttle } from 'lodash';
import { ScrollContainer } from 'mastodon/containers/scroll_container';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import IntersectionObserverArticleContainer from '../../containers/intersection_observer_article_container';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../../features/ui/util/fullscreen';
import IntersectionObserverWrapper from '../../features/ui/util/intersection_observer_wrapper';
import { LoadMore } from './load_more';
import { LoadPending } from './load_pending';
import { LoadingIndicator } from './loading_indicator';
import { LoadMore } from '../load_more';
import { LoadPending } from '../load_pending';
import { LoadingIndicator } from '../loading_indicator';
import { Scrollable, ItemList } from './components';
const MOUSE_IDLE_DELAY = 300;
@@ -336,24 +336,20 @@ class ScrollableList extends PureComponent {
if (showLoading) {
scrollableArea = (
<div className='scrollable scrollable--flex' ref={this.setRef}>
<Scrollable ref={this.setRef}>
{prepend}
<div role='feed' className='item-list' />
<div className='scrollable__append'>
<LoadingIndicator />
</div>
<ItemList isLoading />
{footer}
</div>
</Scrollable>
);
} else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
scrollableArea = (
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
<Scrollable fullscreen={fullscreen} ref={this.setRef} onMouseMove={this.handleMouseMove}>
{prepend}
<div role='feed' className={classNames('item-list', className)}>
<ItemList className={className}>
{loadPending}
{Children.map(this.props.children, (child, index) => (
@@ -378,14 +374,14 @@ class ScrollableList extends PureComponent {
{loadMore}
{!hasMore && append}
</div>
</ItemList>
{footer}
</div>
</Scrollable>
);
} else {
scrollableArea = (
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef}>
<Scrollable fullscreen={fullscreen} ref={this.setRef}>
{alwaysPrepend && prepend}
<div className='empty-column-indicator'>
@@ -393,7 +389,7 @@ class ScrollableList extends PureComponent {
</div>
{footer}
</div>
</Scrollable>
);
}
@@ -1,8 +1,9 @@
import PropTypes from 'prop-types';
import { cloneElement, Component } from 'react';
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
import getRectFromEntry from '../../features/ui/util/get_rect_from_entry';
import scheduleIdleTask from '../../features/ui/util/schedule_idle_task';
import { Article } from './components';
// Diff these props in the "unrendered" state
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
@@ -108,23 +109,22 @@ export default class IntersectionObserverArticle extends Component {
if (!isIntersecting && (isHidden || cachedHeight)) {
return (
<article
<Article
ref={this.handleRef}
aria-posinset={index + 1}
aria-setsize={listLength}
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
data-id={id}
tabIndex={-1}
>
{children && cloneElement(children, { hidden: true })}
</article>
</Article>
);
}
return (
<article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex={-1}>
<Article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id}>
{children && cloneElement(children, { hidden: false })}
</article>
</Article>
);
}