All files / molecules/Toast/web Toast.tsx

3.57% Statements 1/28
0% Branches 0/11
0% Functions 0/6
4% Lines 1/25

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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111                10x                                                                                                                                                                                                            
import { cn } from '@sb/libs';
import { motion } from 'framer-motion';
import { ToastProps } from '../Toast.types';
import { useEffect, useRef, useState } from 'react';
import { X } from '@sb/ui/components/atoms/Icons/web/Icons';
import { useIsMobile } from '@sb/hooks/Utilities/useIsMobile';
import { TOAST_VARIANTS as variants, secondsToMillis } from '../utils';
 
export const Toast = ({ title, message, variant, duration = 'short', onClose }: ToastProps) => {
  const isMobile = useIsMobile();
  const [progress, setProgress] = useState(0);
  const [paused, setPaused] = useState(false);
  const startTimeRef = useRef<number | null>(null);
  const durationMs = duration === 'short' ? secondsToMillis(3) : secondsToMillis(6);
  const { backgroundColor, toastContentColor, iconBackground, icon } = variants[variant];
 
  useEffect(() => {
    let frame: number;
 
    const tick = (timestamp: number) => {
      if (!startTimeRef.current) startTimeRef.current = timestamp;
      const elapsed = timestamp - startTimeRef.current;
      const percent = Math.min((elapsed / durationMs) * 100, 100);
      setProgress(percent);
 
      if (percent < 100) {
        frame = requestAnimationFrame(tick);
      } else {
        onClose?.();
      }
    };
 
    if (!paused) {
      frame = requestAnimationFrame(tick);
    }
 
    return () => cancelAnimationFrame(frame);
  }, [paused, durationMs, onClose]);
 
  const handleMouseEnter = () => setPaused(true);
  const handleMouseLeave = () => {
    setPaused(false);
    startTimeRef.current = performance.now() - (progress / 100) * durationMs;
  };
 
  const toastColors = {
    iconBackground,
    backgroundColor,
    textColor: toastContentColor,
    closeIconColor: toastContentColor,
    progressIndicatorColor: toastContentColor,
    closeIconBorderColor: toastContentColor,
  };
 
  return (
    <motion.div
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      initial={{ opacity: 0, x: 50 }}
      animate={{ opacity: 1, x: 0 }}
      exit={{ opacity: 0, x: 50 }}
      transition={{ type: 'spring', stiffness: 300, damping: 24 }}
      style={{
        color: toastColors.textColor,
        backgroundColor: toastColors.backgroundColor,
      }}
      className={cn(
        'min-w-70 relative flex w-full max-w-80 items-center overflow-hidden rounded-xl shadow-xl',
        {
          'bottom-14 right-0': isMobile,
        }
      )}
    >
      <div className="flex flex-1 flex-row items-center justify-between gap-2 p-3">
        <div
          style={{ backgroundColor: iconBackground }}
          className="grid size-8 place-items-center rounded-full"
        >
          {icon}
        </div>
        <div className="pointer-events-none flex-1">
          {title && <p className="font-semibold">{title}</p>}
          <p className="text-sm">{message}</p>
        </div>
        <div
          role="button"
          onClick={onClose}
          style={{
            borderColor: toastColors.closeIconBorderColor,
          }}
          className={`cursor-pointer rounded-full border p-2 opacity-50 hover:opacity-100`}
        >
          <X color={toastColors.closeIconColor} />
        </div>
      </div>
      {/* Duration progress bar */}
      <motion.div
        role="progressbar"
        aria-valuemin={0}
        aria-valuemax={100}
        aria-valuenow={progress}
        initial={{ width: '0%' }}
        animate={{ width: `${progress}%` }}
        transition={{ ease: 'linear', duration: 0.1 }}
        className={`absolute bottom-0 left-0 h-[3px] opacity-80`}
        style={{ backgroundColor: toastColors.progressIndicatorColor }}
      />
    </motion.div>
  );
};