import { forwardRef, LegacyRef, memo, Ref, useEffect, useMemo, useRef } from 'react'; import type { CSSProperties, ReactNode } from 'react'; import { ListOnItemsRenderedProps, VariableSizeList } from 'react-window'; import { SortableElement as sortableElement } from 'react-sortable-hoc'; import InfiniteScroll from 'react-infinite-scroll-component'; import InfiniteLoader from 'react-window-infinite-loader'; import clsx from 'clsx'; import MuiList from '@mui/material/List'; import MuiListSubheader from '@mui/material/ListSubheader'; import MenuItem from './MenuItem'; import type { InfinityScrollProps, ListItemProps, MenuItemProps, SortableProps } from './types'; const LIST_ITEM_RECURSION_DEPTH_FACTOR = 1000000; const VARIABLE_SIZE_LIST_ITEM_SIZE = 42; const VARIABLE_SIZE_LIST_ITEM_MARGIN = 2; const variableSizeListItemSize = (listItems: MenuItemProps[]) => (index: number) => { const optionsToRender = listItems[index]?.options?.filter(item => !item.hidden).length || 0; return VARIABLE_SIZE_LIST_ITEM_SIZE * (optionsToRender + 1); }; const SortableItem = sortableElement(({ children }: SortableProps) => children); const ListItems = // eslint-disable-next-line forwardRef((props, ref) => { const { listItems, modifyMenuItem, InsertBeforeList, InsertAfterList, styles, classes, handleClick, multiple, sortable, deletable, handleDelete, showSelectedIcon, ariaLabel, noOptionsText, noResultsText, loadingText, variableSizeList, variableSizeListProps, infinityScrollProps } = props; const visibleItemsCount = useMemo( () => listItems.filter(item => !item.hidden).length, [listItems] ); const renderNoResultsText = () => noResultsText ? ( {noResultsText} ) : null; const renderNoOptionsText = () => noOptionsText ? ( {noOptionsText} ) : null; const renderLoadingText = () => loadingText ? ( {loadingText} ) : null; const renderListItem = (item: MenuItemProps, index: number) => { const { id, name, disabled } = item; const key = `key_${id || name}${index}`; if (sortable) { return ( ); } return ( ); }; const renderListItemRecursive = (recursionDepth = 0) => (listItem: MenuItemProps, index: number): JSX.Element => { const { options, ...listItemProps } = listItem; return renderListItem( options ? { ...listItemProps, nestedListItems: options?.map(renderListItemRecursive(recursionDepth + 1)), options } : listItem, // avoiding index collisions index + LIST_ITEM_RECURSION_DEPTH_FACTOR * recursionDepth ); }; const renderListChild = (props: { index: number; style: CSSProperties; data: ListItemProps['listItems']; }) => { const { index, style, data } = props; const listItem = data[index]; const styleHeight = parseFloat(style.height as string) - VARIABLE_SIZE_LIST_ITEM_MARGIN; if (infinityScrollProps && data[index] === undefined) { return (
  • Loading...
  • ); } return renderListItemRecursive()( { ...listItem, style: { ...style, height: styleHeight } }, index ); }; function useResetCache(data: number) { const ref = useRef(null); useEffect(() => { ref.current?.resetAfterIndex(0, true); }, [data]); return ref; } const vslRef = useResetCache(visibleItemsCount); if (variableSizeList) { const innerElementType = forwardRef((props, ref: LegacyRef) => (
      )); const VariableSizeListWrapper = (props: { // eslint-disable-next-line refValue: Ref; itemCount: number; // eslint-disable-next-line onItemsRendered?: (props: ListOnItemsRenderedProps) => any; }) => { return ( <> {InsertBeforeList?.()} {InsertAfterList?.()} ); }; const variableSizeListHeight = variableSizeListProps?.height || VARIABLE_SIZE_LIST_ITEM_SIZE * 6; if (infinityScrollProps) { const { allItemsCount, fetchFunction, threshold = 2 } = infinityScrollProps; const nextPortionExists = listItems.length < allItemsCount; const itemCount = listItems.length < 1 && noOptionsText ? 1 : nextPortionExists ? listItems.length + 1 : listItems.length; const isItemLoaded = (index: number) => !nextPortionExists || index < listItems.length; return ( {({ onItemsRendered, ref }) => VariableSizeListWrapper({ itemCount, refValue: ref, onItemsRendered }) } ); } const itemCount = listItems.length < 1 && noOptionsText ? 1 : listItems.length; return ; } const InfiniteScrollWrapper = (props: { infinityScrollProps: InfinityScrollProps; listItemsLength: number; children: ReactNode; }) => { const { allItemsCount, fetchFunction, threshold = 2 } = props.infinityScrollProps; const nextPortionExists = listItems.length < allItemsCount; return ( {props.children} ); }; return ( {InsertBeforeList?.()} {listItems.length > 0 ? ( <> {visibleItemsCount < 1 ? renderNoResultsText() : null} {infinityScrollProps ? ( {listItems.map(renderListItemRecursive())} ) : ( listItems.map(renderListItemRecursive()) )} ) : ( renderNoOptionsText() )} {InsertAfterList?.()} ); }); export default memo(ListItems);