ข้ามไปเนื้อหา
WhereUI

ค้นหาในเอกสาร

ค้นหาชื่อ component หรือหน้าเอกสาร

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

proptypeค่าเริ่มต้นคำอธิบาย
acceptstring—ชนิดไฟล์ที่รับ ใช้รูปแบบเดียวกับ input accept เช่น ".pdf,.docx,image/*" — ใช้ทั้งกรองตอนเลือกและตรวจตอนลากมาวาง
maxSizeMBnumber5ขนาดสูงสุดต่อไฟล์ (MB) เกินแล้วแจ้งเป็นภาษาไทย
multiplebooleantrueเลือกได้หลายไฟล์ (false = เอาไฟล์ใหม่แทนไฟล์เดิม)
onChange(files: File[]) => void—ส่งรายการไฟล์ปัจจุบันกลับทุกครั้งที่เพิ่มหรือลบ
disabledboolean—ปิดการใช้งาน

ข้อควรรู้

  • 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 }