Files
DocsGPT/frontend/src/components/CopyButton.tsx
Hanzalah Waheed 5aa4ec1b9f fix: cleanup ConversationBubble and fix CopyButton (#2073)
* fix: rm states for hovering. use tailwind classes instead

* fix: use group hover css intead of states

* chore: no point in having separate defaults if cant be customised

* fix: move default bg colors into conditionals
2025-10-18 21:59:15 +03:00

125 lines
3.4 KiB
TypeScript

import clsx from 'clsx';
import copy from 'copy-to-clipboard';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import CheckMark from '../assets/checkmark.svg?react';
import CopyIcon from '../assets/copy.svg?react';
type CopyButtonProps = {
textToCopy: string;
iconSize?: string;
padding?: string;
showText?: boolean;
copiedDuration?: number;
className?: string;
iconWrapperClassName?: string;
textClassName?: string;
};
const DEFAULT_ICON_SIZE = 'w-4 h-4';
const DEFAULT_PADDING = 'p-2';
const DEFAULT_COPIED_DURATION = 2000;
export default function CopyButton({
textToCopy,
iconSize = DEFAULT_ICON_SIZE,
padding = DEFAULT_PADDING,
showText = false,
copiedDuration = DEFAULT_COPIED_DURATION,
className,
iconWrapperClassName,
textClassName,
}: CopyButtonProps) {
const { t } = useTranslation();
const [isCopied, setIsCopied] = useState(false);
const timeoutIdRef = useRef<number | null>(null);
const iconWrapperClasses = clsx(
'flex items-center justify-center rounded-full transition-colors duration-150 ease-in-out',
padding,
{
[`bg-[#FFFFFF}] dark:bg-transparent hover:bg-[#EEEEEE] dark:hover:bg-purple-taupe`]:
!isCopied,
'bg-green-100 dark:bg-green-900 hover:bg-green-100 dark:hover:bg-green-900':
isCopied,
},
iconWrapperClassName,
);
const rootButtonClasses = clsx(
'flex items-center gap-2 group',
'focus:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 rounded-full',
className,
);
const textSpanClasses = clsx(
'text-xs text-gray-600 dark:text-gray-400 transition-opacity duration-150 ease-in-out',
{ 'opacity-75': isCopied },
textClassName,
);
const IconComponent = isCopied ? CheckMark : CopyIcon;
const iconClasses = clsx(iconSize, {
'stroke-green-600 dark:stroke-green-400': isCopied,
'fill-none text-gray-700 dark:text-gray-300': !isCopied,
});
const buttonTitle = isCopied
? t('conversation.copied')
: t('conversation.copy');
const displayedText = isCopied
? t('conversation.copied')
: t('conversation.copy');
const handleCopy = useCallback(() => {
if (isCopied) return;
try {
const success = copy(textToCopy);
if (success) {
setIsCopied(true);
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
timeoutIdRef.current = setTimeout(() => {
setIsCopied(false);
timeoutIdRef.current = null;
}, copiedDuration);
} else {
console.warn('Copy command failed.');
}
} catch (error) {
console.error('Failed to copy text:', error);
}
}, [textToCopy, copiedDuration, isCopied]);
useEffect(() => {
return () => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
};
}, []);
return (
<button
type="button"
onClick={handleCopy}
className={rootButtonClasses}
title={buttonTitle}
aria-label={buttonTitle}
disabled={isCopied}
>
<div className={iconWrapperClasses}>
<IconComponent className={iconClasses} aria-hidden="true" />
</div>
{showText && <span className={textSpanClasses}>{displayedText}</span>}
<span className="sr-only" aria-live="polite" aria-atomic="true">
{isCopied ? t('conversation.copied', 'Copied to clipboard') : ''}
</span>
</button>
);
}