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