Add JSDoc comments to timeline components
This commit is contained in:
parent
aad192d150
commit
fbb460817f
4 changed files with 50 additions and 12 deletions
|
@ -9,13 +9,19 @@ import { useSettings } from 'soapbox/hooks';
|
||||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||||
|
|
||||||
interface IScrollTopButton {
|
interface IScrollTopButton {
|
||||||
|
/** Callback when clicked, and also when scrolled to the top. */
|
||||||
onClick: () => void,
|
onClick: () => void,
|
||||||
|
/** Number of unread items. */
|
||||||
count: number,
|
count: number,
|
||||||
|
/** Message to display in the button (should contain a `{count}` value). */
|
||||||
message: MessageDescriptor,
|
message: MessageDescriptor,
|
||||||
|
/** Distance from the top of the screen (scrolling down) before the button appears. */
|
||||||
threshold?: number,
|
threshold?: number,
|
||||||
|
/** Distance from the top of the screen (scrolling up) before the action is triggered. */
|
||||||
autoloadThreshold?: number,
|
autoloadThreshold?: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Floating new post counter above timelines, clicked to scroll to top. */
|
||||||
const ScrollTopButton: React.FC<IScrollTopButton> = ({
|
const ScrollTopButton: React.FC<IScrollTopButton> = ({
|
||||||
onClick,
|
onClick,
|
||||||
count,
|
count,
|
||||||
|
@ -41,7 +47,7 @@ const ScrollTopButton: React.FC<IScrollTopButton> = ({
|
||||||
} else {
|
} else {
|
||||||
setScrolled(false);
|
setScrolled(false);
|
||||||
}
|
}
|
||||||
}, 150, { trailing: true }), []);
|
}, 150, { trailing: true }), [autoload, threshold, autoloadThreshold]);
|
||||||
|
|
||||||
const scrollUp = () => {
|
const scrollUp = () => {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { useSettings } from 'soapbox/hooks';
|
||||||
import LoadMore from './load_more';
|
import LoadMore from './load_more';
|
||||||
import { Spinner, Text } from './ui';
|
import { Spinner, Text } from './ui';
|
||||||
|
|
||||||
|
/** Custom Viruoso component context. */
|
||||||
type Context = {
|
type Context = {
|
||||||
itemClassName?: string,
|
itemClassName?: string,
|
||||||
listClassName?: string,
|
listClassName?: string,
|
||||||
|
@ -20,6 +21,7 @@ type SavedScrollPosition = {
|
||||||
offset: number,
|
offset: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Custom Virtuoso Item component representing a single scrollable item. */
|
||||||
// NOTE: It's crucial to space lists with **padding** instead of margin!
|
// NOTE: It's crucial to space lists with **padding** instead of margin!
|
||||||
// Pass an `itemClassName` like `pb-3`, NOT a `space-y-3` className
|
// Pass an `itemClassName` like `pb-3`, NOT a `space-y-3` className
|
||||||
// https://virtuoso.dev/troubleshooting#list-does-not-scroll-to-the-bottom--items-jump-around
|
// https://virtuoso.dev/troubleshooting#list-does-not-scroll-to-the-bottom--items-jump-around
|
||||||
|
@ -27,6 +29,7 @@ const Item: Components<Context>['Item'] = ({ context, ...rest }) => (
|
||||||
<div className={context?.itemClassName} {...rest} />
|
<div className={context?.itemClassName} {...rest} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Custom Virtuoso List component for the outer container. */
|
||||||
// Ensure the className winds up here
|
// Ensure the className winds up here
|
||||||
const List: Components<Context>['List'] = React.forwardRef((props, ref) => {
|
const List: Components<Context>['List'] = React.forwardRef((props, ref) => {
|
||||||
const { context, ...rest } = props;
|
const { context, ...rest } = props;
|
||||||
|
@ -34,28 +37,47 @@ const List: Components<Context>['List'] = React.forwardRef((props, ref) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IScrollableList extends VirtuosoProps<any, any> {
|
interface IScrollableList extends VirtuosoProps<any, any> {
|
||||||
|
/** Unique key to preserve the scroll position when navigating back. */
|
||||||
scrollKey?: string,
|
scrollKey?: string,
|
||||||
|
/** Pagination callback when the end of the list is reached. */
|
||||||
onLoadMore?: () => void,
|
onLoadMore?: () => void,
|
||||||
|
/** Whether the data is currently being fetched. */
|
||||||
isLoading?: boolean,
|
isLoading?: boolean,
|
||||||
|
/** Whether to actually display the loading state. */
|
||||||
showLoading?: boolean,
|
showLoading?: boolean,
|
||||||
|
/** Whether we expect an additional page of data. */
|
||||||
hasMore?: boolean,
|
hasMore?: boolean,
|
||||||
|
/** Additional element to display at the top of the list. */
|
||||||
prepend?: React.ReactNode,
|
prepend?: React.ReactNode,
|
||||||
|
/** Whether to display the prepended element. */
|
||||||
alwaysPrepend?: boolean,
|
alwaysPrepend?: boolean,
|
||||||
|
/** Message to display when the list is loaded but empty. */
|
||||||
emptyMessage?: React.ReactNode,
|
emptyMessage?: React.ReactNode,
|
||||||
|
/** Scrollable content. */
|
||||||
children: Iterable<React.ReactNode>,
|
children: Iterable<React.ReactNode>,
|
||||||
|
/** Callback when the list is scrolled to the top. */
|
||||||
onScrollToTop?: () => void,
|
onScrollToTop?: () => void,
|
||||||
|
/** Callback when the list is scrolled. */
|
||||||
onScroll?: () => void,
|
onScroll?: () => void,
|
||||||
|
/** Placeholder component to render while loading. */
|
||||||
placeholderComponent?: React.ComponentType | React.NamedExoticComponent,
|
placeholderComponent?: React.ComponentType | React.NamedExoticComponent,
|
||||||
|
/** Number of placeholders to render while loading. */
|
||||||
placeholderCount?: number,
|
placeholderCount?: number,
|
||||||
|
/** Pull to refresh callback. */
|
||||||
onRefresh?: () => Promise<any>,
|
onRefresh?: () => Promise<any>,
|
||||||
|
/** Extra class names on the Virtuoso element. */
|
||||||
className?: string,
|
className?: string,
|
||||||
|
/** Class names on each item container. */
|
||||||
itemClassName?: string,
|
itemClassName?: string,
|
||||||
|
/** `id` attribute on the Virtuoso element. */
|
||||||
id?: string,
|
id?: string,
|
||||||
|
/** CSS styles on the Virtuoso element. */
|
||||||
style?: React.CSSProperties,
|
style?: React.CSSProperties,
|
||||||
|
/** Whether to use the window to scroll the content instead of Virtuoso's container. */
|
||||||
useWindowScroll?: boolean
|
useWindowScroll?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Legacy ScrollableList with Virtuoso for backwards-compatibility */
|
/** Legacy ScrollableList with Virtuoso for backwards-compatibility. */
|
||||||
const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||||
scrollKey,
|
scrollKey,
|
||||||
prepend = null,
|
prepend = null,
|
||||||
|
@ -88,7 +110,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||||
const topIndex = useRef<number>(scrollData ? scrollData.index : 0);
|
const topIndex = useRef<number>(scrollData ? scrollData.index : 0);
|
||||||
const topOffset = useRef<number>(scrollData ? scrollData.offset : 0);
|
const topOffset = useRef<number>(scrollData ? scrollData.offset : 0);
|
||||||
|
|
||||||
/** Normalized children */
|
/** Normalized children. */
|
||||||
const elements = Array.from(children || []);
|
const elements = Array.from(children || []);
|
||||||
|
|
||||||
const showPlaceholder = showLoading && Placeholder && placeholderCount > 0;
|
const showPlaceholder = showLoading && Placeholder && placeholderCount > 0;
|
||||||
|
@ -129,7 +151,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/* Render an empty state instead of the scrollable list */
|
/* Render an empty state instead of the scrollable list. */
|
||||||
const renderEmpty = (): JSX.Element => {
|
const renderEmpty = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className='mt-2'>
|
<div className='mt-2'>
|
||||||
|
@ -146,7 +168,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Render a single item */
|
/** Render a single item. */
|
||||||
const renderItem = (_i: number, element: JSX.Element): JSX.Element => {
|
const renderItem = (_i: number, element: JSX.Element): JSX.Element => {
|
||||||
if (showPlaceholder) {
|
if (showPlaceholder) {
|
||||||
return <Placeholder />;
|
return <Placeholder />;
|
||||||
|
@ -192,7 +214,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||||
return 0;
|
return 0;
|
||||||
}, [showLoading, initialTopMostItemIndex]);
|
}, [showLoading, initialTopMostItemIndex]);
|
||||||
|
|
||||||
/** Render the actual Virtuoso list */
|
/** Render the actual Virtuoso list. */
|
||||||
const renderFeed = (): JSX.Element => (
|
const renderFeed = (): JSX.Element => (
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@ -222,7 +244,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Conditionally render inner elements */
|
/** Conditionally render inner elements. */
|
||||||
const renderBody = (): JSX.Element => {
|
const renderBody = (): JSX.Element => {
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
return renderEmpty();
|
return renderEmpty();
|
||||||
|
|
|
@ -14,24 +14,31 @@ import type { VirtuosoHandle } from 'react-virtuoso';
|
||||||
import type { IScrollableList } from 'soapbox/components/scrollable_list';
|
import type { IScrollableList } from 'soapbox/components/scrollable_list';
|
||||||
|
|
||||||
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
|
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
|
||||||
|
/** Unique key to preserve the scroll position when navigating back. */
|
||||||
scrollKey: string,
|
scrollKey: string,
|
||||||
|
/** List of status IDs to display. */
|
||||||
statusIds: ImmutableOrderedSet<string>,
|
statusIds: ImmutableOrderedSet<string>,
|
||||||
|
/** Last _unfiltered_ status ID (maxId) for pagination. */
|
||||||
lastStatusId?: string,
|
lastStatusId?: string,
|
||||||
|
/** Pinned statuses to show at the top of the feed. */
|
||||||
featuredStatusIds?: ImmutableOrderedSet<string>,
|
featuredStatusIds?: ImmutableOrderedSet<string>,
|
||||||
|
/** Pagination callback when the end of the list is reached. */
|
||||||
onLoadMore?: (lastStatusId: string) => void,
|
onLoadMore?: (lastStatusId: string) => void,
|
||||||
|
/** Whether the data is currently being fetched. */
|
||||||
isLoading: boolean,
|
isLoading: boolean,
|
||||||
|
/** Whether the server did not return a complete page. */
|
||||||
isPartial?: boolean,
|
isPartial?: boolean,
|
||||||
|
/** Whether we expect an additional page of data. */
|
||||||
hasMore: boolean,
|
hasMore: boolean,
|
||||||
prepend?: React.ReactNode,
|
/** Message to display when the list is loaded but empty. */
|
||||||
emptyMessage: React.ReactNode,
|
emptyMessage: React.ReactNode,
|
||||||
alwaysPrepend?: boolean,
|
/** ID of the timeline in Redux. */
|
||||||
timelineId?: string,
|
timelineId?: string,
|
||||||
queuedItemSize?: number,
|
/** Whether to display a gap or border between statuses in the list. */
|
||||||
onScrollToTop?: () => void,
|
|
||||||
onScroll?: () => void,
|
|
||||||
divideType: 'space' | 'border',
|
divideType: 'space' | 'border',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Feed of statuses, built atop ScrollableList. */
|
||||||
const StatusList: React.FC<IStatusList> = ({
|
const StatusList: React.FC<IStatusList> = ({
|
||||||
statusIds,
|
statusIds,
|
||||||
lastStatusId,
|
lastStatusId,
|
||||||
|
|
|
@ -15,9 +15,11 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ITimeline extends Omit<IStatusList, 'statusIds' | 'isLoading' | 'hasMore'> {
|
interface ITimeline extends Omit<IStatusList, 'statusIds' | 'isLoading' | 'hasMore'> {
|
||||||
|
/** ID of the timeline in Redux. */
|
||||||
timelineId: string,
|
timelineId: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Scrollable list of statuses from a timeline in the Redux store. */
|
||||||
const Timeline: React.FC<ITimeline> = ({
|
const Timeline: React.FC<ITimeline> = ({
|
||||||
timelineId,
|
timelineId,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
|
@ -55,6 +57,7 @@ const Timeline: React.FC<ITimeline> = ({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
|
timelineId={timelineId}
|
||||||
onScrollToTop={handleScrollToTop}
|
onScrollToTop={handleScrollToTop}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
lastStatusId={lastStatusId}
|
lastStatusId={lastStatusId}
|
||||||
|
|
Loading…
Reference in a new issue