impulse_thaw/back_top/
mod.rs1use crate::{ConfigInjection, Icon};
2use leptos::{either::Either, ev, html, prelude::*};
3use thaw_components::{CSSTransition, Teleport};
4use thaw_utils::{
5 add_event_listener, class_list, get_scroll_parent_element, mount_style, BoxCallback,
6 EventListenerHandle,
7};
8
9#[component]
10pub fn BackTop(
11 #[prop(optional, into)] class: MaybeProp<String>,
12 #[prop(default=40.into(), into)]
14 right: Signal<i32>,
15 #[prop(default=40.into(), into)]
17 bottom: Signal<i32>,
18 #[prop(default=180.into(), into)]
20 visibility_height: Signal<i32>,
21 #[prop(optional)] children: Option<Children>,
22) -> impl IntoView {
23 mount_style("back-top", include_str!("./back-top.css"));
24 let config_provider = ConfigInjection::expect_context();
25 let placeholder_ref = NodeRef::<html::Div>::new();
26 let is_show_back_top = RwSignal::new(false);
27 let scroll_top = RwSignal::new(0);
28
29 Effect::new(move |prev: Option<()>| {
30 scroll_top.track();
31 if prev.is_some() {
32 is_show_back_top.set(scroll_top.get() > visibility_height.get_untracked());
33 }
34 });
35
36 let scroll_to_top = StoredValue::new(None::<BoxCallback>);
37 let scroll_handle = StoredValue::new(None::<EventListenerHandle>);
38
39 Effect::new(move |_| {
40 let Some(placeholder_el) = placeholder_ref.get() else {
41 return;
42 };
43
44 request_animation_frame(move || {
45 let scroll_el = get_scroll_parent_element(&placeholder_el)
46 .unwrap_or_else(|| document().document_element().unwrap());
47
48 {
49 let scroll_el = send_wrapper::SendWrapper::new(scroll_el.clone());
50 scroll_to_top.set_value(Some(BoxCallback::new(move || {
51 let options = web_sys::ScrollToOptions::new();
52 options.set_top(0.0);
53 options.set_behavior(web_sys::ScrollBehavior::Smooth);
54 scroll_el.scroll_to_with_scroll_to_options(&options);
55 })));
56 }
57
58 let handle = add_event_listener(scroll_el.clone(), ev::scroll, move |_| {
59 scroll_top.set(scroll_el.scroll_top());
60 });
61 scroll_handle.set_value(Some(handle));
62 });
63 });
64
65 on_cleanup(move || {
66 scroll_handle.update_value(|handle| {
67 if let Some(handle) = handle.take() {
68 handle.remove();
69 }
70 });
71 });
72
73 let on_click = move |_| {
74 scroll_to_top.with_value(|scroll_to_top| {
75 if let Some(scroll_to_top) = scroll_to_top {
76 scroll_to_top();
77 }
78 });
79 };
80
81 view! {
82 <div style="display: none" class="thaw-back-top-placeholder" node_ref=placeholder_ref>
83 <Teleport immediate=is_show_back_top>
84 <CSSTransition
85 name="fade-in-scale-up-transition"
86 appear=is_show_back_top.get_untracked()
87 show=is_show_back_top
88 >
89 <div
90 class=class_list!["thaw-config-provider thaw-back-top", class]
91 data-thaw-id=config_provider.id()
92 style=move || {
93 format!("right: {}px; bottom: {}px", right.get(), bottom.get())
94 }
95
96 on:click=on_click
97 >
98 {if let Some(children) = children {
99 Either::Left(children())
100 } else {
101 Either::Right(
102 view! { <Icon icon=icondata_ai::AiVerticalAlignTopOutlined /> },
103 )
104 }}
105 </div>
106 </CSSTransition>
107 </Teleport>
108 </div>
109 }
110}