feat: add image paste functionality to DescriptionImageDropZone component

- Implemented handlePaste function to process images from clipboard across all OS.
- Updated the component to handle pasted images and prevent default paste behavior.
- Enhanced user instructions to include pasting images in the UI.

Added a utility function to simulate pasting images in tests, ensuring cross-platform compatibility.
This commit is contained in:
Kacper
2025-12-16 02:49:26 +01:00
parent 87c0ab6daa
commit 7eeba5f17c
2 changed files with 92 additions and 1 deletions

View File

@@ -268,6 +268,52 @@ export function DescriptionImageDropZone({
[images, onImagesChange]
);
// Handle paste events to detect and process images from clipboard
// Works across all OS (Windows, Linux, macOS)
const handlePaste = useCallback(
(e: React.ClipboardEvent) => {
if (disabled || isProcessing) return;
const clipboardItems = e.clipboardData?.items;
if (!clipboardItems) return;
const imageFiles: File[] = [];
// Iterate through clipboard items to find images
for (let i = 0; i < clipboardItems.length; i++) {
const item = clipboardItems[i];
// Check if the item is an image
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
// Generate a filename for pasted images since they don't have one
const extension = item.type.split("/")[1] || "png";
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const renamedFile = new File(
[file],
`pasted-image-${timestamp}.${extension}`,
{ type: file.type }
);
imageFiles.push(renamedFile);
}
}
}
// If we found images, process them and prevent default paste behavior
if (imageFiles.length > 0) {
e.preventDefault();
// Create a FileList-like object from the array
const dataTransfer = new DataTransfer();
imageFiles.forEach((file) => dataTransfer.items.add(file));
processFiles(dataTransfer.files);
}
// If no images found, let the default paste behavior happen (paste text)
},
[disabled, isProcessing, processFiles]
);
return (
<div className={cn("relative", className)}>
{/* Hidden file input */}
@@ -313,6 +359,7 @@ export function DescriptionImageDropZone({
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
onPaste={handlePaste}
disabled={disabled}
autoFocus={autoFocus}
aria-invalid={error}
@@ -326,7 +373,7 @@ export function DescriptionImageDropZone({
{/* Hint text */}
<p className="text-xs text-muted-foreground mt-1">
Drag and drop images here or{" "}
Paste, drag and drop images, or{" "}
<button
type="button"
onClick={handleBrowseClick}

View File

@@ -36,3 +36,47 @@ export async function simulateFileDrop(
{ selector: targetSelector, content: fileContent, name: fileName, mime: mimeType }
);
}
/**
* Simulate pasting an image from clipboard onto an element
* Works across all OS (Windows, Linux, macOS)
*/
export async function simulateImagePaste(
page: Page,
targetSelector: string,
imageBase64: string,
mimeType: string = "image/png"
): Promise<void> {
await page.evaluate(
({ selector, base64, mime }) => {
const target = document.querySelector(selector);
if (!target) throw new Error(`Element not found: ${selector}`);
// Convert base64 to Blob
const byteCharacters = atob(base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: mime });
// Create a File from Blob
const file = new File([blob], "pasted-image.png", { type: mime });
// Create a DataTransfer with clipboard items
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
// Create ClipboardEvent with the image data
const clipboardEvent = new ClipboardEvent("paste", {
bubbles: true,
cancelable: true,
clipboardData: dataTransfer,
});
target.dispatchEvent(clipboardEvent);
},
{ selector: targetSelector, base64: imageBase64, mime: mimeType }
);
}