All files / src/components IconSpinner.tsx

93.93% Statements 31/33
85.71% Branches 6/7
100% Functions 2/2
93.93% Lines 31/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 831x 1x 1x 1x   1x             1x   1x 1x 1x 1x 1x 1x 21x 21x 21x   1x                                                                       1x 1x 21x   21x 3x 3x 3x   21x 21x     21x 21x 21x 21x     21x   1x  
import { SPIN_SPEED } from '@/constants/common';
import { isSpinSpeed } from '@/utils/typeGuards/common';
import { keyframes, styled } from '@mui/material/styles';
import { cloneElement, isValidElement, FC } from 'react';
 
const spin = keyframes`
  from {
    transform: rotate(360deg);
  }
  to {
    transform: rotate(0deg);
  }
`;
 
// Defined at module scope (not inside the component) so the styled component
// keeps a stable identity across re-renders; otherwise a parent re-render —
// e.g. an updating result count — remounts the node and restarts the animation.
const SpinningWrapper = styled('div', {
  shouldForwardProp: (prop) => prop !== 'speed',
})<{ speed: number }>(({ speed }) => ({
  display: 'inline-flex',
  animation: `${spin} ${speed}s linear infinite`,
}));
 
/**
 * A component that adds a spinning animation to any icon component passed as a child.
 * The component preserves any existing styles while adding the spinning animation.
 *
 * @component
 * @param props - The props from Material-UI's SvgIcon component
 * @returns An animated version of the provided icon
 *
 * @example
 * ```typescript
 * // Simple icon with default size and speed
 * <IconSpinner>
 *   <MyIcon />
 * </IconSpinner>
 *
 * // Icon with custom size, default speed
 * <IconSpinner>
 *   <MyIcon style={{ width: 100, height: 100 }} />
 * </IconSpinner>
 *
 * // Icon with custom speed (as string)
 * <IconSpinner speed={"fast"}>
 *   <MyIcon style={{ width: 100, height: 100 }} />
 * </IconSpinner>
 *
 * // Icon with custom speed (as number)
 * <IconSpinner speed={2}>
 *   <MyIcon style={{ width: 100, height: 100 }} />
 * </IconSpinner>
 *
 * // Icon with custom speed from SpinSpeeds enum
 * <IconSpinner speed={SpinSpeeds.MEDIUM}>
 *   <MyIcon style={{ width: 100, height: 100 }} />
 * </IconSpinner>
 * ```
 * @source
 */
const IconSpinner: FC<IconSpinnerProps> = (props: IconSpinnerProps) => {
  let { speed = 2 } = props;
 
  if (typeof speed === 'string') {
    const key = speed.toUpperCase();
    speed = isSpinSpeed(key) ? SPIN_SPEED[key] : SPIN_SPEED.MEDIUM;
  }
 
  // Clone the child element to preserve its props
  if (!isValidElement(props.children)) {
    return null;
  }
  const child = cloneElement(props.children);
  return (
    <SpinningWrapper speed={speed} data-testid="spinning-wrapper">
      {child}
    </SpinningWrapper>
  );
};
 
export default IconSpinner;