radix_leptos_primitives/components/
switch.rs1use leptos::*;
2use leptos::prelude::*;
3
4#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum SwitchVariant {
7 Default,
8 Destructive,
9 Ghost,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum SwitchSize {
14 Default,
15 Sm,
16 Lg,
17}
18
19impl SwitchVariant {
20 pub fn as_str(&self) -> &'static str {
21 match self {
22 SwitchVariant::Default => "default",
23 SwitchVariant::Destructive => "destructive",
24 SwitchVariant::Ghost => "ghost",
25 }
26 }
27}
28
29impl SwitchSize {
30 pub fn as_str(&self) -> &'static str {
31 match self {
32 SwitchSize::Default => "default",
33 SwitchSize::Sm => "sm",
34 SwitchSize::Lg => "lg",
35 }
36 }
37}
38
39fn generate_id(prefix: &str) -> String {
41 static COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
42 let id = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
43 format!("{}-{}", prefix, id)
44}
45
46fn merge_classes(existing: Option<&str>, additional: Option<&str>) -> Option<String> {
48 match (existing, additional) {
49 (Some(a), Some(b)) => Some(format!("{} {}", a, b)),
50 (Some(a), None) => Some(a.to_string()),
51 (None, Some(b)) => Some(b.to_string()),
52 (None, None) => None,
53 }
54}
55
56#[component]
58pub fn Switch(
59 #[prop(optional, default = false)]
61 checked: bool,
62 #[prop(optional, default = false)]
64 disabled: bool,
65 #[prop(optional, default = SwitchVariant::Default)]
67 variant: SwitchVariant,
68 #[prop(optional, default = SwitchSize::Default)]
70 size: SwitchSize,
71 #[prop(optional)]
73 class: Option<String>,
74 #[prop(optional)]
76 style: Option<String>,
77 #[prop(optional)]
79 on_checked_change: Option<Callback<bool>>,
80 children: Children,
82) -> impl IntoView {
83 let switch_id = generate_id("switch");
84 let thumb_id = generate_id("switch-thumb");
85
86 let data_variant = variant.as_str();
88 let data_size = size.as_str();
89
90 let base_classes = "radix-switch";
92 let combined_class = merge_classes(Some(base_classes), class.as_deref())
93 .unwrap_or_else(|| base_classes.to_string());
94
95 let handle_keydown = move |e: web_sys::KeyboardEvent| {
97 match e.key().as_str() {
98 " " | "Enter" => {
99 e.prevent_default();
100 if !disabled {
101 if let Some(on_checked_change) = on_checked_change {
102 on_checked_change.run(!checked);
103 }
104 }
105 }
106 _ => {}
107 }
108 };
109
110 let handle_click = move |e: web_sys::MouseEvent| {
112 e.prevent_default();
113 if !disabled {
114 if let Some(on_checked_change) = on_checked_change {
115 on_checked_change.run(!checked);
116 }
117 }
118 };
119
120 view! {
121 <div
122 class=combined_class
123 style=style
124 data-variant=data_variant
125 data-size=data_size
126 data-checked=checked
127 data-disabled=disabled
128 role="switch"
129 aria-checked=checked
130 aria-disabled=disabled
131 tabindex=if disabled { "-1" } else { "0" }
132 on:keydown=handle_keydown
133 on:click=handle_click
134 >
135 <input
136 id=switch_id.clone()
137 type="checkbox"
138 checked=checked
139 disabled=disabled
140 tabindex="-1"
141 aria-hidden="true"
142 />
143 <div class="radix-switch-track">
144 <div
145 id=thumb_id
146 class="radix-switch-thumb"
147 data-state=if checked { "checked" } else { "unchecked" }
148 >
149 </div>
150 </div>
151 {children()}
152 </div>
153 }
154}
155
156#[component]
158pub fn SwitchThumb(
159 #[prop(optional)]
161 class: Option<String>,
162 #[prop(optional)]
164 style: Option<String>,
165) -> impl IntoView {
166 let base_classes = "radix-switch-thumb";
167 let combined_class = merge_classes(Some(base_classes), class.as_deref())
168 .unwrap_or_else(|| base_classes.to_string());
169
170 view! {
171 <div
172 class=combined_class
173 style=style
174 >
175 </div>
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use proptest::prelude::*;
183
184 #[test]
186 fn test_switch_variants() {
187 run_test(|| {
188 let variants = vec![
189 SwitchVariant::Default,
190 SwitchVariant::Destructive,
191 SwitchVariant::Ghost,
192 ];
193
194 for variant in variants {
195 assert!(!variant.as_str().is_empty());
196 }
197 });
198 }
199
200 #[test]
201 fn test_switch_sizes() {
202 run_test(|| {
203 let sizes = vec![
204 SwitchSize::Default,
205 SwitchSize::Sm,
206 SwitchSize::Lg,
207 ];
208
209 for size in sizes {
210 assert!(!size.as_str().is_empty());
211 }
212 });
213 }
214
215 #[test]
217 fn test_switch_on_state() {
218 run_test(|| {
219 let checked = true;
220 let disabled = false;
221 let variant = SwitchVariant::Default;
222 let size = SwitchSize::Default;
223
224 assert!(checked);
225 assert!(!disabled);
226 assert_eq!(variant, SwitchVariant::Default);
227 assert_eq!(size, SwitchSize::Default);
228 });
229 }
230
231 #[test]
232 fn test_switch_off_state() {
233 run_test(|| {
234 let checked = false;
235 let disabled = false;
236 let variant = SwitchVariant::Destructive;
237 let size = SwitchSize::Lg;
238
239 assert!(!checked);
240 assert!(!disabled);
241 assert_eq!(variant, SwitchVariant::Destructive);
242 assert_eq!(size, SwitchSize::Lg);
243 });
244 }
245
246 #[test]
247 fn test_switch_disabled_state() {
248 run_test(|| {
249 let checked = false;
250 let disabled = true;
251 let variant = SwitchVariant::Ghost;
252 let size = SwitchSize::Sm;
253
254 assert!(!checked);
255 assert!(disabled);
256 assert_eq!(variant, SwitchVariant::Ghost);
257 assert_eq!(size, SwitchSize::Sm);
258 });
259 }
260
261 #[test]
263 fn test_switch_state_changes() {
264 run_test(|| {
265 let mut checked = false;
266 let disabled = false;
267
268 assert!(!checked);
270 assert!(!disabled);
271
272 checked = true;
274
275 assert!(checked);
276 assert!(!disabled);
277
278 checked = false;
280
281 assert!(!checked);
282 assert!(!disabled);
283 });
284 }
285
286 #[test]
288 fn test_switch_keyboard_navigation() {
289 run_test(|| {
290 let space_pressed = true;
291 let enter_pressed = false;
292 let disabled = false;
293 let checked = false;
294
295 assert!(space_pressed);
296 assert!(!enter_pressed);
297 assert!(!disabled);
298 assert!(!checked);
299
300 if space_pressed && !disabled {
301 assert!(true);
302 }
303
304 if enter_pressed && !disabled {
305 assert!(false);
306 }
307 });
308 }
309
310 #[test]
311 fn test_switch_click_handling() {
312 run_test(|| {
313 let clicked = true;
314 let disabled = false;
315 let checked = false;
316
317 assert!(clicked);
318 assert!(!disabled);
319 assert!(!checked);
320
321 if clicked && !disabled {
322 assert!(true);
323 }
324 });
325 }
326
327 #[test]
329 fn test_switch_accessibility() {
330 run_test(|| {
331 let role = "switch";
332 let aria_checked = "false";
333 let aria_disabled = "false";
334 let tabindex = "0";
335
336 assert_eq!(role, "switch");
337 assert_eq!(aria_checked, "false");
338 assert_eq!(aria_disabled, "false");
339 assert_eq!(tabindex, "0");
340 });
341 }
342
343 #[test]
345 fn test_switch_edge_cases() {
346 run_test(|| {
347 let checked = true;
348 let disabled = true;
349
350 assert!(checked);
351 assert!(disabled);
352 });
353 }
354
355 #[test]
356 fn test_switch_toggle_behavior() {
357 run_test(|| {
358 let mut checked = false;
359 let disabled = false;
360
361 assert!(!checked);
362 assert!(!disabled);
363
364 checked = !checked;
366
367 assert!(checked);
368 assert!(!disabled);
369
370 checked = !checked;
372
373 assert!(!checked);
374 assert!(!disabled);
375 });
376 }
377
378 proptest! {
380 #[test]
381 fn test_switch_properties(
382 variant in prop::sample::select(vec![
383 SwitchVariant::Default,
384 SwitchVariant::Destructive,
385 SwitchVariant::Ghost,
386 ]),
387 size in prop::sample::select(vec![
388 SwitchSize::Default,
389 SwitchSize::Sm,
390 SwitchSize::Lg,
391 ]),
392 checked in prop::bool::ANY,
393 disabled in prop::bool::ANY
394 ) {
395 assert!(!variant.as_str().is_empty());
396 assert!(!size.as_str().is_empty());
397
398 assert!(checked == true || checked == false);
399 assert!(disabled == true || disabled == false);
400
401 if disabled {
402 }
404 }
405 }
406
407 fn run_test<F>(f: F) where F: FnOnce() {
409 f();
410 }
411}