1use crate::components::control::{ControlStatus, push_status_class};
2use crate::components::form::{
3 form_value_to_bool, form_value_to_string_vec, use_form_item_control,
4};
5use dioxus::prelude::*;
6use serde_json::Value;
7
8#[derive(Clone)]
9struct CheckboxGroupContext {
10 selected: Signal<Vec<String>>,
11 disabled: bool,
12 controlled: bool,
13 on_change: Option<EventHandler<Vec<String>>>,
14}
15
16#[derive(Props, Clone, PartialEq)]
18pub struct CheckboxProps {
19 #[props(optional)]
21 pub checked: Option<bool>,
22 #[props(default)]
24 pub default_checked: bool,
25 #[props(default)]
27 pub indeterminate: bool,
28 #[props(default)]
29 pub disabled: bool,
30 #[props(optional)]
32 pub value: Option<String>,
33 #[props(optional)]
34 pub status: Option<ControlStatus>,
35 #[props(optional)]
36 pub class: Option<String>,
37 #[props(optional)]
38 pub style: Option<String>,
39 #[props(optional)]
40 pub on_change: Option<EventHandler<bool>>,
41 #[props(optional)]
43 pub children: Element,
44}
45
46#[component]
48pub fn Checkbox(props: CheckboxProps) -> Element {
49 let CheckboxProps {
50 checked,
51 default_checked,
52 indeterminate,
53 disabled,
54 value,
55 status,
56 class,
57 style,
58 on_change,
59 children,
60 } = props;
61
62 let form_control = use_form_item_control();
63 let group_ctx = try_use_context::<CheckboxGroupContext>();
64
65 let controlled_by_prop = checked.is_some();
66 let inner_checked = use_signal(|| default_checked);
67
68 let is_disabled = disabled
69 || group_ctx.as_ref().is_some_and(|ctx| ctx.disabled)
70 || form_control.as_ref().is_some_and(|ctx| ctx.is_disabled());
71
72 let is_checked = resolve_checked(
73 &group_ctx,
74 &form_control,
75 checked,
76 value.as_deref(),
77 inner_checked,
78 );
79
80 let mut class_list = vec!["adui-checkbox".to_string()];
81 if is_checked {
82 class_list.push("adui-checkbox-checked".into());
83 }
84 if indeterminate && !is_checked {
85 class_list.push("adui-checkbox-indeterminate".into());
86 }
87 if is_disabled {
88 class_list.push("adui-checkbox-disabled".into());
89 }
90 push_status_class(&mut class_list, status);
91 if let Some(extra) = class {
92 class_list.push(extra);
93 }
94 let class_attr = class_list.join(" ");
95 let style_attr = style.unwrap_or_default();
96
97 rsx! {
98 label {
99 class: "{class_attr}",
100 style: "{style_attr}",
101 role: "checkbox",
102 "aria-checked": is_checked,
103 "aria-disabled": is_disabled,
104 input {
105 class: "adui-checkbox-input",
106 r#type: "checkbox",
107 checked: is_checked,
108 disabled: is_disabled,
109 onclick: {
110 let group_for_click = group_ctx.clone();
111 let form_for_click = form_control.clone();
112 let value_for_click = value.clone();
113 let on_change_cb = on_change;
114 let mut inner_for_click = inner_checked;
115 move |_| {
116 if is_disabled {
117 return;
118 }
119 handle_checkbox_toggle(
120 &group_for_click,
121 &form_for_click,
122 controlled_by_prop,
123 &mut inner_for_click,
124 value_for_click.as_deref(),
125 on_change_cb,
126 );
127 }
128 },
129 }
130 span { class: "adui-checkbox-inner" }
131 span { {children} }
132 }
133 }
134}
135
136fn resolve_checked(
137 group_ctx: &Option<CheckboxGroupContext>,
138 form_control: &Option<crate::components::form::FormItemControlContext>,
139 prop_checked: Option<bool>,
140 value: Option<&str>,
141 inner: Signal<bool>,
142) -> bool {
143 if let Some(group) = group_ctx
144 && let Some(val) = value
145 {
146 return group.selected.read().contains(&val.to_string());
147 }
148 if let Some(ctx) = form_control {
149 return form_value_to_bool(ctx.value(), false);
150 }
151 if let Some(c) = prop_checked {
152 return c;
153 }
154 *inner.read()
155}
156
157fn handle_checkbox_toggle(
158 group_ctx: &Option<CheckboxGroupContext>,
159 form_control: &Option<crate::components::form::FormItemControlContext>,
160 controlled_by_prop: bool,
161 inner: &mut Signal<bool>,
162 value: Option<&str>,
163 on_change: Option<EventHandler<bool>>,
164) {
165 if let Some(group) = group_ctx {
167 if let Some(val) = value {
168 let next = {
170 let current = group.selected.read();
171 toggle_membership(¤t, val)
172 };
173 if let Some(cb) = group.on_change {
174 cb.call(next.clone());
175 }
176 if let Some(ctx) = form_control {
177 let json = Value::Array(next.iter().cloned().map(Value::String).collect());
178 ctx.set_value(json);
179 }
180 if !group.controlled {
181 let mut signal = group.selected;
182 signal.set(next);
183 }
184 }
185 return;
187 }
188
189 let next = if let Some(ctx) = form_control {
191 let current = form_value_to_bool(ctx.value(), false);
192 let next = !current;
193 ctx.set_value(Value::Bool(next));
194 next
195 } else {
196 let current = *inner.read();
197 let next = !current;
198 if !controlled_by_prop {
199 let mut state = *inner;
200 state.set(next);
201 }
202 next
203 };
204
205 if let Some(cb) = on_change {
206 cb.call(next);
207 }
208}
209
210fn toggle_membership(current: &[String], value: &str) -> Vec<String> {
213 let mut next = current.to_vec();
214 if let Some(pos) = next.iter().position(|v| v == value) {
215 next.remove(pos);
216 } else {
217 next.push(value.to_string());
218 }
219 next
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn toggle_membership_adds_when_missing_and_removes_when_present() {
228 let current: Vec<String> = vec![];
229 let next = toggle_membership(¤t, "a");
230 assert_eq!(next, vec!["a".to_string()]);
231
232 let next2 = toggle_membership(&next, "a");
233 assert!(next2.is_empty());
234 }
235}
236
237#[derive(Props, Clone, PartialEq)]
239pub struct CheckboxGroupProps {
240 #[props(optional)]
242 pub value: Option<Vec<String>>,
243 #[props(default)]
245 pub default_value: Vec<String>,
246 #[props(default)]
247 pub disabled: bool,
248 #[props(optional)]
249 pub class: Option<String>,
250 #[props(optional)]
251 pub style: Option<String>,
252 #[props(optional)]
253 pub on_change: Option<EventHandler<Vec<String>>>,
254 pub children: Element,
255}
256
257#[component]
259pub fn CheckboxGroup(props: CheckboxGroupProps) -> Element {
260 let CheckboxGroupProps {
261 value,
262 default_value,
263 disabled,
264 class,
265 style,
266 on_change,
267 children,
268 } = props;
269
270 let form_control = crate::components::form::use_form_item_control();
271
272 let controlled = value.is_some();
273 let selected = use_signal(|| {
274 if let Some(external) = value.clone() {
275 external
276 } else if let Some(ctx) = form_control.as_ref() {
277 form_value_to_string_vec(ctx.value())
278 } else {
279 default_value.clone()
280 }
281 });
282
283 {
285 let mut selected_signal = selected;
286 let external = value.clone();
287 let form_ctx = form_control.clone();
288 use_effect(move || {
289 if let Some(external_value) = external.clone() {
290 selected_signal.set(external_value);
291 } else if let Some(ctx) = form_ctx.as_ref() {
292 let next = form_value_to_string_vec(ctx.value());
293 selected_signal.set(next);
294 }
295 });
296 }
297
298 let ctx = CheckboxGroupContext {
299 selected,
300 disabled,
301 controlled,
302 on_change,
303 };
304 use_context_provider(|| ctx);
305
306 let mut class_list = vec!["adui-checkbox-group".to_string()];
307 if let Some(extra) = class {
308 class_list.push(extra);
309 }
310 let class_attr = class_list.join(" ");
311 let style_attr = style.unwrap_or_default();
312
313 rsx! {
314 div {
315 class: "{class_attr}",
316 style: "{style_attr}",
317 {children}
318 }
319 }
320}