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 | 32x 25x 25x 25x 18x 13x 2x 25x 5x 5x 5x 5x 5x 2x 2x 25x 5x | 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);
Iif (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>
);
}
|