Skip to main content

stratum_components/forms/
select.rs

1//! Styled Select dropdown component.
2
3use crate::common::{Size, merge_classes};
4use stratum_core::aria::{AriaAttributes, AriaHasPopup, AriaRole};
5use stratum_core::render::{AttrValue, RenderOutput};
6
7/// Properties for the styled Select trigger.
8#[derive(Debug, Clone, PartialEq, Default)]
9pub struct SelectProps {
10    pub size: Size,
11    pub disabled: bool,
12    pub required: bool,
13    pub placeholder: Option<String>,
14    pub open: bool,
15    pub class: Option<String>,
16    pub aria_label: Option<String>,
17    pub id: Option<String>,
18}
19
20pub struct Select;
21
22impl Select {
23    const BASE: &'static str = "flex w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1";
24
25    pub fn classes(props: &SelectProps) -> String {
26        let size_cls = match props.size {
27            Size::Xs => "h-7 text-xs",
28            Size::Sm => "h-8 text-xs",
29            Size::Md => "h-9 text-sm",
30            Size::Lg => "h-10 text-base",
31            Size::Xl => "h-12 text-lg",
32        };
33
34        let computed = format!("{} {}", Self::BASE, size_cls);
35        merge_classes(&computed, &props.class)
36    }
37
38    pub fn render(props: &SelectProps) -> RenderOutput {
39        let classes = Self::classes(props);
40        let mut aria = AriaAttributes::new()
41            .with_role(AriaRole::Combobox)
42            .with_expanded(props.open)
43            .with_haspopup(AriaHasPopup::ListBox);
44
45        if let Some(ref label) = props.aria_label {
46            aria = aria.with_label(label.clone());
47        }
48        if props.disabled {
49            aria = aria.with_disabled(true);
50        }
51        if props.required {
52            aria.required = Some(true);
53        }
54
55        let mut output = RenderOutput::new()
56            .with_tag("button")
57            .with_class(classes)
58            .with_aria(aria)
59            .with_attr("type", AttrValue::String("button".to_string()));
60
61        if props.disabled {
62            output = output.with_attr("disabled", AttrValue::Bool(true));
63        }
64        if let Some(ref id) = props.id {
65            output = output.with_attr("id", AttrValue::String(id.clone()));
66        }
67
68        output
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn default_select_classes() {
78        let props = SelectProps::default();
79        let classes = Select::classes(&props);
80        assert!(classes.contains("rounded-md"));
81        assert!(classes.contains("h-9"));
82    }
83
84    #[test]
85    fn render_open_select() {
86        let props = SelectProps {
87            open: true,
88            ..Default::default()
89        };
90        let output = Select::render(&props);
91        assert_eq!(output.aria.expanded, Some(true));
92    }
93
94    #[test]
95    fn render_closed_select() {
96        let props = SelectProps::default();
97        let output = Select::render(&props);
98        assert_eq!(output.aria.expanded, Some(false));
99    }
100
101    #[test]
102    fn render_has_combobox_role() {
103        let props = SelectProps::default();
104        let output = Select::render(&props);
105        assert_eq!(output.aria.role, Some(AriaRole::Combobox));
106    }
107}