All files / src/features/Merchants/MerchantSidePanel/GiveMerchantFile/components CopyButton.tsx

84.37% Statements 27/32
87.5% Branches 21/24
100% Functions 7/7
84.37% Lines 27/32

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 112 113 114 115 116 117 118 119                                170x               989x 989x       989x   989x 1978x   6x         1972x       989x 989x   3x         986x       989x   2x     2x         2x 1x     2x 2x 2x                 989x 173x 173x 2x         989x 989x   989x                                                      
import { CheckIcon, CopyIcon, WarningIcon } from "@phosphor-icons/react";
import GiveIconButton from "@shared/IconButton/GiveIconButton";
import GiveTooltip from "@shared/Tooltip/GiveTooltip";
import { useState, useCallback, useRef, useEffect } from "react";
 
type CopyState = "idle" | "copied" | "error";
 
type Props = {
  text: string;
  disabled?: boolean;
  tooltipText?: string;
  testId?: string;
  hidden?: boolean;
  fluidWidth?: boolean;
};
 
const CopyButton = ({
  text,
  disabled = false,
  tooltipText = "link",
  testId,
  hidden,
  fluidWidth,
}: Props) => {
  const [copyState, setCopyState] = useState<CopyState>("idle");
  const capitalizedText = `${tooltipText
    .charAt(0)
    .toUpperCase()}${tooltipText.slice(1)}`;
 
  const resetStateRef = useRef<NodeJS.Timeout | null>(null);
 
  const getTooltipTitle = () => {
    switch (copyState) {
      case "copied":
        return `${capitalizedText} Copied`;
      case "error":
        return `Failed to copy ${capitalizedText}`;
      case "idle":
      default:
        return `Copy ${capitalizedText}`;
    }
  };
 
  const getTooltipIcon = () => {
    switch (copyState) {
      case "copied":
        return <CheckIcon size={18} weight="bold" />;
      case "error":
        return <WarningIcon size={18} weight="bold" />;
      case "idle":
      default:
        return null;
    }
  };
 
  const handleCopy = useCallback(
    async (e: React.MouseEvent<HTMLButtonElement>) => {
      e.stopPropagation();
 
      // Don't try to copy if already processing or no text
      Iif (copyState !== "idle" || !text) {
        return;
      }
 
      // Reset state after a delay, regardless of success/failure
      resetStateRef.current = setTimeout(() => {
        setCopyState("idle");
      }, 2500); // Slightly longer for error visibility
 
      try {
        await navigator.clipboard.writeText(text);
        setCopyState("copied");
      } catch (err) {
        console.error(`Failed to copy ${tooltipText}:`, err);
        setCopyState("error");
      }
    },
    [text, copyState, tooltipText, capitalizedText],
  );
 
  useEffect(() => {
    return () => {
      if (resetStateRef.current) {
        clearTimeout(resetStateRef.current);
      }
    };
  }, []);
 
  const isProcessing = copyState !== "idle";
  const effectiveDisabled = disabled || isProcessing;
 
  return (
    <GiveTooltip
      title={getTooltipTitle()}
      placement="top"
      color="default"
      iconRight={getTooltipIcon()}
      disableHoverListener={disabled}
      fluidWidth={fluidWidth}
    >
      <GiveIconButton
        Icon={CopyIcon}
        size="small"
        variant={copyState === "copied" ? "filled" : "ghost"}
        onClick={handleCopy}
        sx={{
          pointerEvents: copyState === "copied" ? "none" : "auto",
        }}
        disabled={effectiveDisabled}
        data-testid={testId}
        hidden={hidden || !text}
        aria-label={getTooltipTitle()}
      />
    </GiveTooltip>
  );
};
 
export default CopyButton;