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    let id = crate::use_unique_id();
28    let id_clone = id.clone();
29
30    // HTML's default checkbox input are notoriously difficult to style consistently across browsers.
31    // This one uses a common pattern using a fake box for look and real input for sementics and
32    // form integration, aria-hidden to prevent duplication.
33    // The hidden native input ensures that assistive technologies (like screen readers) still
34    // recognize the component as a proper checkbox.
35    use_effect(move || {
36        let checked = *checked.read();
37        let js = document::eval(
38            r#"
39            let id = await dioxus.recv();
40            let action = await dioxus.recv();
41            let input = document.getElementById(id);
42
43            switch(action) {
44                case "checked":
45                    input.checked = true;
46                    input.indeterminate = false;
47                    break;
48                case "unchecked": 
49                    input.checked = false;
50                    input.indeterminate = false;
51                    break;
52            }
53            "#,
54        );
55
56        let _ = js.send(id_clone.clone());
57        let _ = js.send(if checked { "checked" } else { "unchecked" });
58    });
59
60    rsx! {
61        button {
62            type: "button",
63            role: "checkbox",
64            "data-checked": if *checked.read() { "checked" } else { "unchecked" },
65            onclick: move |event| {
66                let new_checked = !checked();
67                checked.set(new_checked);
68                props.checked.set(new_checked);
69                if props.onchange.call(new_checked) {
70                    event.stop_propagation();
71                }
72            },
73
74            // Aria says only spacebar can change state of checkboxes.
75            onkeydown: move |e| {
76                if e.key() == Key::Enter {
77                    e.prevent_default();
78                }
79            },
80            ..props.attributes,
81            span { class: "checkbox-indicator",
82                if *checked.read() {
83                    Icon {
84                        icon: Icons::Check
85                    }
86                }
87            }
88        }
89        input {
90            id,
91            type: "checkbox",
92            aria_hidden: "true",
93            tabindex: "-1",
94            position: "absolute",
95            pointer_events: "none",
96            opacity: "0",
97            margin: "0",
98            transform: "translateX(-100%)",
99        }
100    }
101}