diff --git a/striker-ui/components/IconWithIndicator.tsx b/striker-ui/components/IconWithIndicator.tsx index bf1f9123..d05f5b31 100644 --- a/striker-ui/components/IconWithIndicator.tsx +++ b/striker-ui/components/IconWithIndicator.tsx @@ -4,15 +4,29 @@ import { BoxProps as MUIBoxProps, SvgIconProps, } from '@mui/material'; -import { createElement, FC } from 'react'; +import { + createElement, + forwardRef, + ReactNode, + useCallback, + useImperativeHandle, + useMemo, +} from 'react'; import { BLACK, BLUE } from '../lib/consts/DEFAULT_THEME'; import FlexBox, { FlexBoxProps } from './FlexBox'; +import { BodyText, BodyTextProps } from './Text'; +import useProtect from '../hooks/useProtect'; +import useProtectedState from '../hooks/useProtectedState'; + +type IndicatorValue = boolean | number; type IconWithIndicatorOptionalPropsWithDefault = { iconProps?: SvgIconProps; indicatorProps?: FlexBoxProps; + indicatorTextProps?: BodyTextProps; + initialIndicatorValue?: IndicatorValue; }; type IconWithIndicatorOptionalProps = IconWithIndicatorOptionalPropsWithDefault; @@ -22,71 +36,159 @@ type IconWithIndicatorProps = MUIBoxProps & icon: SvgIconComponent; }; +type IconWithIndicatorForwardedRefContent = { + indicate?: (value: IndicatorValue) => void; +}; + +const CONTAINER_LENGTH = '1.7em'; const ICON_WITH_INDICATOR_DEFAULT_PROPS: Required = { iconProps: {}, indicatorProps: {}, + indicatorTextProps: {}, + initialIndicatorValue: false, }; - -const IconWithIndicator: FC = ({ - icon, - iconProps: { - sx: iconSx, - - ...restIconProps - } = ICON_WITH_INDICATOR_DEFAULT_PROPS.iconProps, - indicatorProps: { - sx: indicatorSx, - - ...restIndicatorProps - } = ICON_WITH_INDICATOR_DEFAULT_PROPS.indicatorProps, - sx, -}) => { - const containerLength = '1.7em'; - const indicatorLength = '24%'; - const indicatorOffset = '.1rem'; - - return ( - - {createElement(icon, { - ...restIconProps, - - sx: { height: '100%', width: '100%', ...iconSx }, - })} - {createElement(FlexBox, { - row: true, - - ...restIndicatorProps, - - sx: { - backgroundColor: BLUE, - borderColor: BLACK, - borderRadius: '50%', - borderStyle: 'solid', - borderWidth: '.2rem', - bottom: indicatorOffset, - boxSizing: 'content-box', - height: 0, - justifyContent: 'center', - paddingBottom: indicatorLength, - position: 'absolute', - right: indicatorOffset, - width: indicatorLength, - - ...indicatorSx, - }, - })} - - ); -}; +const INDICATOR_LENGTH = { small: '24%', medium: '50%' }; +const INDICATOR_MAX = 99; +const INDICATOR_OFFSET = { small: '.1rem', medium: '0rem' }; + +const IconWithIndicator = forwardRef< + IconWithIndicatorForwardedRefContent, + IconWithIndicatorProps +>( + ( + { + icon, + iconProps: { + sx: iconSx, + + ...restIconProps + } = ICON_WITH_INDICATOR_DEFAULT_PROPS.iconProps, + indicatorProps: { + sx: indicatorSx, + + ...restIndicatorProps + } = ICON_WITH_INDICATOR_DEFAULT_PROPS.indicatorProps, + indicatorTextProps: { + sx: indicatorTextSx, + + ...restIndicatorTextProps + } = ICON_WITH_INDICATOR_DEFAULT_PROPS.indicatorTextProps, + initialIndicatorValue = ICON_WITH_INDICATOR_DEFAULT_PROPS.initialIndicatorValue, + sx, + }, + ref, + ) => { + const { protect } = useProtect(); + const [indicatorValue, setIndicatorValue] = useProtectedState< + boolean | number + >(initialIndicatorValue, protect); + + const buildIndicator = useCallback( + ( + indicatorContent: ReactNode, + indicatorLength: number | string, + indicatorOffset: number | string, + ) => ( + + {indicatorContent} + + ), + [indicatorSx, restIndicatorProps], + ); + const buildIndicatorText = useCallback( + (value: IndicatorValue) => ( + + {value > INDICATOR_MAX ? `${INDICATOR_MAX}+` : value} + + ), + [indicatorTextSx, restIndicatorTextProps], + ); + + const indicator = useMemo(() => { + let result; + + if (indicatorValue) { + let indicatorContent; + let indicatorLength = INDICATOR_LENGTH.small; + let indicatorOffset = INDICATOR_OFFSET.small; + + if (Number.isFinite(indicatorValue)) { + indicatorContent = buildIndicatorText(indicatorValue); + indicatorLength = INDICATOR_LENGTH.medium; + indicatorOffset = INDICATOR_OFFSET.medium; + } + + result = buildIndicator( + indicatorContent, + indicatorLength, + indicatorOffset, + ); + } + + return result; + }, [buildIndicator, buildIndicatorText, indicatorValue]); + + useImperativeHandle( + ref, + () => ({ + indicate: (value) => setIndicatorValue(value), + }), + [setIndicatorValue], + ); + + return ( + + {createElement(icon, { + ...restIconProps, + + sx: { height: '100%', width: '100%', ...iconSx }, + })} + {indicator} + + ); + }, +); IconWithIndicator.defaultProps = ICON_WITH_INDICATOR_DEFAULT_PROPS; +IconWithIndicator.displayName = 'IconWithIndicator'; + +export type { IconWithIndicatorForwardedRefContent, IconWithIndicatorProps }; export default IconWithIndicator;