adui_dioxus/components/
steps.rs1use crate::components::config_provider::ComponentSize;
2use dioxus::prelude::*;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum StepStatus {
7 Wait,
8 Process,
9 Finish,
10 Error,
11}
12
13impl StepStatus {
14 fn as_class(&self) -> &'static str {
15 match self {
16 StepStatus::Wait => "adui-steps-status-wait",
17 StepStatus::Process => "adui-steps-status-process",
18 StepStatus::Finish => "adui-steps-status-finish",
19 StepStatus::Error => "adui-steps-status-error",
20 }
21 }
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum StepsDirection {
27 Horizontal,
28 Vertical,
29}
30
31impl StepsDirection {
32 fn as_class(&self) -> &'static str {
33 match self {
34 StepsDirection::Horizontal => "adui-steps-horizontal",
35 StepsDirection::Vertical => "adui-steps-vertical",
36 }
37 }
38}
39
40#[derive(Clone, PartialEq)]
42pub struct StepItem {
43 pub key: String,
44 pub title: Element,
45 pub description: Option<Element>,
46 pub status: Option<StepStatus>,
47 pub disabled: bool,
48}
49
50impl StepItem {
51 pub fn new(key: impl Into<String>, title: Element) -> Self {
52 Self {
53 key: key.into(),
54 title,
55 description: None,
56 status: None,
57 disabled: false,
58 }
59 }
60}
61
62#[derive(Props, Clone, PartialEq)]
64pub struct StepsProps {
65 pub items: Vec<StepItem>,
66 #[props(optional)]
67 pub current: Option<usize>,
68 #[props(optional)]
69 pub default_current: Option<usize>,
70 #[props(optional)]
71 pub on_change: Option<EventHandler<usize>>,
72 #[props(optional)]
73 pub direction: Option<StepsDirection>,
74 #[props(optional)]
75 pub size: Option<ComponentSize>,
76 #[props(optional)]
77 pub class: Option<String>,
78 #[props(optional)]
79 pub style: Option<String>,
80}
81
82fn effective_status(index: usize, current: usize, explicit: Option<StepStatus>) -> StepStatus {
83 if let Some(st) = explicit {
84 return st;
85 }
86 if index < current {
87 StepStatus::Finish
88 } else if index == current {
89 StepStatus::Process
90 } else {
91 StepStatus::Wait
92 }
93}
94
95#[component]
97pub fn Steps(props: StepsProps) -> Element {
98 let StepsProps {
99 items,
100 current,
101 default_current,
102 on_change,
103 direction,
104 size,
105 class,
106 style,
107 } = props;
108
109 let initial_current = default_current.unwrap_or(0);
110 let current_internal: Signal<usize> = use_signal(|| initial_current);
111 let is_controlled = current.is_some();
112 let current_index = current.unwrap_or_else(|| *current_internal.read());
113
114 let dir = direction.unwrap_or(StepsDirection::Horizontal);
115
116 let mut class_list = vec!["adui-steps".to_string(), dir.as_class().to_string()];
117 if let Some(sz) = size {
118 match sz {
119 ComponentSize::Small => class_list.push("adui-steps-sm".into()),
120 ComponentSize::Middle => {}
121 ComponentSize::Large => class_list.push("adui-steps-lg".into()),
122 }
123 }
124 if let Some(extra) = class {
125 class_list.push(extra);
126 }
127 let class_attr = class_list.join(" ");
128 let style_attr = style.unwrap_or_default();
129
130 let on_change_cb = on_change;
131
132 rsx! {
133 ol { class: "{class_attr}", style: "{style_attr}",
134 {items.iter().enumerate().map(|(idx, item)| {
135 let status = effective_status(idx, current_index, item.status);
136 let mut item_class = vec!["adui-steps-item".to_string(), status.as_class().to_string()];
137 if item.disabled {
138 item_class.push("adui-steps-item-disabled".into());
139 }
140 if idx == current_index {
141 item_class.push("adui-steps-item-current".into());
142 }
143 let item_class_attr = item_class.join(" ");
144
145 let current_internal_for_click = current_internal;
146 let on_change_for_click = on_change_cb;
147 let disabled = item.disabled;
148
149 let title = item.title.clone();
150 let description = item.description.clone();
151 let display_index = idx + 1;
152
153 rsx! {
154 li {
155 key: "step-{idx}",
156 class: "{item_class_attr}",
157 onclick: move |_| {
158 if disabled {
159 return;
160 }
161 if !is_controlled {
162 let mut sig = current_internal_for_click;
163 sig.set(idx);
164 }
165 if let Some(cb) = on_change_for_click {
166 cb.call(idx);
167 }
168 },
169 div { class: "adui-steps-item-icon",
170 span { class: "adui-steps-item-index", "{display_index}" }
171 }
172 div { class: "adui-steps-item-content",
173 div { class: "adui-steps-item-title", {title} }
174 if let Some(desc) = description {
175 div { class: "adui-steps-item-description", {desc} }
176 }
177 }
178 }
179 }
180 })}
181 }
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn effective_status_defaults_by_index() {
191 assert_eq!(effective_status(0, 1, None), StepStatus::Finish);
192 assert_eq!(effective_status(1, 1, None), StepStatus::Process);
193 assert_eq!(effective_status(2, 1, None), StepStatus::Wait);
194 }
195}