radix_leptos_primitives/components/
file_upload.rs1use crate::utils::{merge_classes, generate_id};
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5use wasm_bindgen::JsCast;
6
7#[component]
9pub fn FileUpload(
10 #[prop(optional)] class: Option<String>,
11 #[prop(optional)] style: Option<String>,
12 #[prop(optional)] children: Option<Children>,
13 #[prop(optional)] multiple: Option<bool>,
14 #[prop(optional)] accept: Option<String>,
15 #[prop(optional)] max_size: Option<u64>,
16 #[prop(optional)] max_files: Option<usize>,
17 #[prop(optional)] disabled: Option<bool>,
18 #[prop(optional)] drag_drop_enabled: Option<bool>,
19 #[prop(optional)] on_files_select: Option<Callback<Vec<FileInfo>>>,
20 #[prop(optional)] on_upload_progress: Option<Callback<UploadProgress>>,
21 #[prop(optional)] on_upload_complete: Option<Callback<Vec<FileInfo>>>,
22 #[prop(optional)] on_upload_error: Option<Callback<String>>,
23) -> impl IntoView {
24 let _multiple = multiple.unwrap_or(false);
25 let _accept = accept.unwrap_or_default();
26 let _max_size = max_size.unwrap_or(10 * 1024 * 1024); let _max_files = max_files.unwrap_or(10);
28 let disabled = disabled.unwrap_or(false);
29 let drag_drop_enabled = drag_drop_enabled.unwrap_or(true);
30
31 let class = merge_classes(vec![
32 "file-upload",
33 if drag_drop_enabled {
34 "drag-drop-enabled"
35 } else {
36 "drag-drop-disabled"
37 },
38 class.as_deref().unwrap_or(""),
39 ]);
40
41 let handle_drop = move |event: web_sys::DragEvent| {
42 if !disabled && drag_drop_enabled {
43 event.prevent_default();
44 }
46 };
47
48 let handle_dragover = move |event: web_sys::DragEvent| {
49 if !disabled && drag_drop_enabled {
50 event.prevent_default();
51 }
52 };
53
54 view! {
55 <div
56 class=class
57 style=style
58 role="button"
59 aria-label="File upload area"
60 data-multiple=multiple
61 data-accept=_accept
62 data-max-size=_max_size
63 data-max-files=_max_files
64 on:drop=handle_drop
65 on:dragover=handle_dragover
66 tabindex="0"
67 >
68 {children.map(|c| c())}
69 </div>
70 }
71}
72
73#[component]
75pub fn FileUploadInput(
76 #[prop(optional)] class: Option<String>,
77 #[prop(optional)] style: Option<String>,
78 #[prop(optional)] multiple: Option<bool>,
79 #[prop(optional)] accept: Option<String>,
80 #[prop(optional)] disabled: Option<bool>,
81 #[prop(optional)] on_change: Option<Callback<Vec<FileInfo>>>,
82) -> impl IntoView {
83 let multiple = multiple.unwrap_or(false);
84 let accept = accept.unwrap_or_default();
85 let disabled = disabled.unwrap_or(false);
86
87 let class = merge_classes(vec!["file-upload-input", class.as_deref().unwrap_or("")]);
88
89 let handle_change = move |event: web_sys::Event| {
90 if let Some(_input) = event
91 .target()
92 .and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
93 {
94 if let Some(callback) = on_change {
96 callback.run(Vec::new());
97 }
98 }
99 };
100
101 view! {
102 <input
103 class=class
104 style=style
105 type="file"
106 multiple=multiple
107 accept=accept
108 disabled=disabled
109 on:change=handle_change
110 />
111 }
112}
113
114#[component]
116pub fn FileUploadDropZone(
117 #[prop(optional)] class: Option<String>,
118 #[prop(optional)] style: Option<String>,
119 #[prop(optional)] children: Option<Children>,
120 #[prop(optional)] disabled: Option<bool>,
121 #[prop(optional)] on_drop: Option<Callback<Vec<FileInfo>>>,
122 #[prop(optional)] on_drag_enter: Option<Callback<()>>,
123 #[prop(optional)] on_drag_leave: Option<Callback<()>>,
124) -> impl IntoView {
125 let disabled = disabled.unwrap_or(false);
126
127 let class = merge_classes(vec!["file-upload-drop-zone"]);
128
129 let handle_drag_enter = move |_| {
130 if !disabled {
131 if let Some(callback) = on_drag_enter {
132 callback.run(());
133 }
134 }
135 };
136
137 let handle_drag_leave = move |_| {
138 if !disabled {
139 if let Some(callback) = on_drag_leave {
140 callback.run(());
141 }
142 }
143 };
144
145 view! {
146 <div
147 class=class
148 style=style
149 role="button"
150 aria-label="File drop zone"
151 on:drop=move |event: web_sys::DragEvent| {
152 if !disabled {
153 event.prevent_default();
154 if let Some(callback) = on_drop {
155 callback.run(vec![]);
157 }
158 }
159 }
160 on:dragenter=handle_drag_enter
161 on:dragleave=handle_drag_leave
162 tabindex="0"
163 >
164 {children.map(|c| c())}
165 </div>
166 }
167}
168
169#[component]
171pub fn FileUploadList(
172 #[prop(optional)] class: Option<String>,
173 #[prop(optional)] style: Option<String>,
174 #[prop(optional)] children: Option<Children>,
175 #[prop(optional)] files: Option<Vec<FileInfo>>,
176 #[prop(optional)] on_file_remove: Option<Callback<String>>,
177) -> impl IntoView {
178 let files = files.unwrap_or_default();
179
180 let class = merge_classes(vec!["file-upload-list", class.as_deref().unwrap_or("")]);
181
182 view! {
183 <div
184 class=class
185 style=style
186 role="list"
187 aria-label="Uploaded files"
188 >
189 {children.map(|c| c())}
190 </div>
191 }
192}
193
194#[component]
196pub fn FileUploadItem(
197 #[prop(optional)] class: Option<String>,
198 #[prop(optional)] style: Option<String>,
199 #[prop(optional)] children: Option<Children>,
200 #[prop(optional)] file: Option<FileInfo>,
201 #[prop(optional)] on_remove: Option<Callback<String>>,
202) -> impl IntoView {
203 let file = file.unwrap_or_default();
204
205 let class = merge_classes(vec![
206 "file-upload-item",
207 &file.status.to_class(),
208 class.as_deref().unwrap_or(""),
209 ]);
210
211 let file_id = file.id.clone();
212 let handle_remove = move |_: web_sys::MouseEvent| {
213 if let Some(callback) = on_remove {
214 callback.run(file_id.clone());
215 }
216 };
217
218 view! {
219 <div
220 class=class
221 style=style
222 role="listitem"
223 aria-label=format!("File: {}", file.name)
224 data-file-id=file.id
225 data-file-name=file.name
226 data-file-size=file.size
227 data-file-type=file.file_type
228 >
229 {children.map(|c| c())}
230 </div>
231 }
232}
233
234#[derive(Debug, Clone, PartialEq)]
236pub struct FileInfo {
237 pub id: String,
238 pub name: String,
239 pub size: u64,
240 pub file_type: String,
241 pub status: FileStatus,
242 pub progress: f64,
243 pub error_message: Option<String>,
244}
245
246impl Default for FileInfo {
247 fn default() -> Self {
248 Self {
249 id: "file".to_string(),
250 name: "file.txt".to_string(),
251 size: 0,
252 file_type: "text/plain".to_string(),
253 status: FileStatus::Pending,
254 progress: 0.0,
255 error_message: None,
256 }
257 }
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
262pub enum FileStatus {
263 #[default]
264 Pending,
265 Uploading,
266 Completed,
267 Error,
268 Cancelled,
269}
270
271impl FileStatus {
272 pub fn to_class(&self) -> &'static str {
273 match self {
274 FileStatus::Pending => "status-pending",
275 FileStatus::Uploading => "status-uploading",
276 FileStatus::Completed => "status-completed",
277 FileStatus::Error => "status-error",
278 FileStatus::Cancelled => "status-cancelled",
279 }
280 }
281}
282
283#[derive(Debug, Clone, PartialEq)]
285pub struct UploadProgress {
286 pub file_id: String,
287 pub progress: f64,
288 pub bytes_uploaded: u64,
289 pub total_bytes: u64,
290}
291
292impl Default for UploadProgress {
293 fn default() -> Self {
294 Self {
295 file_id: "file".to_string(),
296 progress: 0.0,
297 bytes_uploaded: 0,
298 total_bytes: 0,
299 }
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use proptest::prelude::*;
306 use wasm_bindgen_test::*;
307
308 wasm_bindgen_test_configure!(run_in_browser);
309
310 #[test]
312 fn test_file_upload_creation() {}
313 #[test]
314 fn test_file_upload_with_class() {}
315 #[test]
316 fn test_file_upload_with_style() {}
317 #[test]
318 fn test_file_upload_multiple() {}
319 #[test]
320 fn test_file_upload_accept() {}
321 #[test]
322 fn test_file_upload_max_size() {}
323 #[test]
324 fn test_file_upload_max_files() {}
325 #[test]
326 fn test_file_uploaddisabled() {}
327 #[test]
328 fn test_file_upload_drag_drop_enabled() {}
329 #[test]
330 fn test_file_upload_on_files_select() {}
331 #[test]
332 fn test_file_upload_on_upload_progress() {}
333 #[test]
334 fn test_file_upload_on_upload_complete() {}
335 #[test]
336 fn test_file_upload_on_upload_error() {}
337
338 #[test]
340 fn test_file_upload_input_creation() {}
341 #[test]
342 fn test_file_upload_input_with_class() {}
343 #[test]
344 fn test_file_upload_input_multiple() {}
345 #[test]
346 fn test_file_upload_input_accept() {}
347 #[test]
348 fn test_file_upload_inputdisabled() {}
349 #[test]
350 fn test_file_upload_input_on_change() {}
351
352 #[test]
354 fn test_file_upload_drop_zone_creation() {}
355 #[test]
356 fn test_file_upload_drop_zone_with_class() {}
357 #[test]
358 fn test_file_upload_drop_zonedisabled() {}
359 #[test]
360 fn test_file_upload_drop_zone_on_drop() {}
361 #[test]
362 fn test_file_upload_drop_zone_on_drag_enter() {}
363 #[test]
364 fn test_file_upload_drop_zone_on_drag_leave() {}
365
366 #[test]
368 fn test_file_upload_list_creation() {}
369 #[test]
370 fn test_file_upload_list_with_class() {}
371 #[test]
372 fn test_file_upload_list_files() {}
373 #[test]
374 fn test_file_upload_list_on_file_remove() {}
375
376 #[test]
378 fn test_file_upload_item_creation() {}
379 #[test]
380 fn test_file_upload_item_with_class() {}
381 #[test]
382 fn test_file_upload_item_file() {}
383 #[test]
384 fn test_file_upload_item_on_remove() {}
385
386 #[test]
388 fn test_file_info_default() {}
389 #[test]
390 fn test_file_info_creation() {}
391
392 #[test]
394 fn test_file_status_default() {}
395 #[test]
396 fn test_file_status_pending() {}
397 #[test]
398 fn test_file_status_uploading() {}
399 #[test]
400 fn test_file_status_completed() {}
401 #[test]
402 fn test_file_status_error() {}
403 #[test]
404 fn test_file_status_cancelled() {}
405
406 #[test]
408 fn test_upload_progress_default() {}
409 #[test]
410 fn test_upload_progress_creation() {}
411
412 #[test]
414 fn test_merge_classes_empty() {}
415 #[test]
416 fn test_merge_classes_single() {}
417 #[test]
418 fn test_merge_classes_multiple() {}
419 #[test]
420 fn test_merge_classes_with_empty() {}
421
422 #[test]
424 fn test_file_upload_property_based() {
425 proptest!(|(____class in ".*", __style in ".*")| {
426
427 });
428 }
429
430 #[test]
431 fn test_file_upload_file_validation() {
432 proptest!(|(______file_count in 0..20usize)| {
433
434 });
435 }
436
437 #[test]
438 fn test_file_upload_size_validation() {
439 proptest!(|(____size in 0..1000000000u64)| {
440
441 });
442 }
443
444 #[test]
446 fn test_file_upload_user_interaction() {}
447 #[test]
448 fn test_file_upload_accessibility() {}
449 #[test]
450 fn test_file_upload_drag_drop_workflow() {}
451 #[test]
452 fn test_file_upload_progress_tracking() {}
453 #[test]
454 fn test_file_upload_error_handling() {}
455
456 #[test]
458 fn test_file_upload_large_files() {}
459 #[test]
460 fn test_file_upload_multiple_files() {}
461 #[test]
462 fn test_file_upload_render_performance() {}
463 #[test]
464 fn test_file_upload_memory_usage() {}
465}