Refactor/css transition (#128)

* refactor: CSSTransition

* fix: CSSTransition timeout processing

* fix: CSSTransition repeated processing

* fix: Modal css transition
This commit is contained in:
luoxiaozero 2024-03-04 22:16:18 +08:00 committed by GitHub
parent 1a55b45d01
commit d2383445ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 239 additions and 85 deletions

View file

@ -1,8 +1,9 @@
use crate::utils::{add_event_listener, EventListenerHandle};
use leptos::{html::ElementDescriptor, *}; use leptos::{html::ElementDescriptor, *};
use std::ops::Deref; use std::{ops::Deref, time::Duration};
/// # CSS Transition /// # CSS Transition
/// ///
/// Reference to https://vuejs.org/guide/built-ins/transition.html /// Reference to https://vuejs.org/guide/built-ins/transition.html
#[component] #[component]
pub fn CSSTransition<T, CF, IV>( pub fn CSSTransition<T, CF, IV>(
@ -20,91 +21,234 @@ where
IV: IntoView, IV: IntoView,
{ {
let display = create_rw_signal((!show.get_untracked()).then_some("display: none;")); let display = create_rw_signal((!show.get_untracked()).then_some("display: none;"));
let remove_class_name = store_value(None::<RemoveClassName>);
node_ref.on_load(move |node_el| { node_ref.on_load(move |node_el| {
let el = node_el.clone().into_any(); let any_el = node_el.clone().into_any();
let el = el.deref(); let el = any_el.deref().clone();
let class_list = el.class_list(); let class_list = el.class_list();
let remove_class = Callback::new(move |_| { let end_handle = StoredValue::new(None::<EventListenerHandle>);
remove_class_name.update_value(|class| { let end_count = StoredValue::new(None::<usize>);
if let Some(class) = class.take() { let finish = StoredValue::new(None::<Callback<()>>);
match class {
RemoveClassName::Enter(active, to) => { let on_end = Callback::new(move |remove: Callback<()>| {
let _ = class_list.remove_2(&active, &to); let Some(CSSTransitionInfo {
if let Some(on_after_enter) = on_after_enter { types,
on_after_enter.call(()); prop_count,
} timeout,
} }) = get_transition_info(&el)
RemoveClassName::Leave(active, to) => { else {
let _ = class_list.remove_2(&active, &to); remove.call(());
display.set(Some("display: none;")); return;
if let Some(on_after_leave) = on_after_leave { };
on_after_leave.call(());
} finish.set_value(Some(Callback::new(move |_| {
} end_count.set_value(None);
} remove.call(());
end_handle.update_value(|h| {
h.take().map(|h| {
h.remove();
});
});
})));
set_timeout(
move || {
finish.update_value(|v| {
v.take().map(|f| f.call(()));
});
},
Duration::from_millis(timeout + 1),
);
end_count.set_value(Some(0));
let event_listener = move || {
end_count.update_value(|v| {
let Some(v) = v else {
return;
};
*v += 1;
});
if end_count.with_value(|v| {
let Some(v) = v else {
return false;
};
*v >= prop_count
}) {
finish.update_value(|v| {
v.take().map(|f| f.call(()));
});
} }
}); };
let handle = match types {
AnimationTypes::Transition => {
add_event_listener(any_el.clone(), ev::transitionend, move |_| event_listener())
}
AnimationTypes::Animation => {
add_event_listener(any_el.clone(), ev::animationend, move |_| event_listener())
}
};
end_handle.set_value(Some(handle));
}); });
let _ = node_el let on_finish = move || {
.on(ev::transitionend, move |_| { finish.update_value(|v| {
remove_class.call(()); v.take().map(|f| f.call(()));
})
.on(ev::animationend, move |_| {
remove_class.call(());
}); });
}); };
create_render_effect(move |prev: Option<bool>| { let on_enter_fn = {
let show = show.get(); let class_list = class_list.clone();
if let Some(node_el) = node_ref.get_untracked() { Callback::new(move |name: String| {
if let Some(prev) = prev { let enter_from = format!("{name}-enter-from");
let name = name.get_untracked(); let enter_active = format!("{name}-enter-active");
let enter_to = format!("{name}-enter-to");
let el = node_el.into_any(); let _ = class_list.add_2(&enter_from, &enter_active);
let el = el.deref(); display.set(None);
let class_list = el.class_list();
if show && !prev { let class_list = class_list.clone();
let enter_from = format!("{name}-enter-from"); next_frame(move || {
let enter_active = format!("{name}-enter-active"); let _ = class_list.remove_1(&enter_from);
let enter_to = format!("{name}-enter-to"); let _ = class_list.add_1(&enter_to);
let _ = class_list.add_2(&enter_from, &enter_active); let remove = Callback::new(move |_| {
display.set(None); let _ = class_list.remove_2(&enter_active, &enter_to);
request_animation_frame(move || { if let Some(on_after_enter) = on_after_enter {
let _ = class_list.remove_1(&enter_from); on_after_enter.call(());
let _ = class_list.add_1(&enter_to);
remove_class_name
.set_value(Some(RemoveClassName::Enter(enter_active, enter_to)));
if let Some(on_enter) = on_enter {
on_enter.call(());
} }
}); });
} else if !show && prev { on_end.call(remove);
let leave_from = format!("{name}-leave-from");
let leave_active = format!("{name}-leave-active");
let leave_to = format!("{name}-leave-to");
let _ = class_list.add_2(&leave_from, &leave_active); if let Some(on_enter) = on_enter {
request_animation_frame(move || { on_enter.call(());
let _ = class_list.remove_1(&leave_from); }
let _ = class_list.add_1(&leave_to); });
remove_class_name })
.set_value(Some(RemoveClassName::Leave(leave_active, leave_to))); };
let on_leave_fn = {
let class_list = class_list.clone();
Callback::new(move |name: String| {
let leave_from = format!("{name}-leave-from");
let leave_active = format!("{name}-leave-active");
let leave_to = format!("{name}-leave-to");
let _ = class_list.add_2(&leave_from, &leave_active);
let class_list = class_list.clone();
next_frame(move || {
let _ = class_list.remove_1(&leave_from);
let _ = class_list.add_1(&leave_to);
let remove = Callback::new(move |_| {
let _ = class_list.remove_2(&leave_active, &leave_to);
display.set(Some("display: none;"));
if let Some(on_after_leave) = on_after_leave {
on_after_leave.call(());
}
}); });
} on_end.call(remove);
});
})
};
create_render_effect(move |prev: Option<bool>| {
let show = show.get();
let Some(prev) = prev else {
return show;
};
let name = name.get_untracked();
if show && !prev {
on_finish();
on_enter_fn.call(name);
} else if !show && prev {
on_finish();
on_leave_fn.call(name);
} }
}
show show
});
}); });
children(display.read_only()) children(display.read_only())
} }
enum RemoveClassName { fn next_frame(cb: impl FnOnce() + 'static) {
Enter(String, String), request_animation_frame(move || {
Leave(String, String), request_animation_frame(cb);
});
}
#[derive(PartialEq)]
enum AnimationTypes {
Transition,
Animation,
}
struct CSSTransitionInfo {
types: AnimationTypes,
prop_count: usize,
timeout: u64,
}
fn get_transition_info(el: &web_sys::HtmlElement) -> Option<CSSTransitionInfo> {
let styles = window().get_computed_style(el).ok().flatten()?;
let get_style_properties = |property: &str| {
styles
.get_property_value(property)
.unwrap_or_default()
.split(", ")
.map(|s| s.to_string())
.collect::<Vec<_>>()
};
let transition_delays = get_style_properties("transition-delay");
let transition_durations = get_style_properties("transition-duration");
let transition_timeout = get_timeout(transition_delays, &transition_durations);
let animation_delays = get_style_properties("animation-delay");
let animation_durations = get_style_properties("animation-duration");
let animation_timeout = get_timeout(animation_delays, &animation_durations);
let timeout = u64::max(transition_timeout, animation_timeout);
let (types, prop_count) = if timeout > 0 {
if transition_timeout > animation_timeout {
(AnimationTypes::Transition, transition_durations.len())
} else {
(AnimationTypes::Animation, animation_durations.len())
}
} else {
return None;
};
Some(CSSTransitionInfo {
types,
prop_count,
timeout,
})
}
fn get_timeout(mut delays: Vec<String>, durations: &Vec<String>) -> u64 {
while delays.len() < durations.len() {
delays.append(&mut delays.clone())
}
fn to_ms(s: &String) -> u64 {
if s == "auto" || s.is_empty() {
return 0;
}
let s = s.split_at(s.len() - 1).0;
(s.parse::<f32>().unwrap_or_default() * 1000.0).floor() as u64
}
durations
.iter()
.enumerate()
.map(|(i, d)| to_ms(d) + to_ms(&delays[i]))
.max()
.unwrap_or_default()
} }

View file

@ -22,6 +22,15 @@ pub fn Modal(
) -> impl IntoView { ) -> impl IntoView {
mount_style("modal", include_str!("./modal.css")); mount_style("modal", include_str!("./modal.css"));
let displayed = RwSignal::new(show.get_untracked());
Effect::new(move |prev| {
let show = show.get();
if prev.is_some() && show {
displayed.set(true);
}
show
});
let on_mask_click = move |_| { let on_mask_click = move |_| {
if mask_closeable.get_untracked() { if mask_closeable.get_untracked() {
show.set(false); show.set(false);
@ -73,15 +82,16 @@ pub fn Modal(
ref=mask_ref ref=mask_ref
></div> ></div>
</CSSTransition> </CSSTransition>
<CSSTransition <div class="thaw-modal-scroll" style=move || (!displayed.get()).then_some("display: none") ref=scroll_ref>
node_ref=scroll_ref <CSSTransition
show=show.signal() node_ref=modal_ref
name="fade-in-scale-up-transition" show=show.signal()
on_enter name="fade-in-scale-up-transition"
let:display on_enter
> on_after_leave=move |_| displayed.set(false)
<div class="thaw-modal-scroll" style=move || display.get() ref=scroll_ref> let:display
<div class="thaw-modal-body" ref=modal_ref role="dialog" aria-modal="true"> >
<div class="thaw-modal-body" ref=modal_ref role="dialog" aria-modal="true" style=move || display.get()>
<Card> <Card>
<CardHeader slot> <CardHeader slot>
<span class="thaw-model-title">{move || title.get()}</span> <span class="thaw-model-title">{move || title.get()}</span>
@ -102,8 +112,8 @@ pub fn Modal(
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>
</div> </CSSTransition>
</CSSTransition> </div>
</div> </div>
</Teleport> </Teleport>
} }

View file

@ -57,26 +57,26 @@
font-size: 16px; font-size: 16px;
} }
.fade-in-scale-up-transition-leave-active > .thaw-modal-body { .fade-in-scale-up-transition-leave-active.thaw-modal-body {
transform-origin: inherit; transform-origin: inherit;
transition: opacity 0.25s cubic-bezier(0.4, 0, 1, 1), transition: opacity 0.25s cubic-bezier(0.4, 0, 1, 1),
transform 0.25s cubic-bezier(0.4, 0, 1, 1); transform 0.25s cubic-bezier(0.4, 0, 1, 1);
} }
.fade-in-scale-up-transition-enter-active > .thaw-modal-body { .fade-in-scale-up-transition-enter-active.thaw-modal-body {
transform-origin: inherit; transform-origin: inherit;
transition: opacity 0.25s cubic-bezier(0, 0, 0.2, 1), transition: opacity 0.25s cubic-bezier(0, 0, 0.2, 1),
transform 0.25s cubic-bezier(0, 0, 0.2, 1); transform 0.25s cubic-bezier(0, 0, 0.2, 1);
} }
.fade-in-scale-up-transition-enter-from > .thaw-modal-body, .fade-in-scale-up-transition-enter-from.thaw-modal-body,
.fade-in-scale-up-transition-leave-to > .thaw-modal-body { .fade-in-scale-up-transition-leave-to.thaw-modal-body {
opacity: 0; opacity: 0;
transform: scale(0.5); transform: scale(0.5);
} }
.fade-in-scale-up-transition-leave-from > .thaw-modal-body, .fade-in-scale-up-transition-leave-from.thaw-modal-body,
.fade-in-scale-up-transition-enter-to > .thaw-modal-body { .fade-in-scale-up-transition-enter-to.thaw-modal-body {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }