Skip to main content

animato_dioxus/
transition.rs

1//! Page transition helpers for Dioxus apps.
2
3use crate::PresenceAnimation;
4use dioxus::prelude::*;
5use std::fmt;
6
7/// Page transition strategy.
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9pub enum TransitionMode {
10    /// Old page exits before the new page enters.
11    #[default]
12    Sequential,
13    /// Old and new page animate together.
14    Parallel,
15    /// Opposing opacity transition.
16    CrossFade,
17    /// New page slides over the previous page.
18    SlideOver,
19    /// Shared-element hero morph mode.
20    MorphHero,
21}
22
23impl fmt::Display for TransitionMode {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        write!(f, "{self:?}")
26    }
27}
28
29/// Route-change transition wrapper.
30#[component]
31pub fn PageTransition(
32    /// Transition mode.
33    mode: Option<TransitionMode>,
34    /// Optional route key. Pass the current Dioxus router route string here to
35    /// force keyed transition identity.
36    route_key: Option<String>,
37    /// Enter animation.
38    enter: Option<PresenceAnimation>,
39    /// Exit animation.
40    exit: Option<PresenceAnimation>,
41    /// Child route view.
42    children: Element,
43) -> Element {
44    let mode = mode.unwrap_or_default();
45    let enter = enter.unwrap_or_else(|| match mode {
46        TransitionMode::SlideOver => PresenceAnimation::slide_right(),
47        TransitionMode::MorphHero => PresenceAnimation::zoom_in(),
48        _ => PresenceAnimation::fade(),
49    });
50    let _exit = exit.unwrap_or_else(|| enter.reversed());
51    let base_style = container_css(mode);
52    let style = format!(
53        "{base_style}{}{}",
54        enter.to.to_css(),
55        crate::presence::transition_css(enter.duration)
56    );
57    let route_key = route_key.unwrap_or_default();
58
59    rsx! {
60        div {
61            key: "{route_key}",
62            style: "{style}",
63            {children}
64        }
65    }
66}
67
68/// Return a Dioxus Router route key for use with [`PageTransition`].
69#[cfg(feature = "router")]
70pub fn route_transition_key<R>() -> String
71where
72    R: dioxus_router::Routable + Clone + ToString + 'static,
73{
74    dioxus_router::hooks::use_route::<R>().to_string()
75}
76
77/// Return an empty route key when router support is not enabled.
78#[cfg(not(feature = "router"))]
79pub fn route_transition_key() -> String {
80    String::new()
81}
82
83pub(crate) fn container_css(mode: TransitionMode) -> &'static str {
84    match mode {
85        TransitionMode::SlideOver => "display:block; position:relative; overflow:hidden;",
86        _ => "display:block; position:relative;",
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[allow(non_snake_case)]
95    fn PageTransitionApp() -> Element {
96        rsx! {
97            PageTransition {
98                mode: Some(TransitionMode::MorphHero),
99                route_key: Some("route-a".to_owned()),
100                enter: None::<PresenceAnimation>,
101                exit: None::<PresenceAnimation>,
102                div { "page" }
103            }
104        }
105    }
106
107    #[test]
108    fn container_css_matches_transition_mode() {
109        assert!(container_css(TransitionMode::SlideOver).contains("overflow:hidden"));
110        assert_eq!(
111            container_css(TransitionMode::Sequential),
112            "display:block; position:relative;"
113        );
114    }
115
116    #[test]
117    fn display_matches_debug_label() {
118        assert_eq!(TransitionMode::CrossFade.to_string(), "CrossFade");
119    }
120
121    #[test]
122    fn all_transition_modes_have_stable_container_css() {
123        for mode in [
124            TransitionMode::Sequential,
125            TransitionMode::Parallel,
126            TransitionMode::CrossFade,
127            TransitionMode::SlideOver,
128            TransitionMode::MorphHero,
129        ] {
130            let css = container_css(mode);
131            assert!(css.contains("display:block"));
132            assert!(css.contains("position:relative"));
133        }
134    }
135
136    #[test]
137    fn page_transition_component_renders_with_default_mode_animation() {
138        let mut dom = VirtualDom::new(PageTransitionApp);
139        let mutations = dom.rebuild_to_vec();
140        assert!(!mutations.edits.is_empty());
141    }
142}