117 lines
3.5 KiB
TypeScript
117 lines
3.5 KiB
TypeScript
import { useCallback, useId, useState } from 'react';
|
|
|
|
import { Article } from '@/mastodon/components/scrollable_list/components';
|
|
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
|
|
import KeyboardArrowUpIcon from '@/material-icons/400-24px/keyboard_arrow_up.svg?react';
|
|
|
|
import { Icon } from '../icon';
|
|
import type { IconProp } from '../icon';
|
|
import { ListItemButton, ListItemWrapper } from '../list_item';
|
|
|
|
export interface TruncatedListItemInfo<TListItem> {
|
|
item: TListItem;
|
|
index: number;
|
|
totalListLength: number;
|
|
isLastElement: boolean;
|
|
}
|
|
|
|
interface ToggleButtonOptions {
|
|
title: NonNullable<React.ReactNode>;
|
|
subtitle?: React.ReactNode;
|
|
icon?: IconProp;
|
|
}
|
|
|
|
interface TruncatedListProps<TListItem> {
|
|
visibleItems: TListItem[];
|
|
truncatedItems: TListItem[];
|
|
renderListItem: (
|
|
itemInfo: TruncatedListItemInfo<TListItem>,
|
|
) => React.ReactElement;
|
|
toggleButton: ToggleButtonOptions;
|
|
}
|
|
|
|
/**
|
|
* Truncate the children of an `ItemList` component with this helper
|
|
* component.
|
|
* It handles rendering the children with correct indexes for accessibility,
|
|
* and has a configurable toggle button.
|
|
*/
|
|
export const TruncatedListItems = <TListItem,>({
|
|
visibleItems,
|
|
truncatedItems,
|
|
toggleButton,
|
|
renderListItem,
|
|
}: TruncatedListProps<TListItem>) => {
|
|
const toggleButtonId = useId();
|
|
const toggleButtonDescId = `${toggleButtonId}-desc`;
|
|
const [showTruncatedItems, setShowTruncatedItems] = useState(false);
|
|
const toggleTruncatedItems = useCallback(() => {
|
|
setShowTruncatedItems((prev) => !prev);
|
|
}, []);
|
|
|
|
const hasHiddenAccounts = truncatedItems.length > 0;
|
|
// Add the toggle button's item to the list size when needed
|
|
const initialListSize = visibleItems.length + (hasHiddenAccounts ? 1 : 0);
|
|
const totalListLength =
|
|
initialListSize + (showTruncatedItems ? truncatedItems.length : 0);
|
|
|
|
return (
|
|
<>
|
|
{visibleItems.map((item, index) => {
|
|
return renderListItem({
|
|
item,
|
|
index,
|
|
totalListLength,
|
|
isLastElement:
|
|
index === visibleItems.length - 1 && !hasHiddenAccounts,
|
|
});
|
|
})}
|
|
{hasHiddenAccounts && (
|
|
<Article
|
|
aria-posinset={initialListSize}
|
|
aria-setsize={totalListLength}
|
|
aria-labelledby={toggleButtonId}
|
|
aria-describedby={toggleButtonDescId}
|
|
>
|
|
<ListItemWrapper
|
|
icon={
|
|
toggleButton.icon && (
|
|
<Icon id='toggle-icon' icon={toggleButton.icon} />
|
|
)
|
|
}
|
|
sideContent={
|
|
<Icon
|
|
id='open-status'
|
|
icon={
|
|
showTruncatedItems
|
|
? KeyboardArrowUpIcon
|
|
: KeyboardArrowDownIcon
|
|
}
|
|
/>
|
|
}
|
|
>
|
|
<ListItemButton
|
|
id={toggleButtonId}
|
|
subtitleId={toggleButtonDescId}
|
|
aria-expanded={showTruncatedItems}
|
|
onClick={toggleTruncatedItems}
|
|
subtitle={toggleButton.subtitle}
|
|
>
|
|
{toggleButton.title}
|
|
</ListItemButton>
|
|
</ListItemWrapper>
|
|
</Article>
|
|
)}
|
|
{showTruncatedItems &&
|
|
truncatedItems.map((item, index) => {
|
|
return renderListItem({
|
|
item,
|
|
index: initialListSize + index + 1,
|
|
totalListLength,
|
|
isLastElement: index === truncatedItems.length - 1,
|
|
});
|
|
})}
|
|
</>
|
|
);
|
|
};
|