impulse_thaw/back_top/
mod.rs

1use 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    /// The width of BackTop from the right side of the page.
13    #[prop(default=40.into(), into)]
14    right: Signal<i32>,
15    /// The height of BackTop from the bottom of the page.
16    #[prop(default=40.into(), into)]
17    bottom: Signal<i32>,
18    /// BackTop's trigger scroll top.
19    #[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}