adui_dioxus/components/
checkbox.rs

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/// Props for a single checkbox.
17#[derive(Props, Clone, PartialEq)]
18pub struct CheckboxProps {
19    /// Controlled checked state.
20    #[props(optional)]
21    pub checked: Option<bool>,
22    /// Initial state in uncontrolled mode.
23    #[props(default)]
24    pub default_checked: bool,
25    /// Visual indeterminate state (mainly for \"select all\" scenarios).
26    #[props(default)]
27    pub indeterminate: bool,
28    #[props(default)]
29    pub disabled: bool,
30    /// Value used when inside a CheckboxGroup.
31    #[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    /// Optional label content rendered to the right of the box.
42    #[props(optional)]
43    pub children: Element,
44}
45
46/// Ant Design flavored checkbox.
47#[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    // Group mode: toggle membership in selected set.
166    if let Some(group) = group_ctx {
167        if let Some(val) = value {
168            // Limit the scope of the read borrow so we can safely write later.
169            let next = {
170                let current = group.selected.read();
171                toggle_membership(&current, 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        // Group mode does not call individual on_change; the group callback is primary.
186        return;
187    }
188
189    // Simple checkbox: Form 模式下以 FormStore 为真相源,其他情况使用内部 state。
190    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
210/// Toggle a single value within a set of string values.
211/// If the value already exists it is removed, otherwise it is appended.
212fn 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(&current, "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/// Checkbox group props.
238#[derive(Props, Clone, PartialEq)]
239pub struct CheckboxGroupProps {
240    /// Controlled set of selected values.
241    #[props(optional)]
242    pub value: Option<Vec<String>>,
243    /// Initial selection in uncontrolled mode.
244    #[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/// Group container for multiple checkboxes.
258#[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    // Sync internal signal when controlled value or form value changes.
284    {
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}