stratum_components/forms/
checkbox.rs1use crate::common::{Size, merge_classes};
4use stratum_core::aria::{AriaAttributes, AriaRole, TriState};
5use stratum_core::render::{AttrValue, RenderOutput};
6
7#[derive(Debug, Clone, PartialEq)]
9pub struct CheckboxProps {
10 pub size: Size,
11 pub checked: TriState,
12 pub disabled: bool,
13 pub required: bool,
14 pub class: Option<String>,
15 pub aria_label: Option<String>,
16 pub id: Option<String>,
17}
18
19impl Default for CheckboxProps {
20 fn default() -> Self {
21 Self {
22 size: Size::default(),
23 checked: TriState::False,
24 disabled: false,
25 required: false,
26 class: None,
27 aria_label: None,
28 id: None,
29 }
30 }
31}
32
33pub struct Checkbox;
34
35impl Checkbox {
36 const BASE: &'static str = "peer shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground";
37
38 pub fn classes(props: &CheckboxProps) -> String {
39 let size_cls = match props.size {
40 Size::Xs => "h-3 w-3",
41 Size::Sm => "h-3.5 w-3.5",
42 Size::Md => "h-4 w-4",
43 Size::Lg => "h-5 w-5",
44 Size::Xl => "h-6 w-6",
45 };
46
47 let computed = format!("{} {}", Self::BASE, size_cls);
48 merge_classes(&computed, &props.class)
49 }
50
51 pub fn render(props: &CheckboxProps) -> RenderOutput {
52 let classes = Self::classes(props);
53 let mut aria = AriaAttributes::new()
54 .with_role(AriaRole::Checkbox)
55 .with_checked(props.checked);
56
57 if let Some(ref label) = props.aria_label {
58 aria = aria.with_label(label.clone());
59 }
60 if props.disabled {
61 aria = aria.with_disabled(true);
62 }
63 if props.required {
64 aria.required = Some(true);
65 }
66
67 let state_str = match props.checked {
68 TriState::True => "checked",
69 TriState::False => "unchecked",
70 TriState::Mixed => "indeterminate",
71 };
72
73 let mut output = RenderOutput::new()
74 .with_tag("button")
75 .with_class(classes)
76 .with_aria(aria)
77 .with_attr("type", AttrValue::String("button".to_string()))
78 .with_data("state", state_str);
79
80 if props.disabled {
81 output = output.with_attr("disabled", AttrValue::Bool(true));
82 }
83 if let Some(ref id) = props.id {
84 output = output.with_attr("id", AttrValue::String(id.clone()));
85 }
86
87 output
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Default)]
93pub struct CheckboxGroupProps {
94 pub class: Option<String>,
95 pub aria_label: Option<String>,
96}
97
98pub struct CheckboxGroup;
99
100impl CheckboxGroup {
101 pub fn classes(props: &CheckboxGroupProps) -> String {
102 let computed = "flex flex-col gap-2";
103 merge_classes(computed, &props.class)
104 }
105
106 pub fn render(props: &CheckboxGroupProps) -> RenderOutput {
107 let classes = Self::classes(props);
108 let mut aria = AriaAttributes::new().with_role(AriaRole::Group);
109 if let Some(ref label) = props.aria_label {
110 aria = aria.with_label(label.clone());
111 }
112
113 RenderOutput::new()
114 .with_tag("div")
115 .with_class(classes)
116 .with_aria(aria)
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn default_checkbox_classes() {
126 let props = CheckboxProps::default();
127 let classes = Checkbox::classes(&props);
128 assert!(classes.contains("rounded-sm"));
129 assert!(classes.contains("h-4 w-4"));
130 }
131
132 #[test]
133 fn render_checked_state() {
134 let props = CheckboxProps {
135 checked: TriState::True,
136 ..Default::default()
137 };
138 let output = Checkbox::render(&props);
139 assert_eq!(output.aria.checked, Some(TriState::True));
140 assert!(
141 output
142 .data_attrs
143 .iter()
144 .any(|(k, v)| k == "state" && v == "checked")
145 );
146 }
147
148 #[test]
149 fn render_indeterminate() {
150 let props = CheckboxProps {
151 checked: TriState::Mixed,
152 ..Default::default()
153 };
154 let output = Checkbox::render(&props);
155 assert_eq!(output.aria.checked, Some(TriState::Mixed));
156 }
157
158 #[test]
159 fn checkbox_group_render() {
160 let props = CheckboxGroupProps::default();
161 let output = CheckboxGroup::render(&props);
162 assert_eq!(output.effective_tag(), "div");
163 assert_eq!(output.aria.role, Some(AriaRole::Group));
164 }
165}