Skip to main content

dioxus_tw_components/components/
stepper.rs

1use dioxus::prelude::*;
2
3#[derive(Clone, PartialEq, Props)]
4pub struct StepperProps {
5    #[props(extends = div, extends = GlobalAttributes)]
6    attributes: Vec<Attribute>,
7
8    /// Current step index (0-based)
9    pub current_step: usize,
10    /// Total number of steps
11    pub total_steps: usize,
12    /// Title for each step
13    pub step_titles: Vec<String>,
14    /// Which steps are completed
15    #[props(default)]
16    pub completed_steps: Vec<bool>,
17    /// Called when "Next" is clicked
18    pub on_next: EventHandler<()>,
19    /// Called when "Back" is clicked
20    pub on_back: EventHandler<()>,
21    /// Called when "Complete" is clicked (last step)
22    #[props(optional)]
23    pub on_complete: Option<EventHandler<()>>,
24    /// Called when a step indicator is clicked (passes step index)
25    #[props(optional)]
26    pub on_step_click: Option<EventHandler<usize>>,
27    /// Whether to show "Draft saved" indicator
28    #[props(default)]
29    pub show_saved: bool,
30
31    children: Element,
32}
33
34#[component]
35pub fn Stepper(mut props: StepperProps) -> Element {
36    let default_classes = "stepper";
37    crate::setup_class_attribute(&mut props.attributes, default_classes);
38
39    let is_first = props.current_step == 0;
40    let is_last = props.current_step >= props.total_steps.saturating_sub(1);
41    let step_title = props
42        .step_titles
43        .get(props.current_step)
44        .cloned()
45        .unwrap_or_default();
46
47    rsx! {
48        div {..props.attributes,
49            // Progress bar
50            div { class: "stepper-progress",
51                for i in 0..props.total_steps {
52                    {
53                        let state = if props.completed_steps.get(i).copied().unwrap_or(false) {
54                            "completed"
55                        } else if i == props.current_step {
56                            "current"
57                        } else {
58                            "pending"
59                        };
60                        let clickable = props.on_step_click.is_some();
61                        rsx! {
62                            div {
63                                class: "stepper-progress-segment",
64                                class: if clickable { "cursor-pointer" },
65                                "data-state": state,
66                                onclick: move |_| {
67                                    if let Some(ref on_step_click) = props.on_step_click {
68                                        on_step_click.call(i);
69                                    }
70                                },
71                            }
72                        }
73                    }
74                }
75            }
76
77            // Header
78            div { class: "stepper-header",
79                p { class: "stepper-step-label",
80                    "Step {props.current_step + 1} of {props.total_steps}"
81                }
82                h2 { class: "stepper-step-title",
83                    "{step_title}"
84                }
85            }
86
87            // Content
88            div { class: "stepper-content",
89                {props.children}
90            }
91
92            // Navigation
93            div { class: "stepper-nav",
94                if !is_first {
95                    button {
96                        class: "button",
97                        "data-variant": "outline",
98                        onclick: move |_| props.on_back.call(()),
99                        "Back"
100                    }
101                } else {
102                    div {}
103                }
104                div { class: "flex items-center gap-3",
105                    if props.show_saved {
106                        span { class: "stepper-save-indicator",
107                            "Draft saved"
108                        }
109                    }
110                    if is_last {
111                        if let Some(on_complete) = &props.on_complete {
112                            button {
113                                class: "button",
114                                "data-variant": "default",
115                                onclick: {
116                                    let on_complete = *on_complete;
117                                    move |_| on_complete.call(())
118                                },
119                                "Complete"
120                            }
121                        }
122                    } else {
123                        button {
124                            class: "button",
125                            "data-variant": "default",
126                            onclick: move |_| props.on_next.call(()),
127                            "Next"
128                        }
129                    }
130                }
131            }
132        }
133    }
134}