adui_dioxus/components/
skeleton.rs

1use dioxus::prelude::*;
2
3/// Props for the Skeleton component (MVP subset).
4#[derive(Props, Clone, PartialEq)]
5pub struct SkeletonProps {
6    /// Whether to display the skeleton. When Some(false), render children instead.
7    #[props(optional)]
8    pub loading: Option<bool>,
9    /// Whether to show active animation.
10    #[props(default)]
11    pub active: bool,
12    /// Whether to render a title block.
13    #[props(default = true)]
14    pub title: bool,
15    /// Number of paragraph lines.
16    #[props(optional)]
17    pub paragraph_rows: Option<u8>,
18    /// Extra root class.
19    #[props(optional)]
20    pub class: Option<String>,
21    /// Inline styles for the root.
22    #[props(optional)]
23    pub style: Option<String>,
24    /// Wrapped content. When `loading = Some(false)`, this content is rendered
25    /// instead of the skeleton blocks.
26    #[props(optional)]
27    pub content: Option<Element>,
28}
29
30/// Simple Ant Design flavored Skeleton.
31#[component]
32pub fn Skeleton(props: SkeletonProps) -> Element {
33    let SkeletonProps {
34        loading,
35        active,
36        title,
37        paragraph_rows,
38        class,
39        style,
40        content,
41    } = props;
42
43    let is_loading = loading.unwrap_or(true);
44
45    if !is_loading {
46        if let Some(node) = content {
47            return node;
48        }
49        return rsx! {};
50    }
51
52    let mut classes = vec!["adui-skeleton".to_string()];
53    if active {
54        classes.push("adui-skeleton-active".into());
55    }
56    if let Some(extra) = class {
57        classes.push(extra);
58    }
59    let class_attr = classes.join(" ");
60    let style_attr = style.unwrap_or_default();
61
62    let rows = paragraph_rows.unwrap_or(3).max(1);
63
64    rsx! {
65        div { class: "{class_attr}", style: "{style_attr}",
66            if title {
67                div { class: "adui-skeleton-title" }
68            }
69            div { class: "adui-skeleton-paragraph",
70                {(0..rows).map(|idx| {
71                    let mut line_class = "adui-skeleton-paragraph-line".to_string();
72                    if idx == rows - 1 {
73                        line_class.push_str(" adui-skeleton-paragraph-line-last");
74                    }
75                    rsx! {
76                        div { class: "{line_class}" }
77                    }
78                })}
79            }
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn skeleton_props_defaults() {
90        let props = SkeletonProps {
91            loading: None,
92            active: false,
93            title: true,
94            paragraph_rows: None,
95            class: None,
96            style: None,
97            content: None,
98        };
99        assert!(props.loading.is_none());
100        assert_eq!(props.active, false);
101        assert_eq!(props.title, true);
102    }
103
104    #[test]
105    fn skeleton_props_loading() {
106        let props = SkeletonProps {
107            loading: Some(true),
108            active: false,
109            title: true,
110            paragraph_rows: None,
111            class: None,
112            style: None,
113            content: None,
114        };
115        assert_eq!(props.loading, Some(true));
116    }
117
118    #[test]
119    fn skeleton_props_active() {
120        let props = SkeletonProps {
121            loading: None,
122            active: true,
123            title: true,
124            paragraph_rows: None,
125            class: None,
126            style: None,
127            content: None,
128        };
129        assert_eq!(props.active, true);
130    }
131
132    #[test]
133    fn skeleton_props_title() {
134        let props = SkeletonProps {
135            loading: None,
136            active: false,
137            title: false,
138            paragraph_rows: None,
139            class: None,
140            style: None,
141            content: None,
142        };
143        assert_eq!(props.title, false);
144    }
145
146    #[test]
147    fn skeleton_props_paragraph_rows() {
148        let props = SkeletonProps {
149            loading: None,
150            active: false,
151            title: true,
152            paragraph_rows: Some(5),
153            class: None,
154            style: None,
155            content: None,
156        };
157        assert_eq!(props.paragraph_rows, Some(5));
158    }
159
160    #[test]
161    fn skeleton_paragraph_rows_minimum() {
162        // Test paragraph rows minimum value logic
163        let rows = 0u8;
164        let min_rows = rows.max(1);
165        assert_eq!(min_rows, 1);
166
167        let rows2 = 3u8;
168        let min_rows2 = rows2.max(1);
169        assert_eq!(min_rows2, 3);
170    }
171
172    #[test]
173    fn skeleton_paragraph_rows_boundary_values() {
174        // Test various boundary values
175        assert_eq!(0u8.max(1), 1);
176        assert_eq!(1u8.max(1), 1);
177        assert_eq!(2u8.max(1), 2);
178        assert_eq!(255u8.max(1), 255);
179    }
180
181    #[test]
182    fn skeleton_props_loading_false() {
183        let props = SkeletonProps {
184            loading: Some(false),
185            active: false,
186            title: true,
187            paragraph_rows: None,
188            class: None,
189            style: None,
190            content: None,
191        };
192        assert_eq!(props.loading, Some(false));
193    }
194
195    #[test]
196    fn skeleton_props_all_combinations() {
197        // Active + Title
198        let props = SkeletonProps {
199            loading: None,
200            active: true,
201            title: true,
202            paragraph_rows: Some(5),
203            class: None,
204            style: None,
205            content: None,
206        };
207        assert_eq!(props.active, true);
208        assert_eq!(props.title, true);
209        assert_eq!(props.paragraph_rows, Some(5));
210
211        // Loading + Active
212        let props = SkeletonProps {
213            loading: Some(true),
214            active: true,
215            title: false,
216            paragraph_rows: None,
217            class: None,
218            style: None,
219            content: None,
220        };
221        assert_eq!(props.loading, Some(true));
222        assert_eq!(props.active, true);
223    }
224
225    #[test]
226    fn skeleton_props_with_class_and_style() {
227        let props = SkeletonProps {
228            loading: None,
229            active: false,
230            title: true,
231            paragraph_rows: None,
232            class: Some("custom-class".into()),
233            style: Some("color: red;".into()),
234            content: None,
235        };
236        assert_eq!(props.class, Some("custom-class".into()));
237        assert_eq!(props.style, Some("color: red;".into()));
238    }
239
240    #[test]
241    fn skeleton_props_clone() {
242        let props = SkeletonProps {
243            loading: Some(true),
244            active: true,
245            title: false,
246            paragraph_rows: Some(10),
247            class: Some("test".into()),
248            style: Some("test-style".into()),
249            content: None,
250        };
251        let cloned = props.clone();
252        assert_eq!(props.loading, cloned.loading);
253        assert_eq!(props.active, cloned.active);
254        assert_eq!(props.title, cloned.title);
255        assert_eq!(props.paragraph_rows, cloned.paragraph_rows);
256        assert_eq!(props.class, cloned.class);
257        assert_eq!(props.style, cloned.style);
258    }
259
260    #[test]
261    fn skeleton_props_paragraph_rows_edge_cases() {
262        // Minimum value
263        let props = SkeletonProps {
264            loading: None,
265            active: false,
266            title: true,
267            paragraph_rows: Some(1),
268            class: None,
269            style: None,
270            content: None,
271        };
272        assert_eq!(props.paragraph_rows, Some(1));
273
274        // Maximum u8 value
275        let props = SkeletonProps {
276            loading: None,
277            active: false,
278            title: true,
279            paragraph_rows: Some(255),
280            class: None,
281            style: None,
282            content: None,
283        };
284        assert_eq!(props.paragraph_rows, Some(255));
285    }
286
287    #[test]
288    fn skeleton_props_minimal() {
289        let props = SkeletonProps {
290            loading: None,
291            active: false,
292            title: true,
293            paragraph_rows: None,
294            class: None,
295            style: None,
296            content: None,
297        };
298        // Verify all defaults
299        assert!(props.loading.is_none());
300        assert_eq!(props.active, false);
301        assert_eq!(props.title, true);
302        assert!(props.paragraph_rows.is_none());
303    }
304
305    #[test]
306    fn skeleton_props_loading_none_means_true() {
307        // The component logic: loading.unwrap_or(true) means None defaults to true
308        // This test documents the behavior
309        let props = SkeletonProps {
310            loading: None,
311            active: false,
312            title: true,
313            paragraph_rows: None,
314            class: None,
315            style: None,
316            content: None,
317        };
318        assert!(props.loading.is_none());
319    }
320}