radix_leptos_primitives/components/
pull_to_refresh.rs

1use leptos::*;
2use leptos::prelude::*;
3use wasm_bindgen::JsCast;
4
5/// Pull-to-refresh component for mobile devices
6#[component]
7pub fn PullToRefresh(
8    /// Content to wrap with pull-to-refresh
9    #[prop(into)]
10    children: Children,
11    /// CSS classes to apply
12    #[prop(optional)]
13    class: Option<String>,
14    /// Callback when refresh is triggered
15    #[prop(optional)]
16    on_refresh: Option<Callback<()>>,
17    /// Threshold distance to trigger refresh (in pixels)
18    #[prop(optional, default = 80.0)]
19    refresh_threshold: f64,
20    /// Whether refresh is currently in progress
21    #[prop(optional)]
22    is_refreshing: Option<Signal<bool>>,
23) -> impl IntoView {
24    let (start_y, set_start_y) = signal(0.0);
25    let (current_y, set_current_y) = signal(0.0);
26    let (is_pulling, set_is_pulling) = signal(false);
27    let (translate_y, set_translate_y) = signal(0.0);
28
29    // Handle mouse events for desktop compatibility
30    let handle_mouse_down = move |event: web_sys::MouseEvent| {
31        // Only start pulling if we're at the top of the scrollable area
32        if let Some(element) = event.target() {
33            if let Ok(html_element) = element.dyn_into::<web_sys::Element>() {
34                if html_element.scroll_top() == 0 {
35                    set_start_y.set(event.client_y() as f64);
36                    set_current_y.set(event.client_y() as f64);
37                    set_is_pulling.set(true);
38                    set_translate_y.set(0.0);
39                }
40            }
41        }
42    };
43
44    let handle_mouse_move = move |event: web_sys::MouseEvent| {
45        if is_pulling.get() {
46            let new_y = event.client_y() as f64;
47            set_current_y.set(new_y);
48            
49            let delta_y = new_y - start_y.get();
50            if delta_y > 0.0 {
51                // Only allow downward movement
52                set_translate_y.set(delta_y * 0.5); // Add resistance
53            }
54        }
55    };
56
57    let handle_mouse_up = move |_| {
58        if is_pulling.get() {
59            let delta_y = current_y.get() - start_y.get();
60            
61            if delta_y >= refresh_threshold {
62                // Trigger refresh
63                if let Some(callback) = on_refresh {
64                    callback.run(());
65                }
66            }
67            
68            // Reset state
69            set_is_pulling.set(false);
70            set_translate_y.set(0.0);
71        }
72    };
73
74    // Handle wheel events for scroll detection
75    let handle_wheel = move |event: web_sys::WheelEvent| {
76        if let Some(element) = event.target() {
77            if let Ok(html_element) = element.dyn_into::<web_sys::Element>() {
78                if html_element.scroll_top() == 0 && event.delta_y() < 0.0 {
79                    // At top and scrolling up, allow pull gesture
80                    set_is_pulling.set(true);
81                }
82            }
83        }
84    };
85
86    let is_refreshing_state = is_refreshing.as_ref().map(|r| r.get()).unwrap_or(false);
87
88    view! {
89        <div
90            class={format!("pull-to-refresh {}", class.unwrap_or_default())}
91            style={format!(
92                "transform: translateY({}px); transition: {};",
93                translate_y.get(),
94                if is_pulling.get() { "none" } else { "transform 0.3s ease-out" }
95            )}
96            on:mousedown=handle_mouse_down
97            on:mousemove=handle_mouse_move
98            on:mouseup=handle_mouse_up
99            on:wheel=handle_wheel
100        >
101            // Refresh indicator
102            <div
103                class="refresh-indicator"
104                style={format!(
105                    "opacity: {}; transform: scale({});",
106                    if is_pulling.get() { (translate_y.get() / refresh_threshold).min(1.0) } else { 0.0 },
107                    if is_pulling.get() { 0.5 + (translate_y.get() / refresh_threshold * 0.5).min(0.5) } else { 0.0 }
108                )}
109            >
110                {if is_refreshing_state {
111                    "🔄 Refreshing..."
112                } else {
113                    "⬇ Pull to refresh"
114                }}
115            </div>
116            
117            // Main content
118            <div class="refresh-content">
119                {children()}
120            </div>
121        </div>
122    }
123}
124
125/// Simple refresh button component
126#[component]
127pub fn RefreshButton(
128    /// Button content
129    #[prop(into)]
130    children: Children,
131    /// CSS classes to apply
132    #[prop(optional)]
133    class: Option<String>,
134    /// Click event handler
135    #[prop(optional)]
136    on_click: Option<Callback<()>>,
137    /// Whether refresh is currently in progress
138    #[prop(optional)]
139    is_refreshing: Option<Signal<bool>>,
140) -> impl IntoView {
141    let (is_pressed, set_is_pressed) = signal(false);
142
143    // Handle click events
144    let handle_click = move |_| {
145        if let Some(callback) = on_click {
146            callback.run(());
147        }
148    };
149
150    // Handle mouse events for visual feedback
151    let handle_mouse_down = move |_| {
152        set_is_pressed.set(true);
153    };
154
155    let handle_mouse_up = move |_| {
156        set_is_pressed.set(false);
157    };
158
159    let handle_mouse_leave = move |_| {
160        set_is_pressed.set(false);
161    };
162
163    let is_refreshing_state = is_refreshing.as_ref().map(|r| r.get()).unwrap_or(false);
164    
165    view! {
166        <button
167            class={format!(
168                "refresh-button {} {} {}",
169                if is_refreshing_state { "refreshing" } else { "" },
170                if is_pressed.get() { "pressed" } else { "" },
171                class.unwrap_or_default()
172            )}
173            disabled=is_refreshing_state
174            on:click=handle_click
175            on:mousedown=handle_mouse_down
176            on:mouseup=handle_mouse_up
177            on:mouseleave=handle_mouse_leave
178        >
179            {children()}
180        </button>
181    }
182}