radix_leptos_primitives/components/
checkbox.rs1use leptos::callback::Callback;
2use leptos::children::Children;
3use leptos::prelude::*;
4use crate::utils::{merge_optional_classes, generate_id};
5
6#[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#[component]
44pub fn Checkbox(
45 #[prop(optional, default = false)]
47 checked: bool,
48 #[prop(optional, default = false)]
50 indeterminate: bool,
51 #[prop(optional, default = false)]
53 disabled: bool,
54 #[prop(optional, default = CheckboxVariant::Default)]
56 variant: CheckboxVariant,
57 #[prop(optional, default = CheckboxSize::Default)]
59 size: CheckboxSize,
60 #[prop(optional)]
62 class: Option<String>,
63 #[prop(optional)]
65 style: Option<String>,
66 #[prop(optional)]
68 onchecked_change: Option<Callback<bool>>,
69 #[prop(optional)]
71 _onindeterminate_change: Option<Callback<bool>>,
72 children: Children,
74) -> impl IntoView {
75 let checkbox_id = generate_id("checkbox");
76 let label_id = generate_id("checkbox-label");
77
78 let data_variant = variant.as_str();
80 let data_size = size.as_str();
81
82 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 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 assert!(matches!(checked, true | false));
364 assert!(matches!(indeterminate, true | false));
365 assert!(matches!(disabled, true | false));
366
367 if disabled {
368 }
370
371 if indeterminate && checked {
372 }
374 }
375 }
376
377 fn run_test<F>(f: F)
379 where
380 F: FnOnce(),
381 {
382 f();
383 }
384}