Yugen UI

FileUpload

FileUpload là vùng kéo thả kèm danh sách tệp: chọn bằng chuột, bàn phím, kéo thả hoặc dán; tự kiểm tra định dạng, dung lượng, số lượng.

Dùng <input type="file"> thật (ẩn trực quan) nên Tab tới được, Enter / Space mở hộp thoại chọn tệp và nhãn của Field trỏ đúng control. accept / maxSize / maxFiles / validate kiểm tra cả tệp kéo thả; tệp bị từ chối hiện thông báo tiếng Việt ngay dưới vùng thả (role="alert"). Mỗi tệp là một FileUploadItem: icon theo loại hoặc ảnh thu nhỏ (URL.createObjectURL, tự thu hồi khi gỡ), tên cắt bớt kèm title, dung lượng, ProgressBar khi đang tải và nút Thử lại / Xoá. Thêm / xoá tệp được đọc qua live region polite (useAnnounce). FileUpload không tự gửi tệp — bạn tải lên trong onFilesChange và cập nhật status / progress (chế độ điều khiển).
Có thể chọn nhiều tệp cùng lúc.

Import

Cài từ npm, chép mã nguồn vào dự án bằng @yugen-ui/cli hoặc shadcn CLI, hoặc import thẳng khi làm việc trong repo này.

TSX
// npm install yugen-uiimport { FileUpload, FileUploadItem, createFileEntry, isFileAccepted, type FileUploadEntry } from "yugen-ui";

Biến thể

Mỗi ô là một mẫu độc lập: hover để Copy, hoặc bấm Code để xem mã nguồn.

Tệp bắt đầu tải lên ngay khi được chọn.

Tải lên có tiến trình, lỗi và Thử lại

Chế độ điều khiển: files + onFilesChange; mỗi tệp mới được tải lên ngay, cập nhật progress (0 → 1) và status. Bật “Giả lập lỗi mạng” để thấy nút Thử lại.

Một ảnh, có thumbnail và dán từ clipboard

multiple mặc định false: chọn ảnh mới sẽ thay ảnh cũ. allowPaste nhận ảnh dán bằng Ctrl + V khi vùng thả đang focus.

Mặt trước và mặt sau, PDF hoặc ảnh.

Bắt buộc trong Field

Field gắn nhãn, hint và validationMessage; aria-invalid làm viền vùng thả chuyển sang màu lỗi. Bấm Gửi hồ sơ khi chưa chọn tệp.

Cách dùng

Nên
  • Ghi rõ định dạng và dung lượng cho phép (tự sinh từ accept / maxSize hoặc qua description) trước khi người dùng chọn tệp.
  • Hiện tiến trình cho tệp lớn và luôn có Thử lại khi tải lỗi — đừng bắt chọn lại từ đầu.
  • Bọc trong Field để có nhãn, hint và thông báo lỗi liên kết đúng với control.
Không nên
  • Không chỉ dựa vào accept phía client — server vẫn phải kiểm tra loại và dung lượng tệp.
  • Không dùng FileUpload cho một nút “Đính kèm” nhỏ trong thanh soạn tin — dùng PromptInput / Button + input ẩn.
  • Không tự động gửi form khi vừa chọn tệp; để người dùng xem lại danh sách trước.

Props

Prop · KiểuMặc địnhMô tả
acceptstringMặc định: —Như input gốc: ".pdf,image/*". Áp dụng cả với tệp kéo thả / dán.
multiplebooleanMặc định: falseChọn nhiều tệp. Tắt → tệp mới thay tệp cũ.
maxFilesnumberMặc định: —Tổng số tệp tối đa (khi multiple). Tệp vượt bị từ chối với lý do too-many-files.
maxSizenumberMặc định: —Dung lượng tối đa mỗi tệp (byte).
validate(file: File) => string | null | undefinedMặc định: —Kiểm tra riêng; trả về chuỗi lỗi để từ chối.
files / defaultFilesFileUploadEntry[]Mặc định: —Danh sách tệp (điều khiển / ban đầu). Entry: { id, name, size, type?, file?, previewUrl?, status?, progress?, error? }.
onFilesChange(files, { added, removed }) => voidMặc định: —Gọi khi thêm / xoá tệp.
onReject(rejections: FileUploadRejection[]) => voidMặc định: —Tệp bị từ chối: { file, code: file-invalid-type | file-too-large | too-many-files | custom, message }.
onRetry(entry: FileUploadEntry) => voidMặc định: —Có → tệp status="error" hiện nút Thử lại.
allowPastebooleanMặc định: falseNhận tệp dán bằng Ctrl + V khi vùng thả đang focus.
showThumbnailsbooleanMặc định: trueẢnh thu nhỏ cho tệp ảnh (tạo trong effect, thu hồi khi gỡ / unmount).
label / descriptionReactNodeMặc định: —Dòng chính / dòng phụ trong vùng thả.
disabled / requiredbooleanMặc định: —Field tự truyền. required → aria-required (không dùng required gốc vì input được xoá giá trị sau mỗi lần chọn).
refRef<HTMLInputElement>Mặc định: —Trỏ tới <input type="file"> — ref.current.click() mở hộp thoại, focus() cho react-hook-form.
FileUploadItemcomponent (<li>)Mặc định: —Dòng tệp dùng riêng được: name, size, type, file, previewUrl, status, progress, error, onRemove, onRetry.
createFileEntry / isFileAccepted(file, init?) => FileUploadEntry / (file, accept?) => booleanMặc định: —Tiện ích tạo entry và kiểm tra accept. Định dạng dung lượng dùng formatFileSize (dùng chung với PromptInput).

Messages

Chữ có sẵn của component: nhãn, aria-label, thông báo cho trình đọc màn hình.

Mặc định là tiếng Anh. Đổi qua prop messages của YugenProvider, theo namespace bên dưới; messagesVi chứa bản tiếng Việt. Xem Ngôn ngữ giao diện.

messages.fileUpload

Khoá · KiểuMặc định (tiếng Anh)messagesVi
dropzonestring

Dòng chính của vùng thả: dropzone + nút chữ browse (in đậm, màu link).

Mặc định (tiếng Anh)"Drag files here or"messagesVi"Kéo thả tệp vào đây hoặc"
browsestringMặc định (tiếng Anh)"browse"messagesVi"chọn tệp"
droppingstring

Dòng chính khi đang kéo tệp vào vùng thả.

Mặc định (tiếng Anh)"Drop files to upload"messagesVi"Thả tệp để tải lên"
selectedFilesstring

aria-label danh sách tệp đã chọn.

Mặc định (tiếng Anh)"Selected files"messagesVi"Tệp đã chọn"
typeImagestring

Tên nhóm MIME trong mô tả định dạng (image/* → "ảnh"…).

Mặc định (tiếng Anh)"images"messagesVi"ảnh"
typeVideostringMặc định (tiếng Anh)"videos"messagesVi"video"
typeAudiostringMặc định (tiếng Anh)"audio"messagesVi"âm thanh"
typeTextstringMặc định (tiếng Anh)"text"messagesVi"văn bản"
maxSizeHint(size: string) => string

Phần mô tả tự sinh; size là dung lượng đã định dạng.

Mặc định (tiếng Anh)(size) => `up to ${size} per file`messagesVi(size) => `tối đa ${size} mỗi tệp`
maxFilesHint(count: number) => stringMặc định (tiếng Anh)(count) => (count === 1 ? "up to 1 file" : `up to ${count} files`)messagesVi(count) => `tối đa ${count} tệp`
invalidType(name: string, accepted: string) => string

Lỗi từ chối tệp; accepted là danh sách định dạng, size là dung lượng đã định dạng.

Mặc định (tiếng Anh)(name, accepted) => `"${name}" isn't a supported file type (accepted: ${accepted}).`messagesVi(name, accepted) => `"${name}" không đúng định dạng (chỉ nhận ${accepted}).`
tooLarge(name: string, size: string) => stringMặc định (tiếng Anh)(name, size) => `"${name}" is larger than the ${size} limit.`messagesVi(name, size) => `"${name}" vượt quá dung lượng tối đa ${size}.`
tooMany(name: string, limit: number) => stringMặc định (tiếng Anh)(name, limit) => `You can select up to ${limit === 1 ? "1 file" : `${limit} files`}. "${name}" was skipped.`messagesVi(name, limit) => `Chỉ được chọn tối đa ${limit} tệp — đã bỏ qua "${name}".`
onlyOne(name: string) => string

Khi không bật multiple.

Mặc định (tiếng Anh)(name) => `You can select only 1 file. "${name}" was skipped.`messagesVi(name) => `Chỉ được chọn 1 tệp — đã bỏ qua "${name}".`
added(name: string) => string

Thông báo (aria-live) khi thêm / xoá tệp.

Mặc định (tiếng Anh)(name) => `Added ${name}.`messagesVi(name) => `Đã thêm tệp ${name}.`
addedCount(count: number) => stringMặc định (tiếng Anh)(count) => `Added ${count} files.`messagesVi(count) => `Đã thêm ${count} tệp.`
removed(name: string) => stringMặc định (tiếng Anh)(name) => `Removed ${name}.`messagesVi(name) => `Đã xoá tệp ${name}.`
uploadProgress(name: string) => string

FileUploadItem.

Mặc định (tiếng Anh)(name) => `Upload progress for ${name}`messagesVi(name) => `Tiến trình tải lên ${name}`
uploadingstringMặc định (tiếng Anh)"Uploading…"messagesVi"Đang tải lên…"
uploadingPercent(percent: number) => stringMặc định (tiếng Anh)(percent) => `Uploading… ${percent}%`messagesVi(percent) => `Đang tải lên… ${percent}%`
uploadedstringMặc định (tiếng Anh)"Uploaded"messagesVi"Đã tải lên"
failedstringMặc định (tiếng Anh)"Upload failed"messagesVi"Tải lên thất bại"
retry(name: string) => stringMặc định (tiếng Anh)(name) => `Retry uploading ${name}`messagesVi(name) => `Thử lại tải lên ${name}`
remove(name: string) => stringMặc định (tiếng Anh)(name) => `Remove ${name}`messagesVi(name) => `Xoá tệp ${name}`