radix_leptos_primitives/components/
toggle_group.rs

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