radix_leptos_primitives/components/
checkbox.rs

1use leptos::callback::Callback;
2use leptos::children::Children;
3use leptos::prelude::*;
4use crate::utils::{merge_optional_classes, generate_id};
5
6/// Checkbox component with proper accessibility and styling variants
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum CheckboxVariant {
9    Default,
10    Destructive,
11    Ghost,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum CheckboxSize {
16    Default,
17    Sm,
18    Lg,
19}
20
21impl CheckboxVariant {
22    pub fn as_str(&self) -> &'static str {
23        match self {
24            CheckboxVariant::Default => "default",
25            CheckboxVariant::Destructive => "destructive",
26            CheckboxVariant::Ghost => "ghost",
27        }
28    }
29}
30
31impl CheckboxSize {
32    pub fn as_str(&self) -> &'static str {
33        match self {
34            CheckboxSize::Default => "default",
35            CheckboxSize::Sm => "sm",
36            CheckboxSize::Lg => "lg",
37        }
38    }
39}
40
41
42/// Checkbox root component
43#[component]
44pub fn Checkbox(
45    /// Whether the checkbox is checked
46    #[prop(optional, default = false)]
47    checked: bool,
48    /// Whether the checkbox is indeterminate
49    #[prop(optional, default = false)]
50    indeterminate: bool,
51    /// Whether the checkbox is disabled
52    #[prop(optional, default = false)]
53    disabled: bool,
54    /// Checkbox styling variant
55    #[prop(optional, default = CheckboxVariant::Default)]
56    variant: CheckboxVariant,
57    /// Checkbox size
58    #[prop(optional, default = CheckboxSize::Default)]
59    size: CheckboxSize,
60    /// CSS classes
61    #[prop(optional)]
62    class: Option<String>,
63    /// CSS styles
64    #[prop(optional)]
65    style: Option<String>,
66    /// Checked change event handler
67    #[prop(optional)]
68    onchecked_change: Option<Callback<bool>>,
69    /// Indeterminate change event handler
70    #[prop(optional)]
71    _onindeterminate_change: Option<Callback<bool>>,
72    /// Child content
73    children: Children,
74) -> impl IntoView {
75    let checkbox_id = generate_id("checkbox");
76    let label_id = generate_id("checkbox-label");
77
78    // Build data attributes for styling
79    let data_variant = variant.as_str();
80    let data_size = size.as_str();
81
82    // Merge classes with data attributes for CSS targeting
83    let base_classes = "radix-checkbox";
84    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
85        .unwrap_or_else(|| base_classes.to_string());
86
87    // Handle keyboard navigation
88    let handle_keydown = move |e: web_sys::KeyboardEvent| match e.key().as_str() {
89        " " | "Enter" => {
90            e.prevent_default();
91            if !disabled {
92                if let Some(onchecked_change) = onchecked_change {
93                    onchecked_change.run(!checked);
94                }
95            }
96        }
97        _ => {}
98    };
99
100    // Handle click
101    let handle_click = move |e: web_sys::MouseEvent| {
102        e.prevent_default();
103        if !disabled {
104            if let Some(onchecked_change) = onchecked_change {
105                onchecked_change.run(!checked);
106            }
107        }
108    };
109
110    view! {
111        <div
112            class=combined_class
113            style=style
114            data-variant=data_variant
115            data-size=data_size
116            data-checked=checked
117            data-indeterminate=indeterminate
118            data-disabled=disabled
119            on:keydown=handle_keydown
120        >
121            <input
122                id=checkbox_id.clone()
123                type="checkbox"
124                checked=checked
125                disabled=disabled
126                tabindex="-1"
127                aria-hidden="true"
128            />
129            <label
130                id=label_id
131                for=checkbox_id.clone()
132                class="radix-checkbox-label"
133                on:click=handle_click
134            >
135                {children()}
136            </label>
137        </div>
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use crate::{CheckboxSize, CheckboxVariant};
144    use proptest::prelude::*;
145use crate::utils::{merge_optional_classes, generate_id};
146
147    // 1. Basic Rendering Tests
148    #[test]
149    fn test_checkbox_variants() {
150        run_test(|| {
151            let variants = [
152                CheckboxVariant::Default,
153                CheckboxVariant::Destructive,
154                CheckboxVariant::Ghost,
155            ];
156
157            for variant in variants {
158                assert!(!variant.as_str().is_empty());
159            }
160        });
161    }
162
163    #[test]
164    fn test_checkbox_sizes() {
165        run_test(|| {
166            let sizes = [CheckboxSize::Default, CheckboxSize::Sm, CheckboxSize::Lg];
167
168            for size in sizes {
169                assert!(!size.as_str().is_empty());
170            }
171        });
172    }
173
174    // 2. Props Validation Tests
175    #[test]
176    fn test_checkboxchecked_state() {
177        run_test(|| {
178            let checked = true;
179            let indeterminate = false;
180            let disabled = false;
181            let variant = CheckboxVariant::Default;
182            let size = CheckboxSize::Default;
183
184            assert!(checked);
185            assert!(!indeterminate);
186            assert!(!disabled);
187            assert_eq!(variant, CheckboxVariant::Default);
188            assert_eq!(size, CheckboxSize::Default);
189        });
190    }
191
192    #[test]
193    fn test_checkbox_unchecked_state() {
194        run_test(|| {
195            let checked = false;
196            let indeterminate = false;
197            let disabled = false;
198            let variant = CheckboxVariant::Destructive;
199            let size = CheckboxSize::Lg;
200
201            assert!(!checked);
202            assert!(!indeterminate);
203            assert!(!disabled);
204            assert_eq!(variant, CheckboxVariant::Destructive);
205            assert_eq!(size, CheckboxSize::Lg);
206        });
207    }
208
209    #[test]
210    fn test_checkboxindeterminate_state() {
211        run_test(|| {
212            let checked = false;
213            let indeterminate = true;
214            let disabled = false;
215            let variant = CheckboxVariant::Ghost;
216            let size = CheckboxSize::Sm;
217
218            assert!(!checked);
219            assert!(indeterminate);
220            assert!(!disabled);
221            assert_eq!(variant, CheckboxVariant::Ghost);
222            assert_eq!(size, CheckboxSize::Sm);
223        });
224    }
225
226    // 3. State Management Tests
227    #[test]
228    fn test_checkbox_state_changes() {
229        run_test(|| {
230            let mut checked = false;
231            let mut indeterminate = false;
232            let disabled = false;
233
234            assert!(!checked);
235            assert!(!indeterminate);
236            assert!(!disabled);
237
238            checked = true;
239            indeterminate = false;
240
241            assert!(checked);
242            assert!(!indeterminate);
243            assert!(!disabled);
244
245            checked = false;
246            indeterminate = false;
247
248            assert!(!checked);
249            assert!(!indeterminate);
250            assert!(!disabled);
251
252            checked = false;
253            indeterminate = true;
254
255            assert!(!checked);
256            assert!(indeterminate);
257            assert!(!disabled);
258        });
259    }
260
261    // 4. Event Handling Tests
262    #[test]
263    fn test_checkbox_keyboard_navigation() {
264        run_test(|| {
265            let space_pressed = true;
266            let enter_pressed = false;
267            let disabled = false;
268            let checked = false;
269
270            assert!(space_pressed);
271            assert!(!enter_pressed);
272            assert!(!disabled);
273            assert!(!checked);
274
275            space_pressed && !disabled;
276
277            if enter_pressed && !disabled {
278                panic!("Unexpected condition reached");
279            }
280        });
281    }
282
283    #[test]
284    fn test_checkbox_click_handling() {
285        run_test(|| {
286            let clicked = true;
287            let disabled = false;
288            let checked = false;
289
290            assert!(clicked);
291            assert!(!disabled);
292            assert!(!checked);
293
294            if clicked && !disabled {}
295        });
296    }
297
298    // 5. Accessibility Tests
299    #[test]
300    fn test_checkbox_accessibility() {
301        run_test(|| {
302            let role = "checkbox";
303            let ariachecked = "false";
304            let ariadisabled = "false";
305            let tabindex = "-1";
306
307            assert_eq!(role, "checkbox");
308            assert_eq!(ariachecked, "false");
309            assert_eq!(ariadisabled, "false");
310            assert_eq!(tabindex, "-1");
311        });
312    }
313
314    // 6. Edge Case Tests
315    #[test]
316    fn test_checkbox_edge_cases() {
317        run_test(|| {
318            let checked = true;
319            let indeterminate = true;
320            let disabled = false;
321
322            assert!(checked);
323            assert!(indeterminate);
324            assert!(!disabled);
325        });
326    }
327
328    #[test]
329    fn test_checkboxdisabled_state() {
330        run_test(|| {
331            let disabled = true;
332            let checked = false;
333            let indeterminate = false;
334
335            assert!(disabled);
336            assert!(!checked);
337            assert!(!indeterminate);
338        });
339    }
340
341    // 7. Property-Based Tests
342    proptest! {
343        #[test]
344        fn test_checkbox_properties(
345            variant in prop::sample::select(&[
346                CheckboxVariant::Default,
347                CheckboxVariant::Destructive,
348                CheckboxVariant::Ghost,
349            ]),
350            size in prop::sample::select(&[
351                CheckboxSize::Default,
352                CheckboxSize::Sm,
353                CheckboxSize::Lg,
354            ]),
355            checked in prop::bool::ANY,
356            indeterminate in prop::bool::ANY,
357            disabled in prop::bool::ANY
358        ) {
359            assert!(!variant.as_str().is_empty());
360            assert!(!size.as_str().is_empty());
361
362            // Test that boolean properties are properly typed
363            assert!(matches!(checked, true | false));
364            assert!(matches!(indeterminate, true | false));
365            assert!(matches!(disabled, true | false));
366
367            if disabled {
368                // Disabled checkbox should not be interactive
369            }
370
371            if indeterminate && checked {
372                // Indeterminate would take precedence
373            }
374        }
375    }
376
377    // Helper function for running tests
378    fn run_test<F>(f: F)
379    where
380        F: FnOnce(),
381    {
382        f();
383    }
384}