fix(striker-ui): expose check all state in List

main
Tsu-ba-me 2 years ago
parent 3abd21df72
commit c5c4028fa6
  1. 428
      striker-ui/components/List.tsx

@ -14,7 +14,16 @@ import {
SxProps, SxProps,
Theme, Theme,
} from '@mui/material'; } from '@mui/material';
import { FC, ReactNode, useCallback, useMemo, useState } from 'react'; import {
FC,
ForwardedRef,
forwardRef,
ReactNode,
useCallback,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { BLUE, GREY, RED } from '../lib/consts/DEFAULT_THEME'; import { BLUE, GREY, RED } from '../lib/consts/DEFAULT_THEME';
@ -27,7 +36,7 @@ import { BodyText } from './Text';
type OnCheckboxChange = Exclude<CheckboxProps['onChange'], undefined>; type OnCheckboxChange = Exclude<CheckboxProps['onChange'], undefined>;
type ListOptionalPropsWithDefaults<T = unknown> = { type ListOptionalPropsWithDefaults<T extends unknown = unknown> = {
allowCheckAll?: boolean; allowCheckAll?: boolean;
allowEdit?: boolean; allowEdit?: boolean;
edit?: boolean; edit?: boolean;
@ -40,7 +49,7 @@ type ListOptionalPropsWithDefaults<T = unknown> = {
scroll?: boolean; scroll?: boolean;
}; };
type ListOptionalPropsWithoutDefaults<T = unknown> = { type ListOptionalPropsWithoutDefaults<T extends unknown = unknown> = {
allowAddItem?: boolean; allowAddItem?: boolean;
allowCheckItem?: boolean; allowCheckItem?: boolean;
allowDelete?: boolean; allowDelete?: boolean;
@ -58,14 +67,18 @@ type ListOptionalPropsWithoutDefaults<T = unknown> = {
renderListItemCheckboxState?: (key: string, value: T) => boolean; renderListItemCheckboxState?: (key: string, value: T) => boolean;
}; };
type ListOptionalProps<T = unknown> = ListOptionalPropsWithDefaults<T> & type ListOptionalProps<T extends unknown = unknown> =
ListOptionalPropsWithoutDefaults<T>; ListOptionalPropsWithDefaults<T> & ListOptionalPropsWithoutDefaults<T>;
type ListProps<T = unknown> = FlexBoxProps & type ListProps<T extends unknown = unknown> = FlexBoxProps &
ListOptionalProps<T> & { ListOptionalProps<T> & {
listItems: Record<string, T>; listItems: Record<string, T>;
}; };
type ListForwardedRefContent = {
setCheckAll?: (value: boolean) => void;
};
const HEADER_SPACING = '.3em'; const HEADER_SPACING = '.3em';
const LIST_DEFAULT_PROPS: Required<ListOptionalPropsWithDefaults> & const LIST_DEFAULT_PROPS: Required<ListOptionalPropsWithDefaults> &
ListOptionalPropsWithoutDefaults = { ListOptionalPropsWithoutDefaults = {
@ -96,208 +109,229 @@ const LIST_ICON_MIN_WIDTH = '56px';
const CHECK_ALL_MIN_WIDTH = `calc(${LIST_ICON_MIN_WIDTH} - ${HEADER_SPACING})`; const CHECK_ALL_MIN_WIDTH = `calc(${LIST_ICON_MIN_WIDTH} - ${HEADER_SPACING})`;
const List = <T,>({ const List = forwardRef(
header, <T,>(
allowCheckAll: isAllowCheckAll = LIST_DEFAULT_PROPS.allowCheckAll, {
allowEdit: isAllowEdit = LIST_DEFAULT_PROPS.allowEdit, header,
edit: isEdit = LIST_DEFAULT_PROPS.edit, allowCheckAll: isAllowCheckAll = LIST_DEFAULT_PROPS.allowCheckAll,
initialCheckAll = LIST_DEFAULT_PROPS.initialCheckAll, allowEdit: isAllowEdit = LIST_DEFAULT_PROPS.allowEdit,
insertHeader: isInsertHeader = LIST_DEFAULT_PROPS.insertHeader, edit: isEdit = LIST_DEFAULT_PROPS.edit,
listEmpty = LIST_DEFAULT_PROPS.listEmpty, initialCheckAll = LIST_DEFAULT_PROPS.initialCheckAll,
listItemKeyPrefix = LIST_DEFAULT_PROPS.listItemKeyPrefix, insertHeader: isInsertHeader = LIST_DEFAULT_PROPS.insertHeader,
listItemProps: { listEmpty = LIST_DEFAULT_PROPS.listEmpty,
sx: listItemSx, listItemKeyPrefix = LIST_DEFAULT_PROPS.listItemKeyPrefix,
...restListItemProps listItemProps: {
} = LIST_DEFAULT_PROPS.listItemProps, sx: listItemSx,
listItems, ...restListItemProps
listProps: { sx: listSx, ...restListProps } = LIST_DEFAULT_PROPS.listProps, } = LIST_DEFAULT_PROPS.listItemProps,
onAdd, listItems,
onDelete, listProps: {
onEdit, sx: listSx,
onAllCheckboxChange, ...restListProps
onItemCheckboxChange, } = LIST_DEFAULT_PROPS.listProps,
renderListItem = LIST_DEFAULT_PROPS.renderListItem, onAdd,
renderListItemCheckboxState, onDelete,
scroll: isScroll = LIST_DEFAULT_PROPS.scroll, onEdit,
// Input props that depend on other input props. onAllCheckboxChange,
allowAddItem: isAllowAddItem = isAllowEdit, onItemCheckboxChange,
allowCheckItem: isAllowCheckItem = isAllowEdit, renderListItem = LIST_DEFAULT_PROPS.renderListItem,
allowDelete: isAllowDelete = isAllowEdit, renderListItemCheckboxState,
allowEditItem: isAllowEditItem = isAllowEdit, 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 ...rootProps
}: ListProps<T>): ReturnType<FC<ListProps<T>>> => { }: ListProps<T>,
const [isCheckAll, setIsCheckAll] = useState<boolean>(initialCheckAll); ref: ForwardedRef<ListForwardedRefContent>,
) => {
const [isCheckAll, setIsCheckAll] = useState<boolean>(initialCheckAll);
const addItemButton = useMemo( const addItemButton = useMemo(
() => () =>
isAllowAddItem ? ( isAllowAddItem ? (
<IconButton onClick={onAdd} size="small"> <IconButton onClick={onAdd} size="small">
<MUIAddIcon /> <MUIAddIcon />
</IconButton> </IconButton>
) : undefined, ) : undefined,
[isAllowAddItem, onAdd], [isAllowAddItem, onAdd],
); );
const deleteItemButton = useMemo( const deleteItemButton = useMemo(
() => () =>
isEdit && isAllowDelete ? ( isEdit && isAllowDelete ? (
<IconButton <IconButton
onClick={onDelete} onClick={onDelete}
size="small" size="small"
sx={{ sx={{
backgroundColor: RED, backgroundColor: RED,
color: GREY, color: GREY,
'&:hover': { backgroundColor: `${RED}F0` }, '&:hover': { backgroundColor: `${RED}F0` },
}} }}
> >
<Delete /> <Delete />
</IconButton> </IconButton>
) : undefined, ) : undefined,
[isAllowDelete, isEdit, onDelete], [isAllowDelete, isEdit, onDelete],
); );
const editItemButton = useMemo(() => { const editItemButton = useMemo(() => {
if (isAllowEditItem) { if (isAllowEditItem) {
return ( return (
<IconButton onClick={onEdit} size="small"> <IconButton onClick={onEdit} size="small">
{isEdit ? <MUIDoneIcon sx={{ color: BLUE }} /> : <MUIEditIcon />} {isEdit ? <MUIDoneIcon sx={{ color: BLUE }} /> : <MUIEditIcon />}
</IconButton> </IconButton>
); );
} }
return undefined; return undefined;
}, [isAllowEditItem, isEdit, onEdit]); }, [isAllowEditItem, isEdit, onEdit]);
const checkAllElement = useMemo(() => { const checkAllElement = useMemo(() => {
let element; let element;
if (isEdit && isAllowCheckItem) { if (isEdit && isAllowCheckItem) {
element = isAllowCheckAll ? ( element = isAllowCheckAll ? (
<MUIBox sx={{ minWidth: CHECK_ALL_MIN_WIDTH }}> <MUIBox sx={{ minWidth: CHECK_ALL_MIN_WIDTH }}>
<Checkbox <Checkbox
checked={isCheckAll} checked={isCheckAll}
edge="start" edge="start"
onChange={(...args) => { onChange={(...args) => {
const [, isChecked] = args; const [, isChecked] = args;
onAllCheckboxChange?.call(null, ...args); onAllCheckboxChange?.call(null, ...args);
setIsCheckAll(isChecked); setIsCheckAll(isChecked);
}} }}
/> />
</MUIBox> </MUIBox>
) : ( ) : (
<Divider sx={{ minWidth: CHECK_ALL_MIN_WIDTH }} /> <Divider sx={{ minWidth: CHECK_ALL_MIN_WIDTH }} />
); );
} }
return element; return element;
}, [ }, [
isAllowCheckAll, isAllowCheckAll,
isAllowCheckItem, isAllowCheckItem,
isCheckAll, isCheckAll,
isEdit, isEdit,
onAllCheckboxChange, onAllCheckboxChange,
]); ]);
const headerElement = useMemo( const headerElement = useMemo(
() => () =>
isInsertHeader ? ( isInsertHeader ? (
<FlexBox row spacing={HEADER_SPACING} sx={{ height: '2.4em' }}> <FlexBox row spacing={HEADER_SPACING} sx={{ height: '2.4em' }}>
{checkAllElement} {checkAllElement}
{typeof header === 'string' ? ( {typeof header === 'string' ? (
<> <>
<BodyText>{header}</BodyText> <BodyText>{header}</BodyText>
<Divider sx={{ flexGrow: 1 }} /> <Divider sx={{ flexGrow: 1 }} />
</> </>
) : ( ) : (
header header
)} )}
{deleteItemButton} {deleteItemButton}
{editItemButton} {editItemButton}
{addItemButton} {addItemButton}
</FlexBox> </FlexBox>
) : ( ) : (
header header
), ),
[ [
addItemButton, addItemButton,
checkAllElement, checkAllElement,
deleteItemButton, deleteItemButton,
editItemButton, editItemButton,
header, header,
isInsertHeader, isInsertHeader,
], ],
); );
const listEmptyElement = useMemo( const listEmptyElement = useMemo(
() => () =>
typeof listEmpty === 'string' ? ( typeof listEmpty === 'string' ? (
<BodyText>{listEmpty}</BodyText> <BodyText>{listEmpty}</BodyText>
) : ( ) : (
listEmpty listEmpty
), ),
[listEmpty], [listEmpty],
); );
const listItemCheckbox = useCallback( const listItemCheckbox = useCallback(
(key: string, checked?: boolean) => (key: string, checked?: boolean) =>
isEdit && isAllowCheckItem ? ( isEdit && isAllowCheckItem ? (
<MUIListItemIcon sx={{ minWidth: LIST_ICON_MIN_WIDTH }}> <MUIListItemIcon sx={{ minWidth: LIST_ICON_MIN_WIDTH }}>
<Checkbox <Checkbox
checked={checked} checked={checked}
edge="start" edge="start"
onChange={(...args) => onChange={(...args) =>
onItemCheckboxChange?.call(null, key, ...args) onItemCheckboxChange?.call(null, key, ...args)
} }
/> />
</MUIListItemIcon> </MUIListItemIcon>
) : undefined, ) : undefined,
[isAllowCheckItem, isEdit, onItemCheckboxChange], [isAllowCheckItem, isEdit, onItemCheckboxChange],
); );
const listItemElements = useMemo(() => { const listItemElements = useMemo(() => {
const entries = Object.entries(listItems); const entries = Object.entries(listItems);
return entries.length > 0 return entries.length > 0
? entries.map(([key, value]) => ( ? entries.map(([key, value]) => (
<MUIListItem <MUIListItem
{...restListItemProps} {...restListItemProps}
key={`${listItemKeyPrefix}-${key}`} key={`${listItemKeyPrefix}-${key}`}
sx={{ paddingLeft: 0, paddingRight: 0, ...listItemSx }} sx={{ paddingLeft: 0, paddingRight: 0, ...listItemSx }}
> >
{listItemCheckbox( {listItemCheckbox(
key, key,
renderListItemCheckboxState?.call(null, key, value), renderListItemCheckboxState?.call(null, key, value),
)} )}
{renderListItem(key, value)} {renderListItem(key, value)}
</MUIListItem> </MUIListItem>
)) ))
: listEmptyElement; : listEmptyElement;
}, [ }, [
listEmptyElement, listEmptyElement,
listItemCheckbox, listItemCheckbox,
listItemKeyPrefix, listItemKeyPrefix,
listItems, listItems,
listItemSx, listItemSx,
renderListItem, renderListItem,
renderListItemCheckboxState, renderListItemCheckboxState,
restListItemProps, restListItemProps,
]); ]);
const listScrollSx: SxProps<Theme> | undefined = useMemo( const listScrollSx: SxProps<Theme> | undefined = useMemo(
() => (isScroll ? { maxHeight: '100%', overflowY: 'scroll' } : undefined), () => (isScroll ? { maxHeight: '100%', overflowY: 'scroll' } : undefined),
[isScroll], [isScroll],
); );
return ( useImperativeHandle(
<FlexBox spacing={0} {...rootProps}> ref,
{headerElement} () => ({
<MUIList setCheckAll: (value) => setIsCheckAll(value),
{...restListProps} }),
sx={{ paddingBottom: 0, paddingTop: 0, ...listScrollSx, ...listSx }} [],
> );
{listItemElements}
</MUIList> return (
</FlexBox> <FlexBox spacing={0} {...rootProps}>
); {headerElement}
}; <MUIList
{...restListProps}
sx={{ paddingBottom: 0, paddingTop: 0, ...listScrollSx, ...listSx }}
>
{listItemElements}
</MUIList>
</FlexBox>
);
},
);
List.defaultProps = LIST_DEFAULT_PROPS; List.defaultProps = LIST_DEFAULT_PROPS;
List.displayName = 'List';
export type { ListForwardedRefContent, ListProps };
export default List; export default List as <T>(
props: ListProps<T> & { ref?: ForwardedRef<ListForwardedRefContent> },
) => ReturnType<FC<ListProps<T>>>;

Loading…
Cancel
Save