radix_leptos_primitives/components/
switch.rs

1use leptos::callback::Callback;
2use leptos::children::Children;
3use leptos::prelude::*;
4use crate::utils::{merge_optional_classes, generate_id};
5
6/// Switch component with proper accessibility and styling variants
7#[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/// Switch root component
43#[component]
44pub fn Switch(
45    /// Whether the switch is on
46    #[prop(optional, default = false)]
47    checked: bool,
48    /// Whether the switch is disabled
49    #[prop(optional, default = false)]
50    disabled: bool,
51    /// Switch styling variant
52    #[prop(optional, default = SwitchVariant::Default)]
53    variant: SwitchVariant,
54    /// Switch size
55    #[prop(optional, default = SwitchSize::Default)]
56    size: SwitchSize,
57    /// CSS classes
58    #[prop(optional)]
59    class: Option<String>,
60    /// CSS styles
61    #[prop(optional)]
62    style: Option<String>,
63    /// Checked change event handler
64    #[prop(optional)]
65    onchecked_change: Option<Callback<bool>>,
66    /// Child content
67    children: Children,
68) -> impl IntoView {
69    let switch_id = generate_id("switch");
70    let thumb_id = generate_id("switch-thumb");
71
72    // Build data attributes for styling
73    let data_variant = variant.as_str();
74    let data_size = size.as_str();
75
76    // Merge classes with data attributes for CSS targeting
77    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    // Handle keyboard navigation
82    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    // Handle click
95    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/// Switch Thumb component
122#[component]
123pub fn SwitchThumb(
124    /// CSS classes
125    #[prop(optional)]
126    class: Option<String>,
127    /// CSS styles
128    #[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    // 1. Basic Rendering Tests
151    #[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    // 2. Props Validation Tests
178    #[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    // 3. State Management Tests
224    #[test]
225    fn test_switch_state_changes() {
226        run_test(|| {
227            let mut checked = false;
228            let disabled = false;
229
230            // Initial state
231            assert!(!checked);
232            assert!(!disabled);
233
234            // Turn on switch
235            checked = true;
236
237            assert!(checked);
238            assert!(!disabled);
239
240            // Turn off switch
241            checked = false;
242
243            assert!(!checked);
244            assert!(!disabled);
245        });
246    }
247
248    // 4. Event Handling Tests
249    #[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    // 5. Accessibility Tests
286    #[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    // 6. Edge Case Tests
302    #[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            // Toggle on
323            checked = !checked;
324
325            assert!(checked);
326            assert!(!disabled);
327
328            // Toggle off
329            checked = !checked;
330
331            assert!(!checked);
332            assert!(!disabled);
333        });
334    }
335
336    // 7. Property-Based Tests
337    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            // Test that boolean properties are properly typed
357            assert!(matches!(checked, true | false));
358            assert!(matches!(disabled, true | false));
359
360            if disabled {
361                // Disabled switch should not be interactive
362            }
363        }
364    }
365
366    // Helper function for running tests
367    fn run_test<F>(f: F)
368    where
369        F: FnOnce(),
370    {
371        f();
372    }
373}