FileUpload
WhereUIพื้นที่ลากไฟล์มาวางหรือกดเลือกไฟล์ จำกัดชนิดและขนาดไฟล์ได้ แสดงรายการไฟล์พร้อมปุ่มลบ และข้อความผิดพลาดภาษาไทย — ไม่มีการอัปโหลดจริง ไฟล์ถูกส่งกลับผ่าน onChange
ลากไฟล์มาวางที่นี่ หรือกดเลือกไฟล์
ขนาดไม่เกิน 5 MB ต่อไฟล์
ยังไม่ได้แนบเอกสาร
ติดตั้ง
Terminal
pnpm dlx shadcn@latest add @whereui/file-uploadต้องผูก registry และติดตั้ง @whereui/theme นำหน้าเสมอเมื่อติดตั้งครั้งแรก
การใช้งาน
import { FileUpload } from "@/components/ui/file-upload"const [files, setFiles] = React.useState<File[]>([])<FileUpload accept=".pdf,.docx" maxSizeMB={5} onChange={setFiles} />ตัวอย่างเพิ่มเติม
จำกัดชนิดไฟล์และขนาดที่รับ
รับเฉพาะ PDF ไม่เกิน 2 MB ไฟล์เดียว
ลากไฟล์มาวางที่นี่ หรือกดเลือกไฟล์
รับไฟล์ .pdf,application/pdf · ขนาดไม่เกิน 2 MB ต่อไฟล์
ลองลากไฟล์อื่นหรือไฟล์ใหญ่เกินมาวาง จะเห็นข้อความผิดพลาดภาษาไทย
Props
| prop | type | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| accept | string | — | ชนิดไฟล์ที่รับ ใช้รูปแบบเดียวกับ input accept เช่น ".pdf,.docx,image/*" — ใช้ทั้งกรองตอนเลือกและตรวจตอนลากมาวาง |
| maxSizeMB | number | 5 | ขนาดสูงสุดต่อไฟล์ (MB) เกินแล้วแจ้งเป็นภาษาไทย |
| multiple | boolean | true | เลือกได้หลายไฟล์ (false = เอาไฟล์ใหม่แทนไฟล์เดิม) |
| onChange | (files: File[]) => void | — | ส่งรายการไฟล์ปัจจุบันกลับทุกครั้งที่เพิ่มหรือลบ |
| disabled | boolean | — | ปิดการใช้งาน |
ข้อควรรู้
- component นี้ไม่อัปโหลดไฟล์ขึ้นเซิร์ฟเวอร์ — ส่ง File[] ให้ฟอร์มของคุณจัดการเอง
- พื้นที่วางไฟล์ใช้คีย์บอร์ดได้ (Tab เข้าแล้วกด Enter) และปุ่มลบแต่ละไฟล์มี aria-label ภาษาไทย
ซอร์สโค้ด
components/ui/file-upload.tsx
"use client"import * as React from "react"import { FileUpIcon, PaperclipIcon, XIcon } from "lucide-react"import { cn } from "cn"import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle,} from "@/components/ui/attachment"/** ตรวจว่าไฟล์ผ่านค่า accept (นามสกุล เช่น ".pdf" หรือ MIME เช่น "image/png", "image/*") */function matchesAccept(file: File, accept: string) { return accept .split(",") .map((token) => token.trim().toLowerCase()) .filter(Boolean) .some((token) => { if (token.startsWith(".")) { return file.name.toLowerCase().endsWith(token) } if (token.endsWith("/*")) { return file.type.toLowerCase().startsWith(token.slice(0, -1)) } return file.type.toLowerCase() === token })}/** ขนาดไฟล์เป็นข้อความไทย เช่น "1.2 MB", "340 KB" */function formatBytes(bytes: number) { const nf = new Intl.NumberFormat("th-TH", { maximumFractionDigits: 1 }) return bytes >= 1024 * 1024 ? `${nf.format(bytes / 1024 / 1024)} MB` : `${nf.format(Math.max(1, bytes / 1024))} KB`}function FileUpload({ accept, maxSizeMB = 5, multiple = true, disabled, onChange, className, ...props}: Omit<React.ComponentProps<"div">, "onChange"> & { /** ชนิดไฟล์ที่รับ เช่น ".pdf,.docx,image/*" */ accept?: string /** ขนาดสูงสุดต่อไฟล์ (MB) */ maxSizeMB?: number /** เลือกได้หลายไฟล์ */ multiple?: boolean /** ส่งรายการไฟล์ปัจจุบันกลับทุกครั้งที่เพิ่ม/ลบ (ไม่อัปโหลดจริง) */ onChange?: (files: File[]) => void disabled?: boolean}) { const [files, setFiles] = React.useState<File[]>([]) const [errors, setErrors] = React.useState<string[]>([]) const [dragging, setDragging] = React.useState(false) const inputRef = React.useRef<HTMLInputElement>(null) const emit = (next: File[]) => onChange?.(next) const add = (incoming: File[]) => { const accepted: File[] = [] const rejected: string[] = [] for (const file of incoming) { if (accept && !matchesAccept(file, accept)) { rejected.push(`ไฟล์ ${file.name} ไม่ใช่ชนิดที่รับได้`) } else if (file.size > maxSizeMB * 1024 * 1024) { rejected.push(`ไฟล์ ${file.name} ใหญ่เกิน ${maxSizeMB} MB`) } else { accepted.push(file) } } setErrors(rejected) if (!accepted.length) return const next = multiple ? [...files, ...accepted] : accepted.slice(0, 1) setFiles(next) emit(next) } const removeAt = (index: number) => { const next = files.filter((_, i) => i !== index) setFiles(next) setErrors([]) emit(next) } const openPicker = () => inputRef.current?.click() return ( <div data-slot="file-upload" className={cn("flex w-full min-w-0 flex-col gap-2", className)} {...props} > <div role="button" tabIndex={disabled ? -1 : 0} aria-label="เลือกหรือลากไฟล์มาวาง" aria-disabled={disabled} onClick={disabled ? undefined : openPicker} onKeyDown={(e) => { if (disabled) return if (e.key === "Enter" || e.key === " ") { e.preventDefault() openPicker() } }} onDragOver={(e) => { e.preventDefault() if (!disabled) setDragging(true) }} onDragLeave={() => setDragging(false)} onDrop={(e) => { e.preventDefault() setDragging(false) if (!disabled) add(Array.from(e.dataTransfer.files)) }} className={cn( "flex cursor-pointer flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-input bg-surface px-4 py-6 text-center transition-colors outline-none select-none hover:border-ring hover:bg-muted/50 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50", dragging && "border-ring bg-muted/50" )} > <FileUpIcon aria-hidden="true" className="size-5 text-muted-foreground [&_svg]:pointer-events-none" /> <p className="text-sm font-medium"> ลากไฟล์มาวางที่นี่ หรือกดเลือกไฟล์ </p> <p className="text-xs text-muted-foreground"> {accept ? `รับไฟล์ ${accept} · ` : ""} ขนาดไม่เกิน {maxSizeMB} MB ต่อไฟล์ </p> </div> <input ref={inputRef} type="file" accept={accept} multiple={multiple} disabled={disabled} className="hidden" onChange={(e) => { add(Array.from(e.target.files ?? [])) e.target.value = "" }} /> {errors.length > 0 && ( <ul className="flex flex-col gap-0.5 text-xs text-destructive"> {errors.map((message) => ( <li key={message}>{message}</li> ))} </ul> )} {files.length > 0 && ( <ul className="flex flex-col gap-2"> {files.map((file, index) => ( <li key={`${file.name}-${index}`}> <Attachment className="w-full"> <AttachmentMedia> <PaperclipIcon aria-hidden="true" /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>{file.name}</AttachmentTitle> <AttachmentDescription> {formatBytes(file.size)} </AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label={`ลบไฟล์ ${file.name}`} onClick={() => removeAt(index)} > <XIcon aria-hidden="true" /> </AttachmentAction> </AttachmentActions> </Attachment> </li> ))} </ul> )} </div> )}export { FileUpload }