import { clsx } from 'clsx'; import { Component } from 'react'; import { Status, Size } from '../common'; const radius = { xxs: 6, xs: 11, sm: 22, xl: 61 }; const ANIMATION_DURATION_IN_MS = 1500; export type ProcessIndicatorStatus = `${Status.PROCESSING | Status.FAILED | Status.SUCCEEDED | Status.HIDDEN}`; export interface ProcessIndicatorProps { /** @default 'processing' */ status?: ProcessIndicatorStatus; /** @default 'sm' */ size?: 'xxs' | `${Size.EXTRA_SMALL | Size.SMALL | Size.EXTRA_LARGE}`; className?: string; 'data-testid'?: string; onAnimationCompleted?: (status: ProcessIndicatorStatus) => void; } type ProcessIndicatorState = Required>; export default class ProcessIndicator extends Component< ProcessIndicatorProps, ProcessIndicatorState > { declare props: ProcessIndicatorProps & Required>; static defaultProps = { status: 'processing', size: 'sm', } satisfies Partial; interval = 0; timeout = 0; constructor(props: ProcessIndicator['props']) { super(props); this.state = { status: props.status, size: props.size, }; } /** * Create interval for animation duration (1500ms) * Update state only at the end of every interval * (end of animation loop) if props changed before end of animation */ componentDidMount() { this.interval = window.setInterval(() => { const { status: targetStatus, size: targetSize, onAnimationCompleted } = this.props; const { status: currentStatus, size: currentSize } = this.state; if (currentStatus !== targetStatus) { this.setState({ status: targetStatus }, () => { if (onAnimationCompleted) { this.timeout = window.setTimeout(() => { onAnimationCompleted(targetStatus); }, ANIMATION_DURATION_IN_MS); } }); } if (currentSize !== targetSize) { this.setState({ size: targetSize }); } }, ANIMATION_DURATION_IN_MS); } /** * Only trigger render if comopnent's state got * updated from interval callback */ shouldComponentUpdate(nextProps: ProcessIndicator['props'], nextState: ProcessIndicatorState) { const isSameStatus = nextProps.status === nextState.status; const isSameSize = nextProps.size === nextState.size; return isSameStatus && isSameSize; } // Clear interval before destroying component componentWillUnmount() { window.clearInterval(this.interval); window.clearTimeout(this.timeout); } render() { const { className, 'data-testid': dataTestId } = this.props; const { size, status } = this.state; const classes = clsx(`process process-${size}`, className, { [`process-danger`]: status === Status.FAILED, [`process-stopped`]: status === Status.HIDDEN, [`process-success`]: status === Status.SUCCEEDED, }); return ( ); } }