1use 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
196 #[test]
197 fn effective_status_respects_explicit_status() {
198 assert_eq!(
199 effective_status(0, 1, Some(StepStatus::Error)),
200 StepStatus::Error
201 );
202 assert_eq!(
203 effective_status(1, 1, Some(StepStatus::Wait)),
204 StepStatus::Wait
205 );
206 assert_eq!(
207 effective_status(2, 1, Some(StepStatus::Finish)),
208 StepStatus::Finish
209 );
210 }
211
212 #[test]
213 fn effective_status_before_current() {
214 assert_eq!(effective_status(0, 2, None), StepStatus::Finish);
215 assert_eq!(effective_status(1, 2, None), StepStatus::Finish);
216 }
217
218 #[test]
219 fn effective_status_at_current() {
220 assert_eq!(effective_status(0, 0, None), StepStatus::Process);
221 assert_eq!(effective_status(5, 5, None), StepStatus::Process);
222 }
223
224 #[test]
225 fn effective_status_after_current() {
226 assert_eq!(effective_status(3, 1, None), StepStatus::Wait);
227 assert_eq!(effective_status(10, 5, None), StepStatus::Wait);
228 }
229
230 #[test]
231 fn step_status_class_mapping() {
232 assert_eq!(StepStatus::Wait.as_class(), "adui-steps-status-wait");
233 assert_eq!(StepStatus::Process.as_class(), "adui-steps-status-process");
234 assert_eq!(StepStatus::Finish.as_class(), "adui-steps-status-finish");
235 assert_eq!(StepStatus::Error.as_class(), "adui-steps-status-error");
236 }
237
238 #[test]
239 fn step_status_all_variants() {
240 let variants = [
241 StepStatus::Wait,
242 StepStatus::Process,
243 StepStatus::Finish,
244 StepStatus::Error,
245 ];
246 for variant in variants.iter() {
247 let class = variant.as_class();
248 assert!(!class.is_empty());
249 assert!(class.starts_with("adui-steps-status-"));
250 }
251 }
252
253 #[test]
254 fn steps_direction_class_mapping() {
255 assert_eq!(
256 StepsDirection::Horizontal.as_class(),
257 "adui-steps-horizontal"
258 );
259 assert_eq!(StepsDirection::Vertical.as_class(), "adui-steps-vertical");
260 }
261
262 #[test]
263 fn steps_direction_equality() {
264 assert_eq!(StepsDirection::Horizontal, StepsDirection::Horizontal);
265 assert_eq!(StepsDirection::Vertical, StepsDirection::Vertical);
266 assert_ne!(StepsDirection::Horizontal, StepsDirection::Vertical);
267 }
268
269 #[test]
270 fn step_item_new() {
271 let item = StepItem::new("key1", rsx!(div { "Title" }));
272 assert_eq!(item.key, "key1");
273 assert_eq!(item.description, None);
274 assert_eq!(item.status, None);
275 assert_eq!(item.disabled, false);
276 }
277
278 #[test]
279 fn step_item_clone() {
280 let item = StepItem::new("key1", rsx!(div { "Title" }));
281 let cloned = item.clone();
282 assert_eq!(item.key, cloned.key);
283 assert_eq!(item.disabled, cloned.disabled);
284 }
285
286 #[test]
287 fn steps_props_defaults() {
288 }
295
296 #[test]
297 fn effective_status_all_status_variants() {
298 assert_eq!(
300 effective_status(0, 0, Some(StepStatus::Wait)),
301 StepStatus::Wait
302 );
303 assert_eq!(
304 effective_status(0, 0, Some(StepStatus::Process)),
305 StepStatus::Process
306 );
307 assert_eq!(
308 effective_status(0, 0, Some(StepStatus::Finish)),
309 StepStatus::Finish
310 );
311 assert_eq!(
312 effective_status(0, 0, Some(StepStatus::Error)),
313 StepStatus::Error
314 );
315 }
316
317 #[test]
318 fn effective_status_explicit_overrides_index() {
319 assert_eq!(
321 effective_status(10, 5, Some(StepStatus::Finish)),
322 StepStatus::Finish
323 );
324 assert_eq!(
325 effective_status(0, 10, Some(StepStatus::Wait)),
326 StepStatus::Wait
327 );
328 }
329
330 #[test]
331 fn effective_status_boundary_index_zero() {
332 assert_eq!(effective_status(0, 0, None), StepStatus::Process);
333 assert_eq!(effective_status(0, 1, None), StepStatus::Finish);
334 }
335
336 #[test]
337 fn effective_status_large_indices() {
338 assert_eq!(effective_status(100, 50, None), StepStatus::Wait);
339 assert_eq!(effective_status(50, 100, None), StepStatus::Finish);
340 assert_eq!(effective_status(100, 100, None), StepStatus::Process);
341 }
342
343 #[test]
344 fn effective_status_index_equals_current() {
345 assert_eq!(effective_status(0, 0, None), StepStatus::Process);
347 assert_eq!(effective_status(1, 1, None), StepStatus::Process);
348 assert_eq!(effective_status(99, 99, None), StepStatus::Process);
349 }
350
351 #[test]
352 fn effective_status_index_less_than_current() {
353 assert_eq!(effective_status(0, 1, None), StepStatus::Finish);
355 assert_eq!(effective_status(5, 10, None), StepStatus::Finish);
356 assert_eq!(effective_status(98, 99, None), StepStatus::Finish);
357 }
358
359 #[test]
360 fn effective_status_index_greater_than_current() {
361 assert_eq!(effective_status(1, 0, None), StepStatus::Wait);
363 assert_eq!(effective_status(10, 5, None), StepStatus::Wait);
364 assert_eq!(effective_status(99, 98, None), StepStatus::Wait);
365 }
366
367 #[test]
368 fn step_status_equality() {
369 assert_eq!(StepStatus::Wait, StepStatus::Wait);
370 assert_eq!(StepStatus::Process, StepStatus::Process);
371 assert_eq!(StepStatus::Finish, StepStatus::Finish);
372 assert_eq!(StepStatus::Error, StepStatus::Error);
373 assert_ne!(StepStatus::Wait, StepStatus::Process);
374 assert_ne!(StepStatus::Finish, StepStatus::Error);
375 }
376
377 #[test]
378 fn step_status_clone() {
379 let original = StepStatus::Error;
380 let cloned = original;
381 assert_eq!(original, cloned);
382 assert_eq!(original.as_class(), cloned.as_class());
383 }
384
385 #[test]
386 fn step_status_debug() {
387 let wait = StepStatus::Wait;
388 let process = StepStatus::Process;
389 let finish = StepStatus::Finish;
390 let error = StepStatus::Error;
391
392 let wait_str = format!("{:?}", wait);
393 let process_str = format!("{:?}", process);
394 let finish_str = format!("{:?}", finish);
395 let error_str = format!("{:?}", error);
396
397 assert!(wait_str.contains("Wait"));
398 assert!(process_str.contains("Process"));
399 assert!(finish_str.contains("Finish"));
400 assert!(error_str.contains("Error"));
401 }
402
403 #[test]
404 fn steps_direction_clone() {
405 let original = StepsDirection::Vertical;
406 let cloned = original;
407 assert_eq!(original, cloned);
408 assert_eq!(original.as_class(), cloned.as_class());
409 }
410
411 #[test]
412 fn steps_direction_debug() {
413 let horizontal = StepsDirection::Horizontal;
414 let vertical = StepsDirection::Vertical;
415
416 let h_str = format!("{:?}", horizontal);
417 let v_str = format!("{:?}", vertical);
418
419 assert!(h_str.contains("Horizontal"));
420 assert!(v_str.contains("Vertical"));
421 }
422
423 #[test]
424 fn step_item_equality() {
425 let item1 = StepItem::new("key1", rsx!(div { "Title" }));
426 let item2 = StepItem::new("key1", rsx!(div { "Title" }));
427 let item3 = StepItem::new("key2", rsx!(div { "Title" }));
428
429 assert_eq!(item1.key, item2.key);
432 assert_ne!(item1.key, item3.key);
433 }
434
435 #[test]
436 fn step_item_with_all_fields() {
437 let mut item = StepItem::new("key1", rsx!(div { "Title" }));
438 item.description = Some(rsx!(div { "Description" }));
439 item.status = Some(StepStatus::Error);
440 item.disabled = true;
441
442 assert_eq!(item.key, "key1");
443 assert!(item.description.is_some());
444 assert_eq!(item.status, Some(StepStatus::Error));
445 assert_eq!(item.disabled, true);
446 }
447}