Skip to content

Commit 1b0f664

Browse files
authored
Feat/back top (#169)
* feat: thaw_utils adds get_scroll_parent * feat: add BackTop * feat: BackTop scroll
1 parent 95abde2 commit 1b0f664

File tree

15 files changed

+351
-82
lines changed

15 files changed

+351
-82
lines changed

demo/src/app.rs

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
4949
<Route path="/alert" view=AlertMdPage/>
5050
<Route path="/auto-complete" view=AutoCompleteMdPage/>
5151
<Route path="/avatar" view=AvatarMdPage/>
52+
<Route path="/back-top" view=BackTopMdPage/>
5253
<Route path="/badge" view=BadgeMdPage/>
5354
<Route path="/breadcrumb" view=BreadcrumbMdPage/>
5455
<Route path="/button" view=ButtonMdPage/>

demo/src/pages/components.rs

+4
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
208208
MenuGroupOption {
209209
label: "Navigation Components".into(),
210210
children: vec![
211+
MenuItemOption {
212+
value: "back-top".into(),
213+
label: "Back Top".into(),
214+
},
211215
MenuItemOption {
212216
value: "breadcrumb".into(),
213217
label: "Breadcrumb".into(),

demo_markdown/docs/back_top/mod.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Back Top
2+
3+
BackTop will find its first scrollable ascendant element and listen scroll event on it.
4+
5+
```rust demo
6+
view! {
7+
<BackTop />
8+
}
9+
```
10+
11+
### Visibility height
12+
13+
```rust demo
14+
view! {
15+
<BackTop bottom=100 visibility_height=280>
16+
<div style="width: 200px; text-align: center;">
17+
"Visibility Height: 280px"
18+
</div>
19+
</BackTop>
20+
}
21+
```
22+
23+
### Change position
24+
25+
```rust demo
26+
view! {
27+
<BackTop right=40 bottom=160>
28+
<div style="width: 200px; text-align: center;">
29+
"Change Position"
30+
</div>
31+
</BackTop>
32+
}
33+
```
34+
35+
### BackTop Props
36+
37+
| Name | Type | Default | Description |
38+
| --- | --- | --- | --- |
39+
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the back top element. |
40+
| right | `MaybeSignal<i32>` | `40` | The width of BackTop from the right side of the page. |
41+
| bottom | `MaybeSignal<i32>` | `40` | The height of BackTop from the bottom of the page. |
42+
| bottom | `MaybeSignal<i32>` | `180` | BackTop's trigger scroll top. |
43+
| children | `Option<Children>` | `None` | BackTop's content. |
44+
45+
<div style="height: 600px">
46+
</div>

demo_markdown/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
3131
"AlertMdPage" => "../docs/alert/mod.md",
3232
"AutoCompleteMdPage" => "../docs/auto_complete/mod.md",
3333
"AvatarMdPage" => "../docs/avatar/mod.md",
34+
"BackTopMdPage" => "../docs/back_top/mod.md",
3435
"BadgeMdPage" => "../docs/badge/mod.md",
3536
"BreadcrumbMdPage" => "../docs/breadcrumb/mod.md",
3637
"ButtonMdPage" => "../docs/button/mod.md",

thaw/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ web-sys = { version = "0.3.69", features = [
2121
"File",
2222
"FileList",
2323
"DataTransfer",
24+
"ScrollToOptions",
25+
"ScrollBehavior",
2426
] }
2527
wasm-bindgen = "0.2.92"
2628
icondata_core = "0.1.0"

thaw/src/back_top/back-top.css

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
.thaw-back-top {
2+
position: fixed;
3+
cursor: pointer;
4+
display: flex;
5+
align-items: center;
6+
justify-content: center;
7+
transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
8+
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
9+
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
10+
border-radius: 22px;
11+
height: 44px;
12+
min-width: 44px;
13+
box-shadow: 0 2px 8px 0px rgba(0, 0, 0, 0.12);
14+
background-color: var(--thaw-background-color);
15+
}
16+
17+
.thaw-back-top.fade-in-scale-up-transition-leave-active {
18+
transform-origin: inherit;
19+
transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1),
20+
transform 0.2s cubic-bezier(0.4, 0, 1, 1);
21+
}
22+
23+
.thaw-back-top.fade-in-scale-up-transition-enter-active {
24+
transform-origin: inherit;
25+
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1),
26+
transform 0.2s cubic-bezier(0, 0, 0.2, 1);
27+
}
28+
29+
.thaw-back-top.fade-in-scale-up-transition-enter-from,
30+
.thaw-back-top.fade-in-scale-up-transition-leave-to {
31+
opacity: 0;
32+
transform: scale(0.9);
33+
}
34+
35+
.thaw-back-top.fade-in-scale-up-transition-leave-from,
36+
.thaw-back-top.fade-in-scale-up-transition-enter-to {
37+
opacity: 1;
38+
transform: scale(1);
39+
}
40+
41+
.thaw-back-top > svg {
42+
font-size: 24px;
43+
transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
44+
}
45+
46+
.thaw-back-top:hover {
47+
box-shadow: 0 2px 12px 0px #0000002e;
48+
}
49+
50+
.thaw-back-top:hover > svg {
51+
color: var(--thaw-icon-color-hover);
52+
}
53+
54+
.thaw-back-top:active {
55+
box-shadow: 0 2px 12px 0px #0000002e;
56+
}
57+
58+
.thaw-back-top:active svg {
59+
color: var(--thaw-icon-color-active);
60+
}

thaw/src/back_top/mod.rs

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
mod theme;
2+
3+
pub use theme::BackTopTheme;
4+
5+
use crate::{use_theme, Icon, Theme};
6+
use leptos::{html::ToHtmlElement, *};
7+
use thaw_components::{CSSTransition, Fallback, OptionComp, Teleport};
8+
use thaw_utils::{
9+
add_event_listener, class_list, get_scroll_parent, mount_style, EventListenerHandle, OptionalProp,
10+
};
11+
12+
#[component]
13+
pub fn BackTop(
14+
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
15+
#[prop(default=40.into(), into)] right: MaybeSignal<i32>,
16+
#[prop(default=40.into(), into)] bottom: MaybeSignal<i32>,
17+
#[prop(default=180.into(), into)] visibility_height: MaybeSignal<i32>,
18+
#[prop(optional)] children: Option<Children>,
19+
) -> impl IntoView {
20+
mount_style("back-top", include_str!("./back-top.css"));
21+
let theme = use_theme(Theme::light);
22+
let style = Memo::new(move |_| {
23+
let mut style = String::new();
24+
style.push_str(&format!("right: {}px;", right.get_untracked()));
25+
style.push_str(&format!("bottom: {}px;", bottom.get_untracked()));
26+
theme.with(|theme| {
27+
style.push_str(&format!(
28+
"--thaw-icon-color-hover: {};",
29+
theme.common.color_primary_hover
30+
));
31+
style.push_str(&format!(
32+
"--thaw-icon-color-active: {};",
33+
theme.common.color_primary_active
34+
));
35+
style.push_str(&format!(
36+
"--thaw-background-color: {};",
37+
theme.back_top.background_color
38+
));
39+
});
40+
style
41+
});
42+
let placeholder_ref = NodeRef::<html::Div>::new();
43+
let back_top_ref = NodeRef::<html::Div>::new();
44+
let is_show_back_top = RwSignal::new(false);
45+
let scroll_top = RwSignal::new(0);
46+
47+
let _ = watch(
48+
move || scroll_top.get(),
49+
move |scroll_top, _, _| {
50+
is_show_back_top.set(scroll_top > &visibility_height.get());
51+
},
52+
false,
53+
);
54+
55+
let scroll_to_top = StoredValue::new(None::<Callback<()>>);
56+
let scroll_handle = StoredValue::new(None::<EventListenerHandle>);
57+
58+
placeholder_ref.on_load(move |el| {
59+
request_animation_frame(move || {
60+
let scroll_el = get_scroll_parent(&el.into_any())
61+
.unwrap_or_else(|| document().document_element().unwrap().to_leptos_element());
62+
63+
{
64+
let scroll_el = scroll_el.clone();
65+
scroll_to_top.set_value(Some(Callback::new(move |_| {
66+
scroll_el.scroll_to_with_scroll_to_options(
67+
web_sys::ScrollToOptions::new()
68+
.top(0.0)
69+
.behavior(web_sys::ScrollBehavior::Smooth),
70+
);
71+
})));
72+
}
73+
74+
let handle = add_event_listener(scroll_el.clone(), ev::scroll, move |_| {
75+
scroll_top.set(scroll_el.scroll_top());
76+
});
77+
scroll_handle.set_value(Some(handle));
78+
});
79+
});
80+
81+
on_cleanup(move || {
82+
scroll_handle.update_value(|handle| {
83+
if let Some(handle) = handle.take() {
84+
handle.remove();
85+
}
86+
});
87+
});
88+
89+
let on_click = move |_| {
90+
scroll_to_top.with_value(|scroll_to_top| {
91+
if let Some(scroll_to_top) = scroll_to_top {
92+
scroll_to_top.call(());
93+
}
94+
});
95+
};
96+
97+
view! {
98+
<div style="display: none" class="thaw-back-top-placeholder" ref=placeholder_ref>
99+
<Teleport immediate=is_show_back_top>
100+
<CSSTransition
101+
node_ref=back_top_ref
102+
name="fade-in-scale-up-transition"
103+
appear=is_show_back_top.get_untracked()
104+
show=is_show_back_top
105+
let:display
106+
>
107+
<div
108+
class=class_list!["thaw-back-top", class.map(| c | move || c.get())]
109+
ref=back_top_ref
110+
style=move || {
111+
display.get().map(|d| d.to_string()).unwrap_or_else(|| style.get())
112+
}
113+
on:click=on_click
114+
>
115+
<OptionComp value=children let:children>
116+
<Fallback slot>
117+
<Icon icon=icondata_ai::AiVerticalAlignTopOutlined/>
118+
</Fallback>
119+
{children()}
120+
</OptionComp>
121+
</div>
122+
</CSSTransition>
123+
</Teleport>
124+
</div>
125+
}
126+
}

thaw/src/back_top/theme.rs

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use crate::theme::ThemeMethod;
2+
3+
#[derive(Clone)]
4+
pub struct BackTopTheme {
5+
pub background_color: String,
6+
7+
}
8+
9+
impl ThemeMethod for BackTopTheme {
10+
fn light() -> Self {
11+
Self {
12+
background_color: "#fff".into(),
13+
}
14+
}
15+
16+
fn dark() -> Self {
17+
Self {
18+
background_color: "#48484e".into(),
19+
}
20+
}
21+
}

thaw/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod alert;
22
mod auto_complete;
33
mod avatar;
4+
mod back_top;
45
mod badge;
56
mod breadcrumb;
67
mod button;
@@ -46,6 +47,7 @@ mod upload;
4647
pub use alert::*;
4748
pub use auto_complete::*;
4849
pub use avatar::*;
50+
pub use back_top::*;
4951
pub use badge::*;
5052
pub use breadcrumb::*;
5153
pub use button::*;

thaw/src/theme/mod.rs

+8-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ mod common;
33
use self::common::CommonTheme;
44
use crate::{
55
mobile::{NavBarTheme, TabbarTheme},
6-
AlertTheme, AutoCompleteTheme, AvatarTheme, BreadcrumbTheme, ButtonTheme, CalendarTheme,
7-
CollapseTheme, ColorPickerTheme, DatePickerTheme, InputTheme, MenuTheme, MessageTheme,
8-
PopoverTheme, ProgressTheme, ScrollbarTheme, SelectTheme, SkeletionTheme, SliderTheme,
9-
SpinnerTheme, SwitchTheme, TableTheme, TagTheme, TimePickerTheme, TypographyTheme, UploadTheme,
6+
AlertTheme, AutoCompleteTheme, AvatarTheme, BackTopTheme, BreadcrumbTheme, ButtonTheme,
7+
CalendarTheme, CollapseTheme, ColorPickerTheme, DatePickerTheme, InputTheme, MenuTheme,
8+
MessageTheme, PopoverTheme, ProgressTheme, ScrollbarTheme, SelectTheme, SkeletionTheme,
9+
SliderTheme, SpinnerTheme, SwitchTheme, TableTheme, TagTheme, TimePickerTheme, TypographyTheme,
10+
UploadTheme,
1011
};
1112
use leptos::*;
1213

@@ -46,6 +47,7 @@ pub struct Theme {
4647
pub popover: PopoverTheme,
4748
pub collapse: CollapseTheme,
4849
pub scrollbar: ScrollbarTheme,
50+
pub back_top: BackTopTheme,
4951
}
5052

5153
impl Theme {
@@ -80,6 +82,7 @@ impl Theme {
8082
popover: PopoverTheme::light(),
8183
collapse: CollapseTheme::light(),
8284
scrollbar: ScrollbarTheme::light(),
85+
back_top: BackTopTheme::light(),
8386
}
8487
}
8588
pub fn dark() -> Self {
@@ -113,6 +116,7 @@ impl Theme {
113116
popover: PopoverTheme::dark(),
114117
collapse: CollapseTheme::dark(),
115118
scrollbar: ScrollbarTheme::dark(),
119+
back_top: BackTopTheme::dark(),
116120
}
117121
}
118122
}

0 commit comments

Comments
 (0)