All files / src/components/display CopyButton.tsx

12.5% Statements 6/48
100% Branches 0/0
0% Functions 0/1
12.5% Lines 6/48

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 761x 1x   1x 1x     1x                               1x                                                                                                        
import { useCallback, useEffect, useRef, useState, type JSX } from "react";
import { Check, Clipboard } from "lucide-react";
import { type CopyButtonBuiltinProps } from "@grackle-ai/common";
import { ICON_MD } from "../../utils/iconSize.js";
import styles from "./CopyButton.module.scss";
 
/** Duration in milliseconds to show the "copied" checkmark before reverting. */
const COPIED_FEEDBACK_DURATION: number = 2000;
 
/** Props for the CopyButton component (`text` inferred from the built-in's zod schema). */
interface CopyButtonProps extends CopyButtonBuiltinProps {
  /** Additional CSS class name for positioning variants. */
  className?: string;
  /** Test ID for Storybook and E2E tests. */
  "data-testid"?: string;
}
 
/**
 * Small copy-to-clipboard button with visual feedback.
 *
 * Shows a clipboard emoji by default, switches to a checkmark on click,
 * then reverts after 2 seconds.
 */
export function CopyButton({
  text,
  className,
  "data-testid": testId,
}: CopyButtonProps): JSX.Element {
  const [copied, setCopied] = useState(false);
  const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
 
  useEffect(() => {
    return () => {
      if (timerRef.current !== undefined) {
        clearTimeout(timerRef.current);
      }
    };
  }, []);
 
  const handleClick = useCallback(async (): Promise<void> => {
    try {
      await navigator.clipboard.writeText(text);
      setCopied(true);
      if (timerRef.current !== undefined) {
        clearTimeout(timerRef.current);
      }
      timerRef.current = setTimeout(() => {
        setCopied(false);
        timerRef.current = undefined;
      }, COPIED_FEEDBACK_DURATION);
    } catch {
      /* clipboard API unavailable — fail silently */
    }
  }, [text]);
 
  return (
    <button
      type="button"
      className={`${styles.copyButton} ${className ?? ""}`}
      onClick={() => {
        handleClick().catch(() => {
          /* clipboard unavailable */
        });
      }}
      aria-label={copied ? "Copied" : "Copy to clipboard"}
      data-testid={testId ?? "copy-button"}
    >
      {copied ? (
        <Check size={ICON_MD} aria-hidden="true" />
      ) : (
        <Clipboard size={ICON_MD} aria-hidden="true" />
      )}
    </button>
  );
}