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,
});
})}
</>
);
};