Skip to main content

stratum_components/data_display/
spinner.rs

1//! Loading spinner component.
2
3use crate::common::{Size, merge_classes};
4use stratum_core::aria::{AriaAttributes, AriaRole};
5use stratum_core::render::RenderOutput;
6
7/// Properties for the Spinner component.
8#[derive(Debug, Clone, PartialEq)]
9pub struct SpinnerProps {
10    pub size: Size,
11    pub class: Option<String>,
12    pub aria_label: Option<String>,
13}
14
15impl Default for SpinnerProps {
16    fn default() -> Self {
17        Self {
18            size: Size::default(),
19            class: None,
20            aria_label: Some("Loading".to_string()),
21        }
22    }
23}
24
25pub struct Spinner;
26
27impl Spinner {
28    const BASE: &'static str =
29        "animate-spin rounded-full border-2 border-current border-t-transparent";
30
31    pub fn classes(props: &SpinnerProps) -> String {
32        let size_cls = match props.size {
33            Size::Xs => "h-3 w-3",
34            Size::Sm => "h-4 w-4",
35            Size::Md => "h-6 w-6",
36            Size::Lg => "h-8 w-8",
37            Size::Xl => "h-12 w-12",
38        };
39
40        let computed = format!("{} {}", Self::BASE, size_cls);
41        merge_classes(&computed, &props.class)
42    }
43
44    pub fn render(props: &SpinnerProps) -> RenderOutput {
45        let classes = Self::classes(props);
46        let mut aria = AriaAttributes::new().with_role(AriaRole::Status);
47        aria.busy = Some(true);
48
49        if let Some(ref label) = props.aria_label {
50            aria = aria.with_label(label.clone());
51        }
52
53        RenderOutput::new()
54            .with_tag("div")
55            .with_class(classes)
56            .with_aria(aria)
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn spinner_default_classes() {
66        let props = SpinnerProps::default();
67        let classes = Spinner::classes(&props);
68        assert!(classes.contains("animate-spin"));
69        assert!(classes.contains("h-6 w-6"));
70    }
71
72    #[test]
73    fn spinner_small() {
74        let props = SpinnerProps {
75            size: Size::Sm,
76            ..Default::default()
77        };
78        let classes = Spinner::classes(&props);
79        assert!(classes.contains("h-4 w-4"));
80    }
81
82    #[test]
83    fn spinner_aria() {
84        let props = SpinnerProps::default();
85        let output = Spinner::render(&props);
86        assert_eq!(output.aria.role, Some(AriaRole::Status));
87        assert_eq!(output.aria.busy, Some(true));
88        assert_eq!(output.aria.label, Some("Loading".to_string()));
89    }
90}