adui_dioxus/components/
spin.rs

1use dioxus::prelude::*;
2
3/// Size variants for the Spin component.
4#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
5pub enum SpinSize {
6    Small,
7    #[default]
8    Default,
9    Large,
10}
11
12/// Props for the Spin component (MVP subset).
13#[derive(Props, Clone, PartialEq)]
14pub struct SpinProps {
15    /// Whether the spin indicator is active. Defaults to true.
16    #[props(optional)]
17    pub spinning: Option<bool>,
18    /// Visual size of the indicator.
19    #[props(optional)]
20    pub size: Option<SpinSize>,
21    /// Optional text shown under the indicator.
22    #[props(optional)]
23    pub tip: Option<String>,
24    /// Extra class for the root element.
25    #[props(optional)]
26    pub class: Option<String>,
27    /// Inline style for the root element.
28    #[props(optional)]
29    pub style: Option<String>,
30    /// Whether to treat this as a fullscreen overlay. MVP only exposes
31    /// a class hook, concrete layout can be refined later.
32    #[props(default)]
33    pub fullscreen: bool,
34    /// Optional content wrapped by the spinner. When present, Spin will
35    /// render children and, when spinning, show a semi-transparent mask
36    /// with the indicator on top.
37    pub children: Element,
38}
39
40/// Ant Design flavored loading spinner (MVP).
41#[component]
42pub fn Spin(props: SpinProps) -> Element {
43    let SpinProps {
44        spinning,
45        size,
46        tip,
47        class,
48        style,
49        fullscreen,
50        children,
51    } = props;
52
53    let is_spinning = spinning.unwrap_or(true);
54    let size = size.unwrap_or_default();
55
56    // Build root class list.
57    let mut classes = vec!["adui-spin".to_string(), "adui-spin-nested".to_string()];
58    match size {
59        SpinSize::Small => classes.push("adui-spin-sm".into()),
60        SpinSize::Large => classes.push("adui-spin-lg".into()),
61        SpinSize::Default => {}
62    }
63    if fullscreen {
64        classes.push("adui-spin-fullscreen".into());
65    }
66    if let Some(extra) = class {
67        classes.push(extra);
68    }
69    let class_attr = classes.join(" ");
70    let style_attr = style.unwrap_or_default();
71
72    let tip_text = tip.unwrap_or_default();
73
74    // When not spinning we just render child content.
75    if !is_spinning {
76        return rsx! {
77            div { class: "{class_attr}", style: "{style_attr}",
78                div { class: "adui-spin-nested-container", {children} }
79            }
80        };
81    }
82
83    // Spinning: render child content with an overlay mask and indicator.
84    rsx! {
85        div { class: "{class_attr}", style: "{style_attr}",
86            div { class: "adui-spin-nested-container", {children} }
87            div { class: "adui-spin-nested-mask",
88                div { class: "adui-spin-indicator",
89                    span { class: "adui-spin-dot" }
90                }
91                if !tip_text.is_empty() {
92                    div { class: "adui-spin-text", "{tip_text}" }
93                }
94            }
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn spin_size_default() {
105        assert_eq!(SpinSize::default(), SpinSize::Default);
106    }
107
108    #[test]
109    fn spin_size_variants() {
110        assert_ne!(SpinSize::Small, SpinSize::Default);
111        assert_ne!(SpinSize::Default, SpinSize::Large);
112        assert_ne!(SpinSize::Small, SpinSize::Large);
113    }
114
115    #[test]
116    fn spin_size_equality() {
117        assert_eq!(SpinSize::Small, SpinSize::Small);
118        assert_eq!(SpinSize::Default, SpinSize::Default);
119        assert_eq!(SpinSize::Large, SpinSize::Large);
120    }
121
122    #[test]
123    fn spin_size_clone() {
124        let original = SpinSize::Small;
125        let cloned = original;
126        assert_eq!(original, cloned);
127    }
128
129    #[test]
130    fn spin_props_defaults() {
131        let props = SpinProps {
132            spinning: None,
133            size: None,
134            tip: None,
135            class: None,
136            style: None,
137            fullscreen: false,
138            children: rsx!(div {}),
139        };
140        assert!(props.spinning.is_none());
141        assert!(props.size.is_none());
142        assert!(props.tip.is_none());
143        assert_eq!(props.fullscreen, false);
144    }
145
146    #[test]
147    fn spin_size_all_variants() {
148        // Test all variants exist
149        let small = SpinSize::Small;
150        let default = SpinSize::Default;
151        let large = SpinSize::Large;
152
153        assert_ne!(small, default);
154        assert_ne!(default, large);
155        assert_ne!(small, large);
156    }
157
158    #[test]
159    fn spin_size_debug() {
160        let small = SpinSize::Small;
161        let default = SpinSize::Default;
162        let large = SpinSize::Large;
163
164        let small_str = format!("{:?}", small);
165        let default_str = format!("{:?}", default);
166        let large_str = format!("{:?}", large);
167
168        assert!(small_str.contains("Small"));
169        assert!(default_str.contains("Default"));
170        assert!(large_str.contains("Large"));
171    }
172
173    #[test]
174    fn spin_props_with_all_fields() {
175        let props = SpinProps {
176            spinning: Some(true),
177            size: Some(SpinSize::Large),
178            tip: Some("Loading...".into()),
179            class: Some("custom-class".into()),
180            style: Some("color: red;".into()),
181            fullscreen: true,
182            children: rsx!(div {}),
183        };
184        assert_eq!(props.spinning, Some(true));
185        assert_eq!(props.size, Some(SpinSize::Large));
186        assert_eq!(props.tip, Some("Loading...".into()));
187        assert_eq!(props.fullscreen, true);
188    }
189
190    #[test]
191    fn spin_props_spinning_false() {
192        let props = SpinProps {
193            spinning: Some(false),
194            size: None,
195            tip: None,
196            class: None,
197            style: None,
198            fullscreen: false,
199            children: rsx!(div {}),
200        };
201        assert_eq!(props.spinning, Some(false));
202    }
203
204    #[test]
205    fn spin_props_size_small() {
206        let props = SpinProps {
207            spinning: None,
208            size: Some(SpinSize::Small),
209            tip: None,
210            class: None,
211            style: None,
212            fullscreen: false,
213            children: rsx!(div {}),
214        };
215        assert_eq!(props.size, Some(SpinSize::Small));
216    }
217
218    #[test]
219    fn spin_props_size_default() {
220        let props = SpinProps {
221            spinning: None,
222            size: Some(SpinSize::Default),
223            tip: None,
224            class: None,
225            style: None,
226            fullscreen: false,
227            children: rsx!(div {}),
228        };
229        assert_eq!(props.size, Some(SpinSize::Default));
230    }
231
232    #[test]
233    fn spin_props_size_large() {
234        let props = SpinProps {
235            spinning: None,
236            size: Some(SpinSize::Large),
237            tip: None,
238            class: None,
239            style: None,
240            fullscreen: false,
241            children: rsx!(div {}),
242        };
243        assert_eq!(props.size, Some(SpinSize::Large));
244    }
245
246    #[test]
247    fn spin_props_with_tip() {
248        let props = SpinProps {
249            spinning: None,
250            size: None,
251            tip: Some("Please wait...".into()),
252            class: None,
253            style: None,
254            fullscreen: false,
255            children: rsx!(div {}),
256        };
257        assert_eq!(props.tip, Some("Please wait...".into()));
258    }
259
260    #[test]
261    fn spin_props_fullscreen() {
262        let props = SpinProps {
263            spinning: None,
264            size: None,
265            tip: None,
266            class: None,
267            style: None,
268            fullscreen: true,
269            children: rsx!(div {}),
270        };
271        assert_eq!(props.fullscreen, true);
272    }
273
274    #[test]
275    fn spin_props_all_combinations() {
276        // Spinning + Size + Tip
277        let props = SpinProps {
278            spinning: Some(true),
279            size: Some(SpinSize::Small),
280            tip: Some("Loading".into()),
281            class: None,
282            style: None,
283            fullscreen: false,
284            children: rsx!(div {}),
285        };
286        assert_eq!(props.spinning, Some(true));
287        assert_eq!(props.size, Some(SpinSize::Small));
288        assert_eq!(props.tip, Some("Loading".into()));
289
290        // Fullscreen + Large
291        let props = SpinProps {
292            spinning: Some(true),
293            size: Some(SpinSize::Large),
294            tip: None,
295            class: None,
296            style: None,
297            fullscreen: true,
298            children: rsx!(div {}),
299        };
300        assert_eq!(props.fullscreen, true);
301        assert_eq!(props.size, Some(SpinSize::Large));
302    }
303
304    #[test]
305    fn spin_props_clone() {
306        let props = SpinProps {
307            spinning: Some(true),
308            size: Some(SpinSize::Large),
309            tip: Some("Test".into()),
310            class: Some("test-class".into()),
311            style: Some("test-style".into()),
312            fullscreen: true,
313            children: rsx!(div {}),
314        };
315        let cloned = props.clone();
316        assert_eq!(props.spinning, cloned.spinning);
317        assert_eq!(props.size, cloned.size);
318        assert_eq!(props.tip, cloned.tip);
319        assert_eq!(props.fullscreen, cloned.fullscreen);
320    }
321
322    #[test]
323    fn spin_props_minimal() {
324        let props = SpinProps {
325            spinning: None,
326            size: None,
327            tip: None,
328            class: None,
329            style: None,
330            fullscreen: false,
331            children: rsx!(div {}),
332        };
333        // Verify all defaults
334        assert!(props.spinning.is_none());
335        assert!(props.size.is_none());
336        assert!(props.tip.is_none());
337        assert_eq!(props.fullscreen, false);
338    }
339}