mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 14:12:24 +08:00

* User Experience: apply the same pattern feedback for all copy to clipboard buttons * add copy icon to all ClipboardButton use cases * Change primary color for copy to clipboard in create token * Add success button variant * Remove copy confirmation from TableCellInspectModal because it's in the base component now * Design tweaks to copy confirmation - Only change the icon to tick to avoid the button changing size - Change button to success green - Only show copy confirmation state for 2 seconds * revert TabelCellInspectModal text button back * revert accidental change to ShareLink Co-authored-by: joshhunt <josh@trtr.co>
86 lines
2.5 KiB
TypeScript
86 lines
2.5 KiB
TypeScript
import React, { useCallback, useRef, useState, useEffect } from 'react';
|
|
|
|
import { Button, ButtonProps } from '../Button';
|
|
|
|
export interface Props extends ButtonProps {
|
|
/** A function that returns text to be copied */
|
|
getText(): string;
|
|
/** Callback when the text has been successfully copied */
|
|
onClipboardCopy?(copiedText: string): void;
|
|
/** Callback when there was an error copying the text */
|
|
onClipboardError?(copiedText: string, error: unknown): void;
|
|
}
|
|
|
|
const SHOW_SUCCESS_DURATION = 2 * 1000;
|
|
|
|
export function ClipboardButton({
|
|
onClipboardCopy,
|
|
onClipboardError,
|
|
children,
|
|
getText,
|
|
icon,
|
|
variant,
|
|
...buttonProps
|
|
}: Props) {
|
|
const [showCopySuccess, setShowCopySuccess] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let timeoutId: NodeJS.Timeout;
|
|
|
|
if (showCopySuccess) {
|
|
timeoutId = setTimeout(() => {
|
|
setShowCopySuccess(false);
|
|
}, SHOW_SUCCESS_DURATION);
|
|
}
|
|
|
|
return () => {
|
|
window.clearTimeout(timeoutId);
|
|
};
|
|
}, [showCopySuccess]);
|
|
|
|
const buttonRef = useRef<null | HTMLButtonElement>(null);
|
|
const copyTextCallback = useCallback(async () => {
|
|
const textToCopy = getText();
|
|
|
|
try {
|
|
await copyText(textToCopy, buttonRef);
|
|
setShowCopySuccess(true);
|
|
onClipboardCopy?.(textToCopy);
|
|
} catch (e) {
|
|
onClipboardError?.(textToCopy, e);
|
|
}
|
|
}, [getText, onClipboardCopy, onClipboardError]);
|
|
|
|
return (
|
|
<Button
|
|
onClick={copyTextCallback}
|
|
icon={showCopySuccess ? 'check' : icon}
|
|
variant={showCopySuccess ? 'success' : variant}
|
|
aria-label={showCopySuccess ? 'Copied' : undefined}
|
|
{...buttonProps}
|
|
ref={buttonRef}
|
|
>
|
|
{children}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
const copyText = async (text: string, buttonRef: React.MutableRefObject<HTMLButtonElement | null>) => {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
return navigator.clipboard.writeText(text);
|
|
} else {
|
|
// Use a fallback method for browsers/contexts that don't support the Clipboard API.
|
|
// See https://web.dev/async-clipboard/#feature-detection.
|
|
const input = document.createElement('input');
|
|
// Normally we'd append this to the body. However if we're inside a focus manager
|
|
// from react-aria, we can't focus anything outside of the managed area.
|
|
// Instead, let's append it to the button. Then we're guaranteed to be able to focus + copy.
|
|
buttonRef.current?.appendChild(input);
|
|
input.value = text;
|
|
input.focus();
|
|
input.select();
|
|
document.execCommand('copy');
|
|
input.remove();
|
|
}
|
|
};
|