import React, { useEffect, useRef, useState } from "react"; import classnames from "classnames"; import { Tooltip as ReactTooltip5 } from "react-tooltip-5"; import { uniqueId } from "lodash"; import Button from "components/buttons/Button"; import Icon from "components/Icon"; import { stringToClipboard } from "utilities/copy_text"; type CopyButtonVariant = "secondary" | "subdued" | "compact"; interface ICopyButtonProps { copyText: string; /** Override the button's content. Defaults to ``. */ children?: React.ReactNode; /** `"subdued"` (default) — borderless low-emphasis icon-only button — * see #35329. * `"secondary"` — bordered 36×36 icon-only button (the current preferred * secondary style — see #35329). * `"compact"` — icon collapsed to its natural size, no extra vertical * chrome. Use for inline-with-text copy actions so the surrounding row * doesn't grow to button height. */ variant?: CopyButtonVariant; size?: "small" | "default"; className?: string; ariaLabel?: string; /** Distance in px from the anchor to the tooltip. Defaults to `4` — tight * for inline-with-text copy actions. Pass `10` to match react-tooltip 5's * own default when the trigger is a larger floating button. */ tooltipOffset?: number; /** Table buttons show on row hover and tab focus only */ rowHover?: boolean; } const baseClass = "copy-button"; const HIDE_AFTER_MS = 1000; const SUCCESS_MESSAGE = "Copied!"; const ERROR_MESSAGE = "Copy failed"; const CopyButton = ({ copyText, children, variant = "subdued", size, className, ariaLabel = "Copy to clipboard", tooltipOffset = 4, rowHover = false, }: ICopyButtonProps) => { const [message, setMessage] = useState(null); const tipIdRef = useRef(uniqueId("copy-button-tooltip-")); const timerRef = useRef | null>(null); useEffect(() => { return () => { if (timerRef.current) { clearTimeout(timerRef.current); } }; }, []); const onClick = (evt: React.MouseEvent) => { evt.preventDefault(); // Drop focus only for mouse activations, so a lingering focus ring // doesn't stay visible after a click — keyboard Enter/Space report // `detail === 0` and must keep their tab position. if (evt.detail !== 0) { evt.currentTarget.blur(); } // Cancel any previous click's hide timer so the new badge gets the full // window — and so the hide doesn't race a slow `writeText()` (>1s would // leave the message visible with no timer to clear it). if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } const scheduleHide = () => { timerRef.current = setTimeout(() => setMessage(null), HIDE_AFTER_MS); }; stringToClipboard(copyText) .then(() => { setMessage(SUCCESS_MESSAGE); scheduleHide(); }) .catch(() => { setMessage(ERROR_MESSAGE); scheduleHide(); }); }; const isCompact = variant === "compact"; return ( {message} ); }; export default CopyButton;