# 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>
      );
    }