Skip to main content

orbital_base_components/upload/
base.rs

1use leptos::{ev, html, prelude::*};
2
3use crate::{overlay::dom_events::add_event_listener, Handler, UploadFileList};
4
5/// Headless file upload — hidden input, trigger slot, and drag/drop forwarding.
6#[component]
7pub fn BaseUpload(
8    #[prop(optional, into)] class: MaybeProp<String>,
9    #[prop(optional, into)] id: MaybeProp<String>,
10    #[prop(optional, into)] name: MaybeProp<String>,
11    #[prop(optional, into)] accept: Signal<String>,
12    #[prop(optional, into)] multiple: Signal<bool>,
13    #[prop(into)] on_change: Handler<UploadFileList>,
14    children: Children,
15) -> impl IntoView {
16    let input_ref = NodeRef::<html::Input>::new();
17    let trigger_ref = NodeRef::<html::Div>::new();
18    let is_trigger_dragover = RwSignal::new(false);
19
20    Effect::new(move |_| {
21        let Some(trigger_el) = trigger_ref.get() else {
22            return;
23        };
24        let handle = add_event_listener(trigger_el, ev::click, move |_| {
25            if let Some(input_el) = input_ref.get_untracked() {
26                input_el.click();
27            }
28        });
29        on_cleanup(move || handle.remove());
30    });
31
32    let dispatch_files = {
33        let on_change = on_change.clone();
34        move |files: UploadFileList| {
35            on_change.run(files);
36        }
37    };
38
39    let on_input_change = {
40        let dispatch_files = dispatch_files.clone();
41        move |_| {
42            if let Some(input_el) = input_ref.get_untracked() {
43                if let Some(files) = input_el.files() {
44                    dispatch_files(files);
45                }
46                input_el.set_value("");
47            }
48        }
49    };
50
51    let on_trigger_drop = {
52        let dispatch_files = dispatch_files.clone();
53        move |event: ev::DragEvent| {
54            event.prevent_default();
55            if let Some(data) = event.data_transfer() {
56                if let Some(files) = data.files() {
57                    dispatch_files(files);
58                }
59            }
60            is_trigger_dragover.set(false);
61        }
62    };
63
64    let root_class = move || {
65        let mut parts = vec!["orbital-upload".to_string()];
66        if is_trigger_dragover.get() {
67            parts.push("orbital-upload--drag-over".to_string());
68        }
69        if let Some(extra) = class.get() {
70            let extra = extra.trim();
71            if !extra.is_empty() {
72                parts.push(extra.to_string());
73            }
74        }
75        parts.join(" ")
76    };
77
78    view! {
79        <div class=root_class>
80            <input
81                class="orbital-upload__input"
82                data-testid="upload-input"
83                aria-hidden="true"
84                tabindex="-1"
85                id=move || id.get()
86                name=move || name.get()
87                node_ref=input_ref
88                type="file"
89                accept=move || accept.get()
90                multiple=move || multiple.get()
91                on:change=on_input_change
92            />
93            <div
94                class="orbital-upload__trigger"
95                node_ref=trigger_ref
96                on:drop=on_trigger_drop
97                on:dragover=move |event: ev::DragEvent| {
98                    event.prevent_default();
99                    is_trigger_dragover.set(true);
100                }
101                on:dragenter=move |event: ev::DragEvent| event.prevent_default()
102                on:dragleave=move |event: ev::DragEvent| {
103                    event.prevent_default();
104                    is_trigger_dragover.set(false);
105                }
106            >
107                {children()}
108            </div>
109        </div>
110    }
111}