ui
ui copied to clipboard
enhancement request: dropzone
Hey chad!
Not to be boring, but there is some pretense of making a dropzone component? That'be so freaking useful!
Looking into it.
Adding a thumbs up to this request! Ideal case would be one which displays a preview of the uploaded file. I think Ant handles this quite well.
Hi Sha D. Cn!
I just wanted to ask, shouldn't this issue be flagged with the new component and roadmap labels?
Cheers 👋🏻
@demarchenac Agreed.
related: https://shadcn.rails-components.com/docs/components/dropzone
i needed a simple one today, not sure if this helps anyone but refer below:
import React, { useRef, useState } from 'react'; import { Card, CardContent } from "@/components/ui/card"; import { Icons } from '@/components/ui/icons'; import { Button } from '@/components/ui/button';
interface DropzoneProps { onChange: React.Dispatch<React.SetStateAction<string[]>>; className?: string; fileExtension?: string; }
export function Dropzone({ onChange, className, fileExtension, ...props }: DropzoneProps) { const fileInputRef = useRef<HTMLInputElement>(null); const [fileInfo, setFileInfo] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
const { files } = e.dataTransfer;
handleFiles(files);
};
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { files } = e.target;
if (files) {
handleFiles(files);
}
};
const handleFiles = (files: FileList) => {
const uploadedFile = files[0];
// Check file extension
if (fileExtension && !uploadedFile.name.endsWith(`.${fileExtension}`)) {
setError(`Invalid file type. Expected: .${fileExtension}`);
return;
}
const fileSizeInKB = Math.round(uploadedFile.size / 1024); // Convert to KB
const fileList = Array.from(files).map((file) => URL.createObjectURL(file));
onChange((prevFiles) => [...prevFiles, ...fileList]);
// Display file information
setFileInfo(`Uploaded file: ${uploadedFile.name} (${fileSizeInKB} KB)`);
setError(null); // Reset error state
};
const handleButtonClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
};
return (
<Card
className={`bg-muted border-dashed border-2 hover:border-muted-foreground/50 hover:cursor-pointer ${className}`}
>
<CardContent
className="flex flex-col items-center justify-center px-2 py-4 text-xs space-y-2"
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<Icons.import className="h-8 w-8 text-muted-foreground" />
<div className="flex items-center justify-center text-muted-foreground">
<span className="font-medium">Drag Files to Upload or</span>
<Button
variant="ghost"
size="sm"
className="ml-auto h-8 flex space-x-2 text-xs px-0 pl-1"
onClick={handleButtonClick}
>
Click Here
</Button>
<input
ref={fileInputRef}
type="file"
accept={`.${fileExtension}`} // Set accepted file type
onChange={handleFileInputChange}
className="hidden"
multiple
/>
</div>
{fileInfo && <p className="text-muted-foreground">{fileInfo}</p>}
{error && <span className="text-red-500">{error}</span>}
</CardContent>
</Card>
);
}
import React, { useRef, useState } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; // Define the props expected by the Dropzone component interface DropzoneProps { onChange: React.Dispatch<React.SetStateAction<string[]>>; className?: string; fileExtension?: string; } // Create the Dropzone component receiving props export function Dropzone({ onChange, className, fileExtension, ...props }: DropzoneProps) { // Initialize state variables using the useState hook const fileInputRef = useRef<HTMLInputElement | null>(null); // Reference to file input element const [fileInfo, setFileInfo] = useState<string | null>(null); // Information about the uploaded file const [error, setError] = useState<string | null>(null); // Error message state // Function to handle drag over event const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); }; // Function to handle drop event const handleDrop = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); const { files } = e.dataTransfer; handleFiles(files); }; // Function to handle file input change event const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const { files } = e.target; if (files) { handleFiles(files); } }; // Function to handle processing of uploaded files const handleFiles = (files: FileList) => { const uploadedFile = files[0]; // Check file extension if (fileExtension && !uploadedFile.name.endsWith(`.${fileExtension}`)) { setError(`Invalid file type. Expected: .${fileExtension}`); return; } const fileSizeInKB = Math.round(uploadedFile.size / 1024); // Convert to KB const fileList = Array.from(files).map((file) => URL.createObjectURL(file)); onChange((prevFiles) => [...prevFiles, ...fileList]); // Display file information setFileInfo(`Uploaded file: ${uploadedFile.name} (${fileSizeInKB} KB)`); setError(null); // Reset error state }; // Function to simulate a click on the file input element const handleButtonClick = () => { if (fileInputRef.current) { fileInputRef.current.click(); } }; return ( <Card className={`border-2 border-dashed bg-muted hover:cursor-pointer hover:border-muted-foreground/50 ${className}`} {...props} > <CardContent className="flex flex-col items-center justify-center space-y-2 px-2 py-4 text-xs" onDragOver={handleDragOver} onDrop={handleDrop} > <div className="flex items-center justify-center text-muted-foreground"> <span className="font-medium">Drag Files to Upload or</span> <Button variant="ghost" size="sm" className="ml-auto flex h-8 space-x-2 px-0 pl-1 text-xs" onClick={handleButtonClick} > Click Here </Button> <input ref={fileInputRef} type="file" accept={`.${fileExtension}`} // Set accepted file type onChange={handleFileInputChange} className="hidden" multiple /> </div> {fileInfo && <p className="text-muted-foreground">{fileInfo}</p>} {error && <span className="text-red-500">{error}</span>} </CardContent> </Card> ); }
// example usage
import { Dropzone } from '@/components/ui/dropzone';
import { useState } from 'react';
export default function Page() {
const [files, setFiles] = useState<string[]>([]);
return (
<div className="sm:py-5">
<Dropzone
onChange={setFiles}
className="w-full"
fileExtension="pdf"
/>
</div>
);
}
Fixed formatting for easy copy-paste @step2341 dropzone and removed unknown components and given example usage
@arafays Thanks for this. Would you be willing to explain two things to me, a newb to React/Typescript?
- Can you provide an example of how to declare/use the onChange handler?
- Is the Icons import from shadcn-ui? When I tried to use the shadcn-ui Icons component, I get an error on the Icons.import syntax that there is no method import. TIA!
@dandubya sure here is the example use and No the icons is component is something @step2341 used and implemented custom I don't know where he got it from I am removing it from the comment
Here is an example of how you would use it
import { Dropzone } from '@/components/ui/dropzone';
import { useEffect, useState } from 'react';
export default function Page() {
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
// Function to handle the change in uploaded files
const handleFileChange: React.Dispatch<React.SetStateAction<string[]>> = (
newState: React.SetStateAction<string[]>
) => {
setUploadedFiles(newState);
};
// remove this useEffect hook if you don't need to do anything with the uploaded files
useEffect(() => {
console.log(uploadedFiles);
}, [uploadedFiles]);
return (
<div className="p-5">
<div>
<h1>File Upload</h1>
<Dropzone
onChange={handleFileChange} // Pass the handler function
className="your-custom-class" // Optional: Add any custom class
fileExtension="jpg" // Set the expected file extension
/>
{/* Render the uploaded files remove if needed*/}
{uploadedFiles.length > 0 && (
<div>
<h2>Uploaded Files:</h2>
<ul>
{uploadedFiles.map((file, index) => (
<img key={index} src={file} />
))}
</ul>
</div>
)}
</div>
</div>
);
}
just remember you would need to handle form submission yourself if needed I can make a sandbox for submitting using react-hook-form
@arafays Thank you!
I have developed a dropzone component based on the suggestion provided by @step2341 . This component utilizes the default shadcn/ui input, manages forms, and integrates with React Hook Form.
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import React, { ChangeEvent, useRef } from "react";
interface DropzoneProps
extends Omit<
React.InputHTMLAttributes<HTMLInputElement>,
"value" | "onChange"
> {
classNameWrapper?: string;
className?: string;
dropMessage: string;
handleOnDrop: (acceptedFiles: FileList | null) => void;
}
const Dropzone = React.forwardRef<HTMLDivElement, DropzoneProps>(
(
{ className, classNameWrapper, dropMessage, handleOnDrop, ...props },
ref
) => {
const inputRef = useRef<HTMLInputElement | null>(null);
// Function to handle drag over event
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
handleOnDrop(null);
};
// Function to handle drop event
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
const { files } = e.dataTransfer;
if (inputRef.current) {
inputRef.current.files = files;
handleOnDrop(files);
}
};
// Function to simulate a click on the file input element
const handleButtonClick = () => {
if (inputRef.current) {
inputRef.current.click();
}
};
return (
<Card
ref={ref}
className={cn(
`border-2 border-dashed bg-muted hover:cursor-pointer hover:border-muted-foreground/50`,
classNameWrapper
)}
>
<CardContent
className="flex flex-col items-center justify-center space-y-2 px-2 py-4 text-xs"
onDragOver={handleDragOver}
onDrop={handleDrop}
onClick={handleButtonClick}
>
<div className="flex items-center justify-center text-muted-foreground">
<span className="font-medium">{dropMessage}</span>
<Input
{...props}
value={undefined}
ref={inputRef}
type="file"
className={cn("hidden", className)}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleOnDrop(e.target.files)
}
/>
</div>
</CardContent>
</Card>
);
}
);
export default Dropzone;
Here's how to use it:
const defaultValues: { file: null | File } = {
file: null,
};
const methods = useForm({
defaultValues,
shouldFocusError: true,
shouldUnregister: false,
shouldUseNativeValidation: false,
});
function handleOnDrop(acceptedFiles: FileList | null) {
if (acceptedFiles && acceptedFiles.length > 0) {
const allowedTypes = [
{ name: "csv", types: ["text/csv"] },
{
name: "excel",
types: [
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
],
},
];
const fileType = allowedTypes.find((allowedType) =>
allowedType.types.find((type) => type === acceptedFiles[0].type)
);
if (!fileType) {
methods.setValue("file", null);
methods.setError("file", {
message: "File type is not valid",
type: "typeError",
});
} else {
methods.setValue("file", acceptedFiles[0]);
methods.clearErrors("file");
}
} else {
methods.setValue("file", null);
methods.setError("file", {
message: "File is required",
type: "typeError",
});
}
}
return (
<FormProvider {...methods}>
<form
className="flex flex-col items-center justify-center w-100 gap-2"
onSubmit={methods.handleSubmit(handleFormSubmit)}
noValidate
autoComplete="off"
>
<FormField
control={methods.control}
name="file"
render={({ field }) => (
<FormItem className="w-full">
<FormControl>
<Dropzone
{...field}
dropMessage="Drop files or click here"
handleOnDrop={handleOnDrop}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{methods.watch("file") && (
<div className="flex items-center justify-center gap-3 p-4 relative">
<FileCheck2Icon className="h-4 w-4" />
<p className="text-sm font-medium">{methods.watch("file")?.name}</p>
</div>
)}
<Button type="submit">Salve</Button>
</form>
</FormProvider>
);
Hey @victorcesae , first of all thank you for your component i tried using it and its renders properly but the issue is when i use it inside a dialog component from shadcn ui library after selecting the file it automatically closes the dialog component .
i dont know why and because of what the issue is causing , can you help me regarding this ?
https://github.com/shadcn-ui/ui/assets/89771105/df3bb630-e4d8-4f65-815c-3b908598e38a
this is the code i am using and this code is inside the data table component
<Dialog>
<DialogTrigger asChild>
<Button className="w-full" variant="secondary">
Upload
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Upload Certificate</DialogTitle>
<DialogDescription>
{`Upload Certificate for ${candidate_name}`}
</DialogDescription>
</DialogHeader>
<Form {...methods}>
<form
className="flex flex-col items-center justify-center w-100 gap-2"
onSubmit={methods.handleSubmit(uploadCertificate)}
noValidate
autoComplete="off"
>
<FormField
control={methods.control}
name="file"
render={({ field }) => (
<FormItem className="w-full">
<FormControl>
<Dropzone
{...field}
dropMessage="Drop files or click here"
handleOnDrop={handleOnDrop}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{methods.watch("file") && (
<div className="flex items-center justify-center gap-3 p-4 relative">
<FileCheck2Icon className="h-4 w-4" />
<p className="text-sm font-medium">
{methods.watch("file")?.name}
</p>
</div>
)}
<Button type="submit">Save</Button>
</form>
</Form>
<Progress value={uploadCertificateProgress} />
<DialogFooter>
<Button
onClick={() => uploadCertificate}
className="w-full"
variant={"secondary"}
>
Upload
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
function handleOnDrop(acceptedFiles: FileList | null) { if (acceptedFiles && acceptedFiles.length > 0) { const allowedTypes = [ { name: "csv", types: ["text/csv"] }, { name: "excel", types: [ "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ], }, ]; const fileType = allowedTypes.find((allowedType) => allowedType.types.find((type) => type === acceptedFiles[0].type) ); if (!fileType) { methods.setValue("file", null); methods.setError("file", { message: "File type is not valid", type: "typeError", }); } else { methods.setValue("file", acceptedFiles[0]); methods.clearErrors("file"); } } else { methods.setValue("file", null); methods.setError("file", { message: "File is required", type: "typeError", }); } }
I don't see how can you get this error maybe your are calling the closeModal function, that i think is uploadCertificate inside the handleDrop function
I actually try your code but i don't get this error only when uptting (uploadCertificate or closeModal) functions inside handleDrop
thank you for your response , ill try making the changes u mentioned
function handleOnDrop(acceptedFiles: FileList | null) { if (acceptedFiles && acceptedFiles.length > 0) { const allowedTypes = [ { name: "csv", types: ["text/csv"] }, { name: "excel", types: [ "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ], }, ]; const fileType = allowedTypes.find((allowedType) => allowedType.types.find((type) => type === acceptedFiles[0].type) ); if (!fileType) { methods.setValue("file", null); methods.setError("file", { message: "File type is not valid", type: "typeError", }); } else { methods.setValue("file", acceptedFiles[0]); methods.clearErrors("file"); } } else { methods.setValue("file", null); methods.setError("file", { message: "File is required", type: "typeError", }); } }
I don't see how can you get this error maybe your are calling the closeModal function, that i think is uploadCertificate inside the handleDrop function
I actually try your code but i don't get this error only when uptting (uploadCertificate or closeModal) functions inside handleDrop
Hey! I think we all forgot about unbind URL URL.revokeObjectURL(objectURL);
Hey @victorcesae , first of all thank you for your component i tried using it and its renders properly but the issue is when i use it inside a dialog component from shadcn ui library after selecting the file it automatically closes the dialog component .
i dont know why and because of what the issue is causing , can you help me regarding this ?
dropzone.issue.mp4 this is the code i am using and this code is inside the data table component
<Dialog> <DialogTrigger asChild> <Button className="w-full" variant="secondary"> Upload </Button> </DialogTrigger> <DialogContent className="sm:max-w-[425px]"> <DialogHeader> <DialogTitle>Upload Certificate</DialogTitle> <DialogDescription> {`Upload Certificate for ${candidate_name}`} </DialogDescription> </DialogHeader> <Form {...methods}> <form className="flex flex-col items-center justify-center w-100 gap-2" onSubmit={methods.handleSubmit(uploadCertificate)} noValidate autoComplete="off" > <FormField control={methods.control} name="file" render={({ field }) => ( <FormItem className="w-full"> <FormControl> <Dropzone {...field} dropMessage="Drop files or click here" handleOnDrop={handleOnDrop} /> </FormControl> <FormMessage /> </FormItem> )} /> {methods.watch("file") && ( <div className="flex items-center justify-center gap-3 p-4 relative"> <FileCheck2Icon className="h-4 w-4" /> <p className="text-sm font-medium"> {methods.watch("file")?.name} </p> </div> )} <Button type="submit">Save</Button> </form> </Form> <Progress value={uploadCertificateProgress} /> <DialogFooter> <Button onClick={() => uploadCertificate} className="w-full" variant={"secondary"} > Upload </Button> </DialogFooter> </DialogContent> </Dialog>
you should use state for the opening and closing modal, with
<Dialog open={open} onOpenChange={setOpen}>
and closing the modal upon successful submit
My simple version
import React, { ChangeEvent, useState } from 'react';
import { Input } from '@/components/ui/input';
import { cn } from '@/utils';
interface DropzoneProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
onChange: (files: FileList | null) => void;
}
export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
({ className, onChange, children, ...props }, ref) => {
const [isDragging, setIsDragging] = useState(false);
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
onChange(e.dataTransfer.files);
};
const handleDragLeave = () => {
setIsDragging(false);
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange(e.target.files);
};
return (
<div
className={cn(
'border border-transparent p-4',
{
'rounded-md border-dashed border-primary': isDragging,
},
className,
)}
onDragOver={handleDragOver}
onDrop={handleDrop}
onDragLeave={handleDragLeave}
>
{children}
<Input
ref={ref}
type="file"
className="hidden"
onChange={handleChange}
{...props}
/>
</div>
);
},
);
Dropzone.displayName = 'Dropzone';
how to use it
const InitialTab = () => {
const set = useStore(store => store.set);
const inputRef = useRef<HTMLInputElement>(null);
const handleClickUpload = () => {
inputRef.current?.click();
};
const handleClickRecord = (e: React.MouseEvent) => {
e.stopPropagation();
set({ audioTab: AudioTab.RECORD });
};
const handleChange = (files: FileList | null) => {
const file = files?.[0];
if (file) set({ audio: { file, url: URL.createObjectURL(file) } });
};
return (
<div>
<Dropzone ref={inputRef} onChange={handleChange} multiple={false} accept="audio/*">
<div className="flex flex-col items-center justify-center gap-y-4">
<div className="rounded-full bg-primary-foreground p-2">
<UploadIcon className="text-primary" size={16} />
</div>
<div className="text-center">
<p className="text-lg font-semibold">Drag and drop an audio file</p>
<p className="text-sm font-medium text-muted-foreground">
Audio file up to 50mb
</p>
</div>
<div className="flex space-x-4">
<Button variant="outline" onClick={handleClickUpload}>
Upload Audio
</Button>
<Button variant="outline" onClick={handleClickRecord}>
Record Audio
</Button>
</div>
</div>
</Dropzone>
</div>
);
};
I created a simple version of the drop-zone component, published as shadcn-dropzone. ✨
yarn add shadcn-dropzone
Here is an example of how you would use it:
<Dropzone
onDrop={(acceptedFiles: File) => {
// Do something with the files
}}
>
{(dropzone: DropzoneState) => (
<>
{
dropzone.isDragAccept ? (
<div className='text-sm font-medium'>Drop your files here!</div>
) : (
<div className='flex items-center flex-col gap-1.5'>
<div className='flex items-center flex-row gap-0.5 text-sm font-medium'>
Upload files
</div>
</div>
)
}
<div className='text-xs text-gray-400 font-medium'>
{ dropzone.acceptedFiles.length } files uploaded so far.
</div>
</>
)}
</Dropzone>
Demo: https://diragb.github.io/shadcn-dropzone
This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you.