Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor/css transition #128

Merged
merged 4 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
288 changes: 216 additions & 72 deletions thaw/src/components/css_transition/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::utils::{add_event_listener, EventListenerHandle};
use leptos::{html::ElementDescriptor, *};
use std::ops::Deref;
use std::{ops::Deref, time::Duration};

/// # CSS Transition
///
///
/// Reference to https://vuejs.org/guide/built-ins/transition.html
#[component]
pub fn CSSTransition<T, CF, IV>(
Expand All @@ -20,91 +21,234 @@ where
IV: IntoView,
{
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| {
let el = node_el.clone().into_any();
let el = el.deref();
let any_el = node_el.clone().into_any();
let el = any_el.deref().clone();
let class_list = el.class_list();
let remove_class = Callback::new(move |_| {
remove_class_name.update_value(|class| {
if let Some(class) = class.take() {
match class {
RemoveClassName::Enter(active, to) => {
let _ = class_list.remove_2(&active, &to);
if let Some(on_after_enter) = on_after_enter {
on_after_enter.call(());
}
}
RemoveClassName::Leave(active, to) => {
let _ = class_list.remove_2(&active, &to);
display.set(Some("display: none;"));
if let Some(on_after_leave) = on_after_leave {
on_after_leave.call(());
}
}
}
let end_handle = StoredValue::new(None::<EventListenerHandle>);
let end_count = StoredValue::new(None::<usize>);
let finish = StoredValue::new(None::<Callback<()>>);

let on_end = Callback::new(move |remove: Callback<()>| {
let Some(CSSTransitionInfo {
types,
prop_count,
timeout,
}) = get_transition_info(&el)
else {
remove.call(());
return;
};

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
.on(ev::transitionend, move |_| {
remove_class.call(());
})
.on(ev::animationend, move |_| {
remove_class.call(());
let on_finish = move || {
finish.update_value(|v| {
v.take().map(|f| f.call(()));
});
});
};

let on_enter_fn = {
let class_list = class_list.clone();
Callback::new(move |name: String| {
let enter_from = format!("{name}-enter-from");
let enter_active = format!("{name}-enter-active");
let enter_to = format!("{name}-enter-to");

let _ = class_list.add_2(&enter_from, &enter_active);
display.set(None);

let class_list = class_list.clone();
next_frame(move || {
let _ = class_list.remove_1(&enter_from);
let _ = class_list.add_1(&enter_to);

create_render_effect(move |prev: Option<bool>| {
let show = show.get();
if let Some(node_el) = node_ref.get_untracked() {
if let Some(prev) = prev {
let name = name.get_untracked();

let el = node_el.into_any();
let el = el.deref();
let class_list = el.class_list();

if show && !prev {
let enter_from = format!("{name}-enter-from");
let enter_active = format!("{name}-enter-active");
let enter_to = format!("{name}-enter-to");

let _ = class_list.add_2(&enter_from, &enter_active);
display.set(None);
request_animation_frame(move || {
let _ = class_list.remove_1(&enter_from);
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(());
let remove = Callback::new(move |_| {
let _ = class_list.remove_2(&enter_active, &enter_to);
if let Some(on_after_enter) = on_after_enter {
on_after_enter.call(());
}
});
} else if !show && prev {
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);
request_animation_frame(move || {
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)));
on_end.call(remove);

if let Some(on_enter) = on_enter {
on_enter.call(());
}
});
})
};

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())
}

enum RemoveClassName {
Enter(String, String),
Leave(String, String),
fn next_frame(cb: impl FnOnce() + 'static) {
request_animation_frame(move || {
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()
}
32 changes: 21 additions & 11 deletions thaw/src/modal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ pub fn Modal(
) -> impl IntoView {
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 |_| {
if mask_closeable.get_untracked() {
show.set(false);
Expand Down Expand Up @@ -73,15 +82,16 @@ pub fn Modal(
ref=mask_ref
></div>
</CSSTransition>
<CSSTransition
node_ref=scroll_ref
show=show.signal()
name="fade-in-scale-up-transition"
on_enter
let:display
>
<div class="thaw-modal-scroll" style=move || display.get() ref=scroll_ref>
<div class="thaw-modal-body" ref=modal_ref role="dialog" aria-modal="true">
<div class="thaw-modal-scroll" style=move || (!displayed.get()).then_some("display: none") ref=scroll_ref>
<CSSTransition
node_ref=modal_ref
show=show.signal()
name="fade-in-scale-up-transition"
on_enter
on_after_leave=move |_| displayed.set(false)
let:display
>
<div class="thaw-modal-body" ref=modal_ref role="dialog" aria-modal="true" style=move || display.get()>
<Card>
<CardHeader slot>
<span class="thaw-model-title">{move || title.get()}</span>
Expand All @@ -102,8 +112,8 @@ pub fn Modal(
</CardFooter>
</Card>
</div>
</div>
</CSSTransition>
</CSSTransition>
</div>
</div>
</Teleport>
}
Expand Down
Loading
Loading