import { Add as MUIAddIcon, Delete, Done as MUIDoneIcon, Edit as MUIEditIcon, } from '@mui/icons-material'; import { Box as MUIBox, List as MUIList, ListItem as MUIListItem, ListItemIcon as MUIListItemIcon, ListItemProps as MUIListItemProps, ListProps as MUIListProps, SxProps, Theme, } from '@mui/material'; import { FC, ForwardedRef, forwardRef, ReactNode, useCallback, useImperativeHandle, useMemo, useState, } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { BLUE, GREY, RED } from '../lib/consts/DEFAULT_THEME'; import Checkbox, { CheckboxProps } from './Checkbox'; import Divider from './Divider'; import FlexBox, { FlexBoxProps } from './FlexBox'; import IconButton, { IconButtonProps } from './IconButton'; import { BodyText } from './Text'; type OnCheckboxChange = Exclude; type ListOptionalPropsWithDefaults = { allowCheckAll?: boolean; allowEdit?: boolean; edit?: boolean; initialCheckAll?: boolean; insertHeader?: boolean; listItemKeyPrefix?: string; listItemProps?: MUIListItemProps; listProps?: MUIListProps; renderListItem?: (key: string, value: T) => ReactNode; scroll?: boolean; }; type ListOptionalPropsWithoutDefaults = { allowAddItem?: boolean; allowCheckItem?: boolean; allowDelete?: boolean; allowEditItem?: boolean; header?: ReactNode; listEmpty?: ReactNode; onAdd?: IconButtonProps['onClick']; onDelete?: IconButtonProps['onClick']; onEdit?: IconButtonProps['onClick']; onAllCheckboxChange?: CheckboxProps['onChange']; onItemCheckboxChange?: ( key: string, ...onChangeParams: Parameters ) => ReturnType; renderListItemCheckboxState?: (key: string, value: T) => boolean; }; type ListOptionalProps = ListOptionalPropsWithDefaults & ListOptionalPropsWithoutDefaults; type ListProps = FlexBoxProps & ListOptionalProps & { listItems: Record; }; type ListForwardedRefContent = { setCheckAll?: (value: boolean) => void; }; const HEADER_SPACING = '.3em'; const LIST_DEFAULT_PROPS: Required & ListOptionalPropsWithoutDefaults = { header: undefined, allowAddItem: undefined, allowCheckAll: false, allowCheckItem: undefined, allowDelete: undefined, allowEdit: false, allowEditItem: undefined, edit: false, initialCheckAll: false, insertHeader: true, listEmpty: undefined, listItemKeyPrefix: uuidv4(), listItemProps: {}, listProps: {}, onAdd: undefined, onDelete: undefined, onEdit: undefined, onAllCheckboxChange: undefined, onItemCheckboxChange: undefined, renderListItem: (key) => {key}, renderListItemCheckboxState: undefined, scroll: false, }; const LIST_ICON_MIN_WIDTH = '56px'; const CHECK_ALL_MIN_WIDTH = `calc(${LIST_ICON_MIN_WIDTH} - ${HEADER_SPACING})`; const List = forwardRef( ( { header, allowCheckAll: isAllowCheckAll = LIST_DEFAULT_PROPS.allowCheckAll, allowEdit: isAllowEdit = LIST_DEFAULT_PROPS.allowEdit, edit: isEdit = LIST_DEFAULT_PROPS.edit, initialCheckAll = LIST_DEFAULT_PROPS.initialCheckAll, insertHeader: isInsertHeader = LIST_DEFAULT_PROPS.insertHeader, listEmpty = LIST_DEFAULT_PROPS.listEmpty, listItemKeyPrefix = LIST_DEFAULT_PROPS.listItemKeyPrefix, listItemProps: { sx: listItemSx, ...restListItemProps } = LIST_DEFAULT_PROPS.listItemProps, listItems, listProps: { sx: listSx, ...restListProps } = LIST_DEFAULT_PROPS.listProps, onAdd, onDelete, onEdit, onAllCheckboxChange, onItemCheckboxChange, renderListItem = LIST_DEFAULT_PROPS.renderListItem, renderListItemCheckboxState, scroll: isScroll = LIST_DEFAULT_PROPS.scroll, // Input props that depend on other input props. allowAddItem: isAllowAddItem = isAllowEdit, allowCheckItem: isAllowCheckItem = isAllowEdit, allowDelete: isAllowDelete = isAllowEdit, allowEditItem: isAllowEditItem = isAllowEdit, ...rootProps }: ListProps, ref: ForwardedRef, ) => { const [isCheckAll, setIsCheckAll] = useState(initialCheckAll); const addItemButton = useMemo( () => isAllowAddItem ? ( ) : undefined, [isAllowAddItem, onAdd], ); const deleteItemButton = useMemo( () => isEdit && isAllowDelete ? ( ) : undefined, [isAllowDelete, isEdit, onDelete], ); const editItemButton = useMemo(() => { if (isAllowEditItem) { return ( {isEdit ? : } ); } return undefined; }, [isAllowEditItem, isEdit, onEdit]); const checkAllElement = useMemo(() => { let element; if (isEdit && isAllowCheckItem) { element = isAllowCheckAll ? ( { const [, isChecked] = args; onAllCheckboxChange?.call(null, ...args); setIsCheckAll(isChecked); }} /> ) : ( ); } return element; }, [ isAllowCheckAll, isAllowCheckItem, isCheckAll, isEdit, onAllCheckboxChange, ]); const headerElement = useMemo( () => isInsertHeader && header ? ( {checkAllElement} {typeof header === 'string' ? ( <> {header} ) : ( header )} {deleteItemButton} {editItemButton} {addItemButton} ) : ( header ), [ addItemButton, checkAllElement, deleteItemButton, editItemButton, header, isInsertHeader, ], ); const listEmptyElement = useMemo( () => typeof listEmpty === 'string' ? ( {listEmpty} ) : ( listEmpty ), [listEmpty], ); const listItemCheckbox = useCallback( (key: string, checked?: boolean) => isEdit && isAllowCheckItem ? ( onItemCheckboxChange?.call(null, key, ...args) } /> ) : undefined, [isAllowCheckItem, isEdit, onItemCheckboxChange], ); const listItemElements = useMemo(() => { const entries = Object.entries(listItems); return entries.length > 0 ? entries.map(([key, value]) => ( {listItemCheckbox( key, renderListItemCheckboxState?.call(null, key, value), )} {renderListItem(key, value)} )) : listEmptyElement; }, [ listEmptyElement, listItemCheckbox, listItemKeyPrefix, listItems, listItemSx, renderListItem, renderListItemCheckboxState, restListItemProps, ]); const listScrollSx: SxProps | undefined = useMemo( () => (isScroll ? { maxHeight: '100%', overflowY: 'scroll' } : undefined), [isScroll], ); useImperativeHandle( ref, () => ({ setCheckAll: (value) => setIsCheckAll(value), }), [], ); return ( {headerElement} {listItemElements} ); }, ); List.defaultProps = LIST_DEFAULT_PROPS; List.displayName = 'List'; export type { ListForwardedRefContent, ListProps }; export default List as ( props: ListProps & { ref?: ForwardedRef }, ) => ReturnType>>;