Skip to main content

dioxus_tw_components/components/
checkbox.rs

1use crate::components::icon::*;
2use dioxus::prelude::*;
3
4#[derive(Clone, PartialEq, Props)]
5pub struct CheckboxProps {
6    #[props(extends = button, extends = GlobalAttributes)]
7    attributes: Vec<Attribute>,
8
9    #[props(optional)]
10    default_checked: bool,
11
12    #[props(optional)]
13    checked: Signal<bool>,
14
15    /// Return value determines if the event should strop propagation (false by default)
16    #[props(optional)]
17    onchange: Callback<bool, bool>,
18}
19
20#[component]
21pub fn Checkbox(mut props: CheckboxProps) -> Element {
22    let default_classes = "checkbox";
23    crate::setup_class_attribute(&mut props.attributes, default_classes);
24
25    let mut checked = use_signal(|| props.default_checked);
26
27    // Sync checked state when default_checked prop changes (e.g. readonly view with new data).
28    // peek() avoids subscription; write only when different to prevent re-render loops.
29    if *checked.peek() != props.default_checked {
30        *checked.write() = props.default_checked;
31    }
32
33    let id = crate::use_unique_id();
34    let id_clone = id.clone();
35
36    // HTML's default checkbox input are notoriously difficult to style consistently across browsers.
37    // This one uses a common pattern using a fake box for look and real input for sementics and
38    // form integration, aria-hidden to prevent duplication.
39    // The hidden native input ensures that assistive technologies (like screen readers) still
40    // recognize the component as a proper checkbox.
41    use_effect(move || {
42        let checked = *checked.read();
43        let js = document::eval(
44            r#"
45            let id = await dioxus.recv();
46            let action = await dioxus.recv();
47            let input = document.getElementById(id);
48
49            switch(action) {
50                case "checked":
51                    input.checked = true;
52                    input.indeterminate = false;
53                    break;
54                case "unchecked": 
55                    input.checked = false;
56                    input.indeterminate = false;
57                    break;
58            }
59            "#,
60        );
61
62        let _ = js.send(id_clone.clone());
63        let _ = js.send(if checked { "checked" } else { "unchecked" });
64    });
65
66    rsx! {
67        button {
68            type: "button",
69            role: "checkbox",
70            "data-checked": if *checked.read() { "checked" } else { "unchecked" },
71            onclick: move |event| {
72                let new_checked = !checked();
73                checked.set(new_checked);
74                props.checked.set(new_checked);
75                if props.onchange.call(new_checked) {
76                    event.stop_propagation();
77                }
78            },
79
80            // Aria says only spacebar can change state of checkboxes.
81            onkeydown: move |e| {
82                if e.key() == Key::Enter {
83                    e.prevent_default();
84                }
85            },
86            ..props.attributes,
87            span { class: "checkbox-indicator",
88                if *checked.read() {
89                    Icon {
90                        icon: Icons::Check
91                    }
92                }
93            }
94        }
95        input {
96            id,
97            type: "checkbox",
98            aria_hidden: "true",
99            tabindex: "-1",
100            position: "absolute",
101            pointer_events: "none",
102            opacity: "0",
103            margin: "0",
104            transform: "translateX(-100%)",
105        }
106    }
107}