radix_leptos_primitives/components/
pull_to_refresh.rs1use leptos::*;
2use leptos::prelude::*;
3use wasm_bindgen::JsCast;
4
5#[component]
7pub fn PullToRefresh(
8 #[prop(into)]
10 children: Children,
11 #[prop(optional)]
13 class: Option<String>,
14 #[prop(optional)]
16 on_refresh: Option<Callback<()>>,
17 #[prop(optional, default = 80.0)]
19 refresh_threshold: f64,
20 #[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 let handle_mouse_down = move |event: web_sys::MouseEvent| {
31 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 set_translate_y.set(delta_y * 0.5); }
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 if let Some(callback) = on_refresh {
64 callback.run(());
65 }
66 }
67
68 set_is_pulling.set(false);
70 set_translate_y.set(0.0);
71 }
72 };
73
74 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 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 <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 <div class="refresh-content">
119 {children()}
120 </div>
121 </div>
122 }
123}
124
125#[component]
127pub fn RefreshButton(
128 #[prop(into)]
130 children: Children,
131 #[prop(optional)]
133 class: Option<String>,
134 #[prop(optional)]
136 on_click: Option<Callback<()>>,
137 #[prop(optional)]
139 is_refreshing: Option<Signal<bool>>,
140) -> impl IntoView {
141 let (is_pressed, set_is_pressed) = signal(false);
142
143 let handle_click = move |_| {
145 if let Some(callback) = on_click {
146 callback.run(());
147 }
148 };
149
150 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}