adui_dioxus/components/
steps.rs

1use crate::components::config_provider::ComponentSize;
2use dioxus::prelude::*;
3
4/// Visual status of an individual step.
5#[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/// Direction of the steps bar.
25#[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/// Data model for a single step item.
41#[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/// Props for the Steps component (MVP subset).
63#[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/// Ant Design flavored Steps (MVP: horizontal line steps with basic status).
96#[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}