radix_leptos_primitives/components/
switch.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 SwitchVariant {
9 Default,
10 Destructive,
11 Ghost,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum SwitchSize {
16 Default,
17 Sm,
18 Lg,
19}
20
21impl SwitchVariant {
22 pub fn as_str(&self) -> &'static str {
23 match self {
24 SwitchVariant::Default => "default",
25 SwitchVariant::Destructive => "destructive",
26 SwitchVariant::Ghost => "ghost",
27 }
28 }
29}
30
31impl SwitchSize {
32 pub fn as_str(&self) -> &'static str {
33 match self {
34 SwitchSize::Default => "default",
35 SwitchSize::Sm => "sm",
36 SwitchSize::Lg => "lg",
37 }
38 }
39}
40
41
42#[component]
44pub fn Switch(
45 #[prop(optional, default = false)]
47 checked: bool,
48 #[prop(optional, default = false)]
50 disabled: bool,
51 #[prop(optional, default = SwitchVariant::Default)]
53 variant: SwitchVariant,
54 #[prop(optional, default = SwitchSize::Default)]
56 size: SwitchSize,
57 #[prop(optional)]
59 class: Option<String>,
60 #[prop(optional)]
62 style: Option<String>,
63 #[prop(optional)]
65 onchecked_change: Option<Callback<bool>>,
66 children: Children,
68) -> impl IntoView {
69 let switch_id = generate_id("switch");
70 let thumb_id = generate_id("switch-thumb");
71
72 let data_variant = variant.as_str();
74 let data_size = size.as_str();
75
76 let base_classes = "radix-switch";
78 let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
79 .unwrap_or_else(|| base_classes.to_string());
80
81 let handle_keydown = move |e: web_sys::KeyboardEvent| match e.key().as_str() {
83 " " | "Enter" => {
84 e.prevent_default();
85 if !disabled {
86 if let Some(onchecked_change) = onchecked_change {
87 onchecked_change.run(!checked);
88 }
89 }
90 }
91 _ => {}
92 };
93
94 let handle_click = move |e: web_sys::MouseEvent| {
96 e.prevent_default();
97 if !disabled {
98 if let Some(onchecked_change) = onchecked_change {
99 onchecked_change.run(!checked);
100 }
101 }
102 };
103
104 view! {
105 <div
106 class=combined_class
107 style=style
108 data-variant=data_variant
109 data-size=data_size
110 data-checked=checked
111 data-disabled=disabled
112 role="switch"
113 aria-checked=checked
114 aria-disabled=disabled
115 >
116 {children()}
117 </div>
118 }
119}
120
121#[component]
123pub fn SwitchThumb(
124 #[prop(optional)]
126 class: Option<String>,
127 #[prop(optional)]
129 style: Option<String>,
130) -> impl IntoView {
131 let base_classes = "radix-switch-thumb";
132 let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
133 .unwrap_or_else(|| base_classes.to_string());
134
135 view! {
136 <div
137 class=combined_class
138 style=style
139 >
140 </div>
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use crate::{SwitchSize, SwitchVariant};
147 use proptest::prelude::*;
148use crate::utils::{merge_optional_classes, generate_id};
149
150 #[test]
152 fn test_switch_variants() {
153 run_test(|| {
154 let variants = [
155 SwitchVariant::Default,
156 SwitchVariant::Destructive,
157 SwitchVariant::Ghost,
158 ];
159
160 for variant in variants {
161 assert!(!variant.as_str().is_empty());
162 }
163 });
164 }
165
166 #[test]
167 fn test_switch_sizes() {
168 run_test(|| {
169 let sizes = [SwitchSize::Default, SwitchSize::Sm, SwitchSize::Lg];
170
171 for size in sizes {
172 assert!(!size.as_str().is_empty());
173 }
174 });
175 }
176
177 #[test]
179 fn test_switch_on_state() {
180 run_test(|| {
181 let checked = true;
182 let disabled = false;
183 let variant = SwitchVariant::Default;
184 let size = SwitchSize::Default;
185
186 assert!(checked);
187 assert!(!disabled);
188 assert_eq!(variant, SwitchVariant::Default);
189 assert_eq!(size, SwitchSize::Default);
190 });
191 }
192
193 #[test]
194 fn test_switch_off_state() {
195 run_test(|| {
196 let checked = false;
197 let disabled = false;
198 let variant = SwitchVariant::Destructive;
199 let size = SwitchSize::Lg;
200
201 assert!(!checked);
202 assert!(!disabled);
203 assert_eq!(variant, SwitchVariant::Destructive);
204 assert_eq!(size, SwitchSize::Lg);
205 });
206 }
207
208 #[test]
209 fn test_switchdisabled_state() {
210 run_test(|| {
211 let checked = false;
212 let disabled = true;
213 let variant = SwitchVariant::Ghost;
214 let size = SwitchSize::Sm;
215
216 assert!(!checked);
217 assert!(disabled);
218 assert_eq!(variant, SwitchVariant::Ghost);
219 assert_eq!(size, SwitchSize::Sm);
220 });
221 }
222
223 #[test]
225 fn test_switch_state_changes() {
226 run_test(|| {
227 let mut checked = false;
228 let disabled = false;
229
230 assert!(!checked);
232 assert!(!disabled);
233
234 checked = true;
236
237 assert!(checked);
238 assert!(!disabled);
239
240 checked = false;
242
243 assert!(!checked);
244 assert!(!disabled);
245 });
246 }
247
248 #[test]
250 fn test_switch_keyboard_navigation() {
251 run_test(|| {
252 let space_pressed = true;
253 let enter_pressed = false;
254 let disabled = false;
255 let checked = false;
256
257 assert!(space_pressed);
258 assert!(!enter_pressed);
259 assert!(!disabled);
260 assert!(!checked);
261
262 space_pressed && !disabled;
263
264 if enter_pressed && !disabled {
265 panic!("Unexpected condition reached");
266 }
267 });
268 }
269
270 #[test]
271 fn test_switch_click_handling() {
272 run_test(|| {
273 let clicked = true;
274 let disabled = false;
275 let checked = false;
276
277 assert!(clicked);
278 assert!(!disabled);
279 assert!(!checked);
280
281 if clicked && !disabled {}
282 });
283 }
284
285 #[test]
287 fn test_switch_accessibility() {
288 run_test(|| {
289 let role = "switch";
290 let ariachecked = "false";
291 let ariadisabled = "false";
292 let tabindex = "0";
293
294 assert_eq!(role, "switch");
295 assert_eq!(ariachecked, "false");
296 assert_eq!(ariadisabled, "false");
297 assert_eq!(tabindex, "0");
298 });
299 }
300
301 #[test]
303 fn test_switch_edge_cases() {
304 run_test(|| {
305 let checked = true;
306 let disabled = true;
307
308 assert!(checked);
309 assert!(disabled);
310 });
311 }
312
313 #[test]
314 fn test_switch_toggle_behavior() {
315 run_test(|| {
316 let mut checked = false;
317 let disabled = false;
318
319 assert!(!checked);
320 assert!(!disabled);
321
322 checked = !checked;
324
325 assert!(checked);
326 assert!(!disabled);
327
328 checked = !checked;
330
331 assert!(!checked);
332 assert!(!disabled);
333 });
334 }
335
336 proptest! {
338 #[test]
339 fn test_switch_properties(
340 variant in prop::sample::select(&[
341 SwitchVariant::Default,
342 SwitchVariant::Destructive,
343 SwitchVariant::Ghost,
344 ]),
345 size in prop::sample::select(&[
346 SwitchSize::Default,
347 SwitchSize::Sm,
348 SwitchSize::Lg,
349 ]),
350 checked in prop::bool::ANY,
351 disabled in prop::bool::ANY
352 ) {
353 assert!(!variant.as_str().is_empty());
354 assert!(!size.as_str().is_empty());
355
356 assert!(matches!(checked, true | false));
358 assert!(matches!(disabled, true | false));
359
360 if disabled {
361 }
363 }
364 }
365
366 fn run_test<F>(f: F)
368 where
369 F: FnOnce(),
370 {
371 f();
372 }
373}