dioxus_tw_components/components/
stepper.rs1use dioxus::prelude::*;
2
3#[derive(Clone, PartialEq, Props)]
4pub struct StepperProps {
5 #[props(extends = div, extends = GlobalAttributes)]
6 attributes: Vec<Attribute>,
7
8 pub current_step: usize,
10 pub total_steps: usize,
12 pub step_titles: Vec<String>,
14 #[props(default)]
16 pub completed_steps: Vec<bool>,
17 pub on_next: EventHandler<()>,
19 pub on_back: EventHandler<()>,
21 #[props(optional)]
23 pub on_complete: Option<EventHandler<()>>,
24 #[props(optional)]
26 pub on_step_click: Option<EventHandler<usize>>,
27 #[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 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 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 div { class: "stepper-content",
89 {props.children}
90 }
91
92 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}