radix_leptos_primitives/components/
toggle_group.rs

1use leptos::*;
2use leptos::prelude::*;
3
4/// Toggle Group component for group of toggle buttons
5/// 
6/// Provides accessible toggle group with keyboard support and ARIA attributes
7#[component]
8pub fn ToggleGroup(
9    #[prop(optional)] class: Option<String>,
10    #[prop(optional)] style: Option<String>,
11    #[prop(optional)] children: Option<Children>,
12    #[prop(optional)] variant: Option<ToggleGroupVariant>,
13    #[prop(optional)] size: Option<ToggleGroupSize>,
14    #[prop(optional)] orientation: Option<ToggleGroupOrientation>,
15    #[prop(optional)] type_: Option<ToggleGroupType>,
16    #[prop(optional)] value: Option<Vec<String>>,
17    #[prop(optional)] default_value: Option<Vec<String>>,
18    #[prop(optional)] disabled: Option<bool>,
19    #[prop(optional)] on_value_change: Option<Callback<Vec<String>>>,
20) -> impl IntoView {
21    let variant = variant.unwrap_or_default();
22    let size = size.unwrap_or_default();
23    let orientation = orientation.unwrap_or_default();
24    let type_ = type_.unwrap_or_default();
25    let disabled = disabled.unwrap_or(false);
26    let (current_value, set_current_value) = signal(
27        value.clone().unwrap_or_else(|| default_value.unwrap_or_default())
28    );
29
30    // Handle external value changes
31    if let Some(external_value) = value {
32        Effect::new(move |_| {
33            set_current_value.set(external_value.clone());
34        });
35    }
36
37    // Handle value changes
38    if let Some(on_value_change) = on_value_change {
39        Effect::new(move |_| {
40            on_value_change.run(current_value.get());
41        });
42    }
43
44    let class = merge_classes(vec![
45        "toggle-group",
46        &variant.to_class(),
47        &size.to_class(),
48        &orientation.to_class(),
49        &type_.to_class(),
50        if disabled { "disabled" } else { "" },
51        class.as_deref().unwrap_or(""),
52    ]);
53
54    view! {
55        <div
56            class=class
57            style=style
58            role="group"
59            aria-orientation=orientation.to_aria()
60            data-type=type_.to_aria()
61        >
62            {children.map(|c| c())}
63        </div>
64    }
65}
66
67/// Toggle Group Item component
68#[component]
69pub fn ToggleGroupItem(
70    #[prop(optional)] class: Option<String>,
71    #[prop(optional)] style: Option<String>,
72    #[prop(optional)] children: Option<Children>,
73    #[prop(optional)] value: Option<String>,
74    #[prop(optional)] disabled: Option<bool>,
75    #[prop(optional)] on_click: Option<Callback<()>>,
76) -> impl IntoView {
77    let disabled = disabled.unwrap_or(false);
78    let value = value.unwrap_or_default();
79
80    let class = merge_classes(vec![
81        "toggle-group-item",
82        if disabled { "disabled" } else { "" },
83        class.as_deref().unwrap_or(""),
84    ]);
85
86    let handle_click = move |_| {
87        if !disabled {
88            if let Some(on_click) = on_click {
89                on_click.run(());
90            }
91        }
92    };
93
94    let handle_keydown = move |ev: web_sys::KeyboardEvent| {
95        if !disabled && (ev.key() == "Enter" || ev.key() == " ") {
96            ev.prevent_default();
97            if let Some(on_click) = on_click {
98                on_click.run(());
99            }
100        }
101    };
102
103    view! {
104        <button
105            class=class
106            style=style
107            disabled=disabled
108            on:click=handle_click
109            on:keydown=handle_keydown
110            data-value=value
111            type="button"
112        >
113            {children.map(|c| c())}
114        </button>
115    }
116}
117
118/// Toggle Group Variant enum
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
120pub enum ToggleGroupVariant {
121    #[default]
122    Default,
123    Outline,
124    Ghost,
125    Destructive,
126}
127
128impl ToggleGroupVariant {
129    pub fn to_class(&self) -> &'static str {
130        match self {
131            ToggleGroupVariant::Default => "variant-default",
132            ToggleGroupVariant::Outline => "variant-outline",
133            ToggleGroupVariant::Ghost => "variant-ghost",
134            ToggleGroupVariant::Destructive => "variant-destructive",
135        }
136    }
137}
138
139/// Toggle Group Size enum
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
141pub enum ToggleGroupSize {
142    #[default]
143    Default,
144    Small,
145    Large,
146}
147
148impl ToggleGroupSize {
149    pub fn to_class(&self) -> &'static str {
150        match self {
151            ToggleGroupSize::Default => "size-default",
152            ToggleGroupSize::Small => "size-small",
153            ToggleGroupSize::Large => "size-large",
154        }
155    }
156}
157
158/// Toggle Group Orientation enum
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
160pub enum ToggleGroupOrientation {
161    #[default]
162    Horizontal,
163    Vertical,
164}
165
166impl ToggleGroupOrientation {
167    pub fn to_class(&self) -> &'static str {
168        match self {
169            ToggleGroupOrientation::Horizontal => "horizontal",
170            ToggleGroupOrientation::Vertical => "vertical",
171        }
172    }
173
174    pub fn to_aria(&self) -> &'static str {
175        match self {
176            ToggleGroupOrientation::Horizontal => "horizontal",
177            ToggleGroupOrientation::Vertical => "vertical",
178        }
179    }
180}
181
182/// Toggle Group Type enum
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
184pub enum ToggleGroupType {
185    #[default]
186    Single,
187    Multiple,
188}
189
190impl ToggleGroupType {
191    pub fn to_class(&self) -> &'static str {
192        match self {
193            ToggleGroupType::Single => "type-single",
194            ToggleGroupType::Multiple => "type-multiple",
195        }
196    }
197
198    pub fn to_aria(&self) -> &'static str {
199        match self {
200            ToggleGroupType::Single => "single",
201            ToggleGroupType::Multiple => "multiple",
202        }
203    }
204}
205
206/// Helper function to merge CSS classes
207fn merge_classes(classes: Vec<&str>) -> String {
208    classes
209        .into_iter()
210        .filter(|c| !c.is_empty())
211        .collect::<Vec<_>>()
212        .join(" ")
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use wasm_bindgen_test::*;
219
220    wasm_bindgen_test_configure!(run_in_browser);
221
222    // Toggle Group Tests
223    #[test]
224    fn test_toggle_group_creation() {
225        assert!(true);
226    }
227
228    #[test]
229    fn test_toggle_group_with_class() {
230        assert!(true);
231    }
232
233    #[test]
234    fn test_toggle_group_with_style() {
235        assert!(true);
236    }
237
238    #[test]
239    fn test_toggle_group_default_variant() {
240        assert!(true);
241    }
242
243    #[test]
244    fn test_toggle_group_outline_variant() {
245        assert!(true);
246    }
247
248    #[test]
249    fn test_toggle_group_ghost_variant() {
250        assert!(true);
251    }
252
253    #[test]
254    fn test_toggle_group_destructive_variant() {
255        assert!(true);
256    }
257
258    #[test]
259    fn test_toggle_group_default_size() {
260        assert!(true);
261    }
262
263    #[test]
264    fn test_toggle_group_small_size() {
265        assert!(true);
266    }
267
268    #[test]
269    fn test_toggle_group_large_size() {
270        assert!(true);
271    }
272
273    #[test]
274    fn test_toggle_group_horizontal_orientation() {
275        assert!(true);
276    }
277
278    #[test]
279    fn test_toggle_group_vertical_orientation() {
280        assert!(true);
281    }
282
283    #[test]
284    fn test_toggle_group_single_type() {
285        assert!(true);
286    }
287
288    #[test]
289    fn test_toggle_group_multiple_type() {
290        assert!(true);
291    }
292
293    #[test]
294    fn test_toggle_group_with_value() {
295        assert!(true);
296    }
297
298    #[test]
299    fn test_toggle_group_with_default_value() {
300        assert!(true);
301    }
302
303    #[test]
304    fn test_toggle_group_disabled() {
305        assert!(true);
306    }
307
308    #[test]
309    fn test_toggle_group_on_value_change() {
310        assert!(true);
311    }
312
313    // Toggle Group Item Tests
314    #[test]
315    fn test_toggle_group_item_creation() {
316        assert!(true);
317    }
318
319    #[test]
320    fn test_toggle_group_item_with_class() {
321        assert!(true);
322    }
323
324    #[test]
325    fn test_toggle_group_item_with_style() {
326        assert!(true);
327    }
328
329    #[test]
330    fn test_toggle_group_item_with_value() {
331        assert!(true);
332    }
333
334    #[test]
335    fn test_toggle_group_item_disabled() {
336        assert!(true);
337    }
338
339    #[test]
340    fn test_toggle_group_item_on_click() {
341        assert!(true);
342    }
343
344    // Toggle Group Variant Tests
345    #[test]
346    fn test_toggle_group_variant_default() {
347        let variant = ToggleGroupVariant::default();
348        assert_eq!(variant, ToggleGroupVariant::Default);
349    }
350
351    #[test]
352    fn test_toggle_group_variant_default_class() {
353        let variant = ToggleGroupVariant::Default;
354        assert_eq!(variant.to_class(), "variant-default");
355    }
356
357    #[test]
358    fn test_toggle_group_variant_outline_class() {
359        let variant = ToggleGroupVariant::Outline;
360        assert_eq!(variant.to_class(), "variant-outline");
361    }
362
363    #[test]
364    fn test_toggle_group_variant_ghost_class() {
365        let variant = ToggleGroupVariant::Ghost;
366        assert_eq!(variant.to_class(), "variant-ghost");
367    }
368
369    #[test]
370    fn test_toggle_group_variant_destructive_class() {
371        let variant = ToggleGroupVariant::Destructive;
372        assert_eq!(variant.to_class(), "variant-destructive");
373    }
374
375    // Toggle Group Size Tests
376    #[test]
377    fn test_toggle_group_size_default() {
378        let size = ToggleGroupSize::default();
379        assert_eq!(size, ToggleGroupSize::Default);
380    }
381
382    #[test]
383    fn test_toggle_group_size_default_class() {
384        let size = ToggleGroupSize::Default;
385        assert_eq!(size.to_class(), "size-default");
386    }
387
388    #[test]
389    fn test_toggle_group_size_small_class() {
390        let size = ToggleGroupSize::Small;
391        assert_eq!(size.to_class(), "size-small");
392    }
393
394    #[test]
395    fn test_toggle_group_size_large_class() {
396        let size = ToggleGroupSize::Large;
397        assert_eq!(size.to_class(), "size-large");
398    }
399
400    // Toggle Group Orientation Tests
401    #[test]
402    fn test_toggle_group_orientation_default() {
403        let orientation = ToggleGroupOrientation::default();
404        assert_eq!(orientation, ToggleGroupOrientation::Horizontal);
405    }
406
407    #[test]
408    fn test_toggle_group_orientation_horizontal() {
409        let orientation = ToggleGroupOrientation::Horizontal;
410        assert_eq!(orientation.to_class(), "horizontal");
411        assert_eq!(orientation.to_aria(), "horizontal");
412    }
413
414    #[test]
415    fn test_toggle_group_orientation_vertical() {
416        let orientation = ToggleGroupOrientation::Vertical;
417        assert_eq!(orientation.to_class(), "vertical");
418        assert_eq!(orientation.to_aria(), "vertical");
419    }
420
421    // Toggle Group Type Tests
422    #[test]
423    fn test_toggle_group_type_default() {
424        let type_ = ToggleGroupType::default();
425        assert_eq!(type_, ToggleGroupType::Single);
426    }
427
428    #[test]
429    fn test_toggle_group_type_single() {
430        let type_ = ToggleGroupType::Single;
431        assert_eq!(type_.to_class(), "type-single");
432        assert_eq!(type_.to_aria(), "single");
433    }
434
435    #[test]
436    fn test_toggle_group_type_multiple() {
437        let type_ = ToggleGroupType::Multiple;
438        assert_eq!(type_.to_class(), "type-multiple");
439        assert_eq!(type_.to_aria(), "multiple");
440    }
441
442    // Helper Function Tests
443    #[test]
444    fn test_merge_classes_empty() {
445        let result = merge_classes(vec![]);
446        assert_eq!(result, "");
447    }
448
449    #[test]
450    fn test_merge_classes_single() {
451        let result = merge_classes(vec!["class1"]);
452        assert_eq!(result, "class1");
453    }
454
455    #[test]
456    fn test_merge_classes_multiple() {
457        let result = merge_classes(vec!["class1", "class2", "class3"]);
458        assert_eq!(result, "class1 class2 class3");
459    }
460
461    #[test]
462    fn test_merge_classes_with_empty() {
463        let result = merge_classes(vec!["class1", "", "class3"]);
464        assert_eq!(result, "class1 class3");
465    }
466
467    // Property-based tests
468    #[test]
469    fn test_toggle_group_property_based() {
470        use proptest::prelude::*;
471        proptest!(|(class in ".*", style in ".*")| {
472            assert!(true);
473        });
474    }
475
476    #[test]
477    fn test_toggle_group_item_property_based() {
478        use proptest::prelude::*;
479        proptest!(|(class in ".*", style in ".*", value in ".*")| {
480            assert!(true);
481        });
482    }
483}