radix_leptos_primitives/components/
toggle.rs

1use crate::utils::{merge_classes, generate_id};
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5
6/// Toggle component for toggle button functionality
7///
8/// Provides accessible toggle button with keyboard support and ARIA attributes
9#[component]
10pub fn Toggle(
11    #[prop(optional)] class: Option<String>,
12    #[prop(optional)] style: Option<String>,
13    #[prop(optional)] children: Option<Children>,
14    #[prop(optional)] variant: Option<ToggleVariant>,
15    #[prop(optional)] size: Option<ToggleSize>,
16    #[prop(optional)] pressed: Option<bool>,
17    #[prop(optional)] default_pressed: Option<bool>,
18    #[prop(optional)] disabled: Option<bool>,
19    #[prop(optional)] on_pressed_change: Option<Callback<bool>>,
20    #[prop(optional)] on_click: Option<Callback<()>>,
21) -> impl IntoView {
22    let variant = variant.unwrap_or_default();
23    let size = size.unwrap_or_default();
24    let disabled = disabled.unwrap_or(false);
25    let (is_pressed, set_is_pressed) =
26        signal(pressed.unwrap_or_else(|| default_pressed.unwrap_or(false)));
27
28    // Handle external pressed state changes
29    if let Some(external_pressed) = pressed {
30        Effect::new(move |_| {
31            set_is_pressed.set(external_pressed);
32        });
33    }
34
35    // Handle pressed state changes
36    if let Some(on_pressed_change) = on_pressed_change {
37        Effect::new(move |_| {
38            on_pressed_change.run(is_pressed.get());
39        });
40    }
41
42    let class = merge_classes(vec![
43        "toggle",
44        &variant.to_class(),
45        &size.to_class(),
46        class.as_deref().unwrap_or(""),
47    ]);
48
49    let handle_click = move |_| {
50        if !disabled {
51            set_is_pressed.update(|pressed| *pressed = !*pressed);
52            if let Some(on_click) = on_click {
53                on_click.run(());
54            }
55        }
56    };
57
58    let handle_keydown = move |ev: web_sys::KeyboardEvent| {
59        if !disabled && (ev.key() == "Enter" || ev.key() == " ") {
60            ev.prevent_default();
61            set_is_pressed.update(|pressed| *pressed = !*pressed);
62            if let Some(on_click) = on_click {
63                on_click.run(());
64            }
65        }
66    };
67
68    view! {
69        <button
70            class=class
71            style=style
72            disabled=disabled
73            on:click=handle_click
74            on:keydown=handle_keydown
75            aria-pressed=is_pressed.get()
76            type="button"
77        >
78            {children.map(|c| c())}
79        </button>
80    }
81}
82
83/// Toggle Variant enum
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub enum ToggleVariant {
86    #[default]
87    Default,
88    Outline,
89    Ghost,
90    Destructive,
91}
92
93impl ToggleVariant {
94    pub fn to_class(&self) -> &'static str {
95        match self {
96            ToggleVariant::Default => "variant-default",
97            ToggleVariant::Outline => "variant-outline",
98            ToggleVariant::Ghost => "variant-ghost",
99            ToggleVariant::Destructive => "variant-destructive",
100        }
101    }
102}
103
104/// Toggle Size enum
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
106pub enum ToggleSize {
107    #[default]
108    Default,
109    Small,
110    Large,
111}
112
113impl ToggleSize {
114    pub fn to_class(&self) -> &'static str {
115        match self {
116            ToggleSize::Default => "size-default",
117            ToggleSize::Small => "size-small",
118            ToggleSize::Large => "size-large",
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use crate::utils::{merge_classes, generate_id};
126    use crate::{ToggleSize, ToggleVariant};
127    use wasm_bindgen_test::*;
128
129    wasm_bindgen_test_configure!(run_in_browser);
130
131    // Toggle Tests
132    #[test]
133    fn test_toggle_creation() {}
134
135    #[test]
136    fn test_toggle_with_class() {}
137
138    #[test]
139    fn test_toggle_with_style() {}
140
141    #[test]
142    fn test_toggle_default_variant() {}
143
144    #[test]
145    fn test_toggle_outline_variant() {}
146
147    #[test]
148    fn test_toggle_ghost_variant() {}
149
150    #[test]
151    fn test_toggle_destructive_variant() {}
152
153    #[test]
154    fn test_toggle_default_size() {}
155
156    #[test]
157    fn test_toggle_small_size() {}
158
159    #[test]
160    fn test_toggle_large_size() {}
161
162    #[test]
163    fn test_toggle_pressed() {}
164
165    #[test]
166    fn test_toggle_default_pressed() {}
167
168    #[test]
169    fn test_toggledisabled() {}
170
171    #[test]
172    fn test_toggle_on_pressed_change() {}
173
174    #[test]
175    fn test_toggle_on_click() {}
176
177    // Toggle Variant Tests
178    #[test]
179    fn test_toggle_variant_default() {
180        let variant = ToggleVariant::default();
181        assert_eq!(variant, ToggleVariant::Default);
182    }
183
184    #[test]
185    fn test_toggle_variant_default_class() {
186        let variant = ToggleVariant::Default;
187        assert_eq!(variant.to_class(), "variant-default");
188    }
189
190    #[test]
191    fn test_toggle_variant_outline_class() {
192        let variant = ToggleVariant::Outline;
193        assert_eq!(variant.to_class(), "variant-outline");
194    }
195
196    #[test]
197    fn test_toggle_variant_ghost_class() {
198        let variant = ToggleVariant::Ghost;
199        assert_eq!(variant.to_class(), "variant-ghost");
200    }
201
202    #[test]
203    fn test_toggle_variant_destructive_class() {
204        let variant = ToggleVariant::Destructive;
205        assert_eq!(variant.to_class(), "variant-destructive");
206    }
207
208    // Toggle Size Tests
209    #[test]
210    fn test_toggle_size_default() {
211        let size = ToggleSize::default();
212        assert_eq!(size, ToggleSize::Default);
213    }
214
215    #[test]
216    fn test_toggle_size_default_class() {
217        let size = ToggleSize::Default;
218        assert_eq!(size.to_class(), "size-default");
219    }
220
221    #[test]
222    fn test_toggle_size_small_class() {
223        let size = ToggleSize::Small;
224        assert_eq!(size.to_class(), "size-small");
225    }
226
227    #[test]
228    fn test_toggle_size_large_class() {
229        let size = ToggleSize::Large;
230        assert_eq!(size.to_class(), "size-large");
231    }
232
233    // Helper Function Tests
234    #[test]
235    fn test_merge_classes_empty() {
236        let result = merge_classes(Vec::new());
237        assert_eq!(result, "");
238    }
239
240    #[test]
241    fn test_merge_classes_single() {
242        let result = merge_classes(vec!["class1"]);
243        assert_eq!(result, "class1");
244    }
245
246    #[test]
247    fn test_merge_classes_multiple() {
248        let result = merge_classes(vec!["class1", "class2", "class3"]);
249        assert_eq!(result, "class1 class2 class3");
250    }
251
252    #[test]
253    fn test_merge_classes_with_empty() {
254        let result = merge_classes(vec!["class1", "", "class3"]);
255        assert_eq!(result, "class1 class3");
256    }
257
258    // Property-based tests
259    #[test]
260    fn test_toggle_property_based() {
261        use proptest::prelude::*;
262        proptest!(|(____class in ".*", __style in ".*")| {
263
264        });
265    }
266}