stratum_components/forms/
select.rs1use crate::common::{Size, merge_classes};
4use stratum_core::aria::{AriaAttributes, AriaHasPopup, AriaRole};
5use stratum_core::render::{AttrValue, RenderOutput};
6
7#[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}