radix_leptos_primitives/components/
file_upload.rs

1use crate::utils::{merge_classes, generate_id};
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5use wasm_bindgen::JsCast;
6
7/// File Upload component - File upload with drag & drop support
8#[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); // 10MB default
27    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            // File handling logic would be implemented here
45        }
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/// File Upload Input component
74#[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            // File processing logic would be implemented here
95            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/// File Upload Drop Zone component
115#[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                        // File handling logic would be implemented here
156                        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/// File Upload List component
170#[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/// File Upload Item component
195#[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/// File Info structure
235#[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/// File Status enum
261#[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/// Upload Progress structure
284#[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    // Unit Tests
311    #[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    // File Upload Input tests
339    #[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    // File Upload Drop Zone tests
353    #[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    // File Upload List tests
367    #[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    // File Upload Item tests
377    #[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    // File Info tests
387    #[test]
388    fn test_file_info_default() {}
389    #[test]
390    fn test_file_info_creation() {}
391
392    // File Status tests
393    #[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    // Upload Progress tests
407    #[test]
408    fn test_upload_progress_default() {}
409    #[test]
410    fn test_upload_progress_creation() {}
411
412    // Helper function tests
413    #[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    // Property-based Tests
423    #[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    // Integration Tests
445    #[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    // Performance Tests
457    #[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}