Skip to content

Commit d238344

Browse files
authored
Refactor/css transition (#128)
* refactor: CSSTransition * fix: CSSTransition timeout processing * fix: CSSTransition repeated processing * fix: Modal css transition
1 parent 1a55b45 commit d238344

File tree

3 files changed

+243
-89
lines changed

3 files changed

+243
-89
lines changed
+216-72
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
use crate::utils::{add_event_listener, EventListenerHandle};
12
use leptos::{html::ElementDescriptor, *};
2-
use std::ops::Deref;
3+
use std::{ops::Deref, time::Duration};
34

45
/// # CSS Transition
5-
///
6+
///
67
/// Reference to https://vuejs.org/guide/built-ins/transition.html
78
#[component]
89
pub fn CSSTransition<T, CF, IV>(
@@ -20,91 +21,234 @@ where
2021
IV: IntoView,
2122
{
2223
let display = create_rw_signal((!show.get_untracked()).then_some("display: none;"));
23-
let remove_class_name = store_value(None::<RemoveClassName>);
2424

2525
node_ref.on_load(move |node_el| {
26-
let el = node_el.clone().into_any();
27-
let el = el.deref();
26+
let any_el = node_el.clone().into_any();
27+
let el = any_el.deref().clone();
2828
let class_list = el.class_list();
29-
let remove_class = Callback::new(move |_| {
30-
remove_class_name.update_value(|class| {
31-
if let Some(class) = class.take() {
32-
match class {
33-
RemoveClassName::Enter(active, to) => {
34-
let _ = class_list.remove_2(&active, &to);
35-
if let Some(on_after_enter) = on_after_enter {
36-
on_after_enter.call(());
37-
}
38-
}
39-
RemoveClassName::Leave(active, to) => {
40-
let _ = class_list.remove_2(&active, &to);
41-
display.set(Some("display: none;"));
42-
if let Some(on_after_leave) = on_after_leave {
43-
on_after_leave.call(());
44-
}
45-
}
46-
}
29+
let end_handle = StoredValue::new(None::<EventListenerHandle>);
30+
let end_count = StoredValue::new(None::<usize>);
31+
let finish = StoredValue::new(None::<Callback<()>>);
32+
33+
let on_end = Callback::new(move |remove: Callback<()>| {
34+
let Some(CSSTransitionInfo {
35+
types,
36+
prop_count,
37+
timeout,
38+
}) = get_transition_info(&el)
39+
else {
40+
remove.call(());
41+
return;
42+
};
43+
44+
finish.set_value(Some(Callback::new(move |_| {
45+
end_count.set_value(None);
46+
remove.call(());
47+
end_handle.update_value(|h| {
48+
h.take().map(|h| {
49+
h.remove();
50+
});
51+
});
52+
})));
53+
54+
set_timeout(
55+
move || {
56+
finish.update_value(|v| {
57+
v.take().map(|f| f.call(()));
58+
});
59+
},
60+
Duration::from_millis(timeout + 1),
61+
);
62+
63+
end_count.set_value(Some(0));
64+
let event_listener = move || {
65+
end_count.update_value(|v| {
66+
let Some(v) = v else {
67+
return;
68+
};
69+
*v += 1;
70+
});
71+
if end_count.with_value(|v| {
72+
let Some(v) = v else {
73+
return false;
74+
};
75+
*v >= prop_count
76+
}) {
77+
finish.update_value(|v| {
78+
v.take().map(|f| f.call(()));
79+
});
4780
}
48-
});
81+
};
82+
let handle = match types {
83+
AnimationTypes::Transition => {
84+
add_event_listener(any_el.clone(), ev::transitionend, move |_| event_listener())
85+
}
86+
AnimationTypes::Animation => {
87+
add_event_listener(any_el.clone(), ev::animationend, move |_| event_listener())
88+
}
89+
};
90+
end_handle.set_value(Some(handle));
4991
});
5092

51-
let _ = node_el
52-
.on(ev::transitionend, move |_| {
53-
remove_class.call(());
54-
})
55-
.on(ev::animationend, move |_| {
56-
remove_class.call(());
93+
let on_finish = move || {
94+
finish.update_value(|v| {
95+
v.take().map(|f| f.call(()));
5796
});
58-
});
97+
};
98+
99+
let on_enter_fn = {
100+
let class_list = class_list.clone();
101+
Callback::new(move |name: String| {
102+
let enter_from = format!("{name}-enter-from");
103+
let enter_active = format!("{name}-enter-active");
104+
let enter_to = format!("{name}-enter-to");
105+
106+
let _ = class_list.add_2(&enter_from, &enter_active);
107+
display.set(None);
108+
109+
let class_list = class_list.clone();
110+
next_frame(move || {
111+
let _ = class_list.remove_1(&enter_from);
112+
let _ = class_list.add_1(&enter_to);
59113

60-
create_render_effect(move |prev: Option<bool>| {
61-
let show = show.get();
62-
if let Some(node_el) = node_ref.get_untracked() {
63-
if let Some(prev) = prev {
64-
let name = name.get_untracked();
65-
66-
let el = node_el.into_any();
67-
let el = el.deref();
68-
let class_list = el.class_list();
69-
70-
if show && !prev {
71-
let enter_from = format!("{name}-enter-from");
72-
let enter_active = format!("{name}-enter-active");
73-
let enter_to = format!("{name}-enter-to");
74-
75-
let _ = class_list.add_2(&enter_from, &enter_active);
76-
display.set(None);
77-
request_animation_frame(move || {
78-
let _ = class_list.remove_1(&enter_from);
79-
let _ = class_list.add_1(&enter_to);
80-
remove_class_name
81-
.set_value(Some(RemoveClassName::Enter(enter_active, enter_to)));
82-
if let Some(on_enter) = on_enter {
83-
on_enter.call(());
114+
let remove = Callback::new(move |_| {
115+
let _ = class_list.remove_2(&enter_active, &enter_to);
116+
if let Some(on_after_enter) = on_after_enter {
117+
on_after_enter.call(());
84118
}
85119
});
86-
} else if !show && prev {
87-
let leave_from = format!("{name}-leave-from");
88-
let leave_active = format!("{name}-leave-active");
89-
let leave_to = format!("{name}-leave-to");
90-
91-
let _ = class_list.add_2(&leave_from, &leave_active);
92-
request_animation_frame(move || {
93-
let _ = class_list.remove_1(&leave_from);
94-
let _ = class_list.add_1(&leave_to);
95-
remove_class_name
96-
.set_value(Some(RemoveClassName::Leave(leave_active, leave_to)));
120+
on_end.call(remove);
121+
122+
if let Some(on_enter) = on_enter {
123+
on_enter.call(());
124+
}
125+
});
126+
})
127+
};
128+
129+
let on_leave_fn = {
130+
let class_list = class_list.clone();
131+
Callback::new(move |name: String| {
132+
let leave_from = format!("{name}-leave-from");
133+
let leave_active = format!("{name}-leave-active");
134+
let leave_to = format!("{name}-leave-to");
135+
136+
let _ = class_list.add_2(&leave_from, &leave_active);
137+
138+
let class_list = class_list.clone();
139+
next_frame(move || {
140+
let _ = class_list.remove_1(&leave_from);
141+
let _ = class_list.add_1(&leave_to);
142+
143+
let remove = Callback::new(move |_| {
144+
let _ = class_list.remove_2(&leave_active, &leave_to);
145+
display.set(Some("display: none;"));
146+
if let Some(on_after_leave) = on_after_leave {
147+
on_after_leave.call(());
148+
}
97149
});
98-
}
150+
on_end.call(remove);
151+
});
152+
})
153+
};
154+
155+
create_render_effect(move |prev: Option<bool>| {
156+
let show = show.get();
157+
let Some(prev) = prev else {
158+
return show;
159+
};
160+
161+
let name = name.get_untracked();
162+
163+
if show && !prev {
164+
on_finish();
165+
on_enter_fn.call(name);
166+
} else if !show && prev {
167+
on_finish();
168+
on_leave_fn.call(name);
99169
}
100-
}
101-
show
170+
171+
show
172+
});
102173
});
103174

104175
children(display.read_only())
105176
}
106177

107-
enum RemoveClassName {
108-
Enter(String, String),
109-
Leave(String, String),
178+
fn next_frame(cb: impl FnOnce() + 'static) {
179+
request_animation_frame(move || {
180+
request_animation_frame(cb);
181+
});
182+
}
183+
184+
#[derive(PartialEq)]
185+
enum AnimationTypes {
186+
Transition,
187+
Animation,
188+
}
189+
190+
struct CSSTransitionInfo {
191+
types: AnimationTypes,
192+
prop_count: usize,
193+
timeout: u64,
194+
}
195+
196+
fn get_transition_info(el: &web_sys::HtmlElement) -> Option<CSSTransitionInfo> {
197+
let styles = window().get_computed_style(el).ok().flatten()?;
198+
199+
let get_style_properties = |property: &str| {
200+
styles
201+
.get_property_value(property)
202+
.unwrap_or_default()
203+
.split(", ")
204+
.map(|s| s.to_string())
205+
.collect::<Vec<_>>()
206+
};
207+
208+
let transition_delays = get_style_properties("transition-delay");
209+
let transition_durations = get_style_properties("transition-duration");
210+
let transition_timeout = get_timeout(transition_delays, &transition_durations);
211+
let animation_delays = get_style_properties("animation-delay");
212+
let animation_durations = get_style_properties("animation-duration");
213+
let animation_timeout = get_timeout(animation_delays, &animation_durations);
214+
215+
let timeout = u64::max(transition_timeout, animation_timeout);
216+
let (types, prop_count) = if timeout > 0 {
217+
if transition_timeout > animation_timeout {
218+
(AnimationTypes::Transition, transition_durations.len())
219+
} else {
220+
(AnimationTypes::Animation, animation_durations.len())
221+
}
222+
} else {
223+
return None;
224+
};
225+
226+
Some(CSSTransitionInfo {
227+
types,
228+
prop_count,
229+
timeout,
230+
})
231+
}
232+
233+
fn get_timeout(mut delays: Vec<String>, durations: &Vec<String>) -> u64 {
234+
while delays.len() < durations.len() {
235+
delays.append(&mut delays.clone())
236+
}
237+
238+
fn to_ms(s: &String) -> u64 {
239+
if s == "auto" || s.is_empty() {
240+
return 0;
241+
}
242+
243+
let s = s.split_at(s.len() - 1).0;
244+
245+
(s.parse::<f32>().unwrap_or_default() * 1000.0).floor() as u64
246+
}
247+
248+
durations
249+
.iter()
250+
.enumerate()
251+
.map(|(i, d)| to_ms(d) + to_ms(&delays[i]))
252+
.max()
253+
.unwrap_or_default()
110254
}

thaw/src/modal/mod.rs

+21-11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ pub fn Modal(
2222
) -> impl IntoView {
2323
mount_style("modal", include_str!("./modal.css"));
2424

25+
let displayed = RwSignal::new(show.get_untracked());
26+
Effect::new(move |prev| {
27+
let show = show.get();
28+
if prev.is_some() && show {
29+
displayed.set(true);
30+
}
31+
show
32+
});
33+
2534
let on_mask_click = move |_| {
2635
if mask_closeable.get_untracked() {
2736
show.set(false);
@@ -73,15 +82,16 @@ pub fn Modal(
7382
ref=mask_ref
7483
></div>
7584
</CSSTransition>
76-
<CSSTransition
77-
node_ref=scroll_ref
78-
show=show.signal()
79-
name="fade-in-scale-up-transition"
80-
on_enter
81-
let:display
82-
>
83-
<div class="thaw-modal-scroll" style=move || display.get() ref=scroll_ref>
84-
<div class="thaw-modal-body" ref=modal_ref role="dialog" aria-modal="true">
85+
<div class="thaw-modal-scroll" style=move || (!displayed.get()).then_some("display: none") ref=scroll_ref>
86+
<CSSTransition
87+
node_ref=modal_ref
88+
show=show.signal()
89+
name="fade-in-scale-up-transition"
90+
on_enter
91+
on_after_leave=move |_| displayed.set(false)
92+
let:display
93+
>
94+
<div class="thaw-modal-body" ref=modal_ref role="dialog" aria-modal="true" style=move || display.get()>
8595
<Card>
8696
<CardHeader slot>
8797
<span class="thaw-model-title">{move || title.get()}</span>
@@ -102,8 +112,8 @@ pub fn Modal(
102112
</CardFooter>
103113
</Card>
104114
</div>
105-
</div>
106-
</CSSTransition>
115+
</CSSTransition>
116+
</div>
107117
</div>
108118
</Teleport>
109119
}

0 commit comments

Comments
 (0)