radix_leptos_primitives/components/
menubar.rs

1use crate::utils::merge_classes;
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5
6/// Menubar component for menu bar with keyboard navigation
7///
8/// Provides accessible menu bar with keyboard support and ARIA attributes
9#[component]
10pub fn Menubar(
11    #[prop(optional)] class: Option<String>,
12    #[prop(optional)] style: Option<String>,
13    #[prop(optional)] children: Option<Children>,
14    #[prop(optional)] orientation: Option<MenubarOrientation>,
15    #[prop(optional)] default_value: Option<String>,
16    #[prop(optional)] value: Option<ReadSignal<String>>,
17    #[prop(optional)] on_value_change: Option<Callback<String>>,
18) -> impl IntoView {
19    let orientation = orientation.unwrap_or_default();
20    let (current_value, setcurrent_value) = signal(
21        value
22            .map(|v| v.get())
23            .unwrap_or_else(|| default_value.unwrap_or_default()),
24    );
25
26    // Handle value changes
27    if let Some(on_change) = on_value_change {
28        Effect::new(move |_| {
29            on_change.run(current_value.get());
30        });
31    }
32
33    // Handle external value changes
34    if let Some(external_value) = value {
35        Effect::new(move |_| {
36            setcurrent_value.set(external_value.get());
37        });
38    }
39
40    let class = merge_classes(vec![
41        "menubar",
42        &orientation.to_class(),
43        class.as_deref().unwrap_or(""),
44    ]);
45
46    view! {
47        <div
48            class=class
49            style=style
50            role="menubar"
51            aria-orientation=orientation.to_aria()
52        >
53            {children.map(|c| c())}
54        </div>
55    }
56}
57
58/// Menubar Menu component
59#[component]
60pub fn MenubarMenu(
61    #[prop(optional)] class: Option<String>,
62    #[prop(optional)] style: Option<String>,
63    #[prop(optional)] children: Option<Children>,
64    #[prop(optional)] value: Option<String>,
65    #[prop(optional)] disabled: Option<bool>,
66    #[prop(optional)] on_select: Option<Callback<()>>,
67) -> impl IntoView {
68    let disabled = disabled.unwrap_or(false);
69    let value = value.unwrap_or_default();
70
71    let class = merge_classes(vec!["menubar-menu"]);
72
73    let handle_keydown = move |ev: web_sys::KeyboardEvent| {
74        if !disabled && (ev.key() == "Enter" || ev.key() == " ") {
75            ev.prevent_default();
76            if let Some(on_select) = on_select {
77                on_select.run(());
78            }
79        }
80    };
81
82    view! {
83        <div
84            class=class
85            style=style
86            role="none"
87        >
88        </div>
89    }
90}
91
92/// Menubar Trigger component
93#[component]
94pub fn MenubarTrigger(
95    #[prop(optional)] class: Option<String>,
96    #[prop(optional)] style: Option<String>,
97    #[prop(optional)] children: Option<Children>,
98    #[prop(optional)] disabled: Option<bool>,
99    #[prop(optional)] on_click: Option<Callback<()>>,
100) -> impl IntoView {
101    let disabled = disabled.unwrap_or(false);
102
103    let class = merge_classes(vec!["menubar-trigger"]);
104
105    let handle_keydown = move |ev: web_sys::KeyboardEvent| {
106        if !disabled && (ev.key() == "Enter" || ev.key() == " ") {
107            ev.prevent_default();
108            if let Some(on_click) = on_click {
109                on_click.run(());
110            }
111        }
112    };
113
114    let handle_click = move |_| {
115        if !disabled {
116            if let Some(on_click) = on_click {
117                on_click.run(());
118            }
119        }
120    };
121
122    view! {
123        <button
124            class=class
125            style=style
126            disabled=disabled
127            on:click=handle_click
128            on:keydown=handle_keydown
129            role="menuitem"
130            aria-haspopup="true"
131            aria-expanded="false"
132        >
133            {children.map(|c| c())}
134        </button>
135    }
136}
137
138/// Menubar Content component
139#[component]
140pub fn MenubarContent(
141    #[prop(optional)] class: Option<String>,
142    #[prop(optional)] style: Option<String>,
143    #[prop(optional)] children: Option<Children>,
144    #[prop(optional)] visible: Option<ReadSignal<bool>>,
145) -> impl IntoView {
146    let visible = visible.map(|v| v.get()).unwrap_or(true);
147
148    if !visible {
149        return view! { <></> }.into_any();
150    }
151
152    let class = merge_classes(vec!["menubar-content", class.as_deref().unwrap_or("")]);
153
154    view! {
155        <div
156            class=class
157            style=style
158            role="menu"
159            aria-hidden="false"
160        >
161            {children.map(|c| c())}
162        </div>
163    }
164    .into_any()
165}
166
167/// Menubar Item component
168#[component]
169pub fn MenubarItem(
170    #[prop(optional)] class: Option<String>,
171    #[prop(optional)] style: Option<String>,
172    #[prop(optional)] children: Option<Children>,
173    #[prop(optional)] disabled: Option<bool>,
174    #[prop(optional)] on_select: Option<Callback<()>>,
175) -> impl IntoView {
176    let disabled = disabled.unwrap_or(false);
177
178    let class = merge_classes(vec!["menubar-item"]);
179
180    let handle_keydown = move |ev: web_sys::KeyboardEvent| {
181        if !disabled && (ev.key() == "Enter" || ev.key() == " ") {
182            ev.prevent_default();
183            if let Some(on_select) = on_select {
184                on_select.run(());
185            }
186        }
187    };
188
189    view! {
190        <div
191            class=class
192            style=style
193            role="menuitem"
194        >
195        </div>
196    }
197}
198
199/// Menubar Separator component
200#[component]
201pub fn MenubarSeparator(
202    #[prop(optional)] class: Option<String>,
203    #[prop(optional)] style: Option<String>,
204) -> impl IntoView {
205    let class = merge_classes(vec!["menubar-separator", class.as_deref().unwrap_or("")]);
206
207    view! {
208        <div
209            class=class
210            style=style
211            role="separator"
212            aria-orientation="horizontal"
213        />
214    }
215}
216
217/// Menubar Group component
218#[component]
219pub fn MenubarGroup(
220    #[prop(optional)] class: Option<String>,
221    #[prop(optional)] style: Option<String>,
222    #[prop(optional)] children: Option<Children>,
223) -> impl IntoView {
224    let class = merge_classes(vec!["menubar-group", class.as_deref().unwrap_or("")]);
225
226    view! {
227        <div
228            class=class
229            style=style
230            role="group"
231        >
232            {children.map(|c| c())}
233        </div>
234    }
235}
236
237/// Menubar Label component
238#[component]
239pub fn MenubarLabel(
240    #[prop(optional)] class: Option<String>,
241    #[prop(optional)] style: Option<String>,
242    #[prop(optional)] children: Option<Children>,
243) -> impl IntoView {
244    let class = merge_classes(vec!["menubar-label", class.as_deref().unwrap_or("")]);
245
246    view! {
247        <div
248            class=class
249            style=style
250            role="presentation"
251        >
252            {children.map(|c| c())}
253        </div>
254    }
255}
256
257/// Menubar Orientation enum
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
259pub enum MenubarOrientation {
260    #[default]
261    Horizontal,
262    Vertical,
263}
264
265impl MenubarOrientation {
266    pub fn to_class(&self) -> &'static str {
267        match self {
268            MenubarOrientation::Horizontal => "horizontal",
269            MenubarOrientation::Vertical => "vertical",
270        }
271    }
272
273    pub fn to_aria(&self) -> &'static str {
274        match self {
275            MenubarOrientation::Horizontal => "horizontal",
276            MenubarOrientation::Vertical => "vertical",
277        }
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use crate::MenubarOrientation;
284    use wasm_bindgen_test::*;
285
286    wasm_bindgen_test_configure!(run_in_browser);
287
288    // Menubar Tests
289    #[test]
290    fn test_menubar_creation() {
291        // Test that the component can be created
292    }
293
294    #[test]
295    fn test_menubar_with_class() {
296        // Test that the component can be created with class
297    }
298
299    #[test]
300    fn test_menubar_with_style() {
301        // Test that the component can be created with style
302    }
303
304    #[test]
305    fn test_menubar_horizontal_orientation() {
306        // Test horizontal orientation
307    }
308
309    #[test]
310    fn test_menubar_vertical_orientation() {
311        // Test vertical orientation
312    }
313
314    #[test]
315    fn test_menubar_with_value() {
316        // Test with controlled value
317    }
318
319    #[test]
320    fn test_menubar_with_default_value() {
321        // Test with default value
322    }
323
324    #[test]
325    fn test_menubar_value_change_callback() {
326        // Test value change callback
327    }
328
329    // Menubar Menu Tests
330    #[test]
331    fn test_menubar_menu_creation() {
332        // Test that the component can be created
333    }
334
335    #[test]
336    fn test_menubar_menu_with_class() {
337        // Test that the component can be created with class
338    }
339
340    #[test]
341    fn test_menubar_menu_with_style() {
342        // Test that the component can be created with style
343    }
344
345    #[test]
346    fn test_menubar_menu_with_value() {
347        // Test with value
348    }
349
350    #[test]
351    fn test_menubar_menudisabled() {
352        // Test disabled state
353    }
354
355    #[test]
356    fn test_menubar_menu_on_select() {
357        // Test on_select callback
358    }
359
360    // Menubar Trigger Tests
361    #[test]
362    fn test_menubar_trigger_creation() {
363        // Test that the component can be created
364    }
365
366    #[test]
367    fn test_menubar_trigger_with_class() {
368        // Test that the component can be created with class
369    }
370
371    #[test]
372    fn test_menubar_trigger_with_style() {
373        // Test that the component can be created with style
374    }
375
376    #[test]
377    fn test_menubar_triggerdisabled() {
378        // Test disabled state
379    }
380
381    #[test]
382    fn test_menubar_trigger_on_click() {
383        // Test on_click callback
384    }
385
386    // Menubar Content Tests
387    #[test]
388    fn test_menubar_content_creation() {
389        // Test that the component can be created
390    }
391
392    #[test]
393    fn test_menubar_content_with_class() {
394        // Test that the component can be created with class
395    }
396
397    #[test]
398    fn test_menubar_content_with_style() {
399        // Test that the component can be created with style
400    }
401
402    #[test]
403    fn test_menubar_contentvisible() {
404        // Test visible state
405    }
406
407    #[test]
408    fn test_menubar_content_hidden() {
409        // Test hidden state
410    }
411
412    // Menubar Item Tests
413    #[test]
414    fn test_menubar_item_creation() {
415        // Test that the component can be created
416    }
417
418    #[test]
419    fn test_menubar_item_with_class() {
420        // Test that the component can be created with class
421    }
422
423    #[test]
424    fn test_menubar_item_with_style() {
425        // Test that the component can be created with style
426    }
427
428    #[test]
429    fn test_menubar_itemdisabled() {
430        // Test disabled state
431    }
432
433    #[test]
434    fn test_menubar_item_on_select() {
435        // Test on_select callback
436    }
437
438    // Menubar Separator Tests
439    #[test]
440    fn test_menubar_separator_creation() {
441        // Test that the component can be created
442    }
443
444    #[test]
445    fn test_menubar_separator_with_class() {
446        // Test that the component can be created with class
447    }
448
449    #[test]
450    fn test_menubar_separator_with_style() {
451        // Test that the component can be created with style
452    }
453
454    // Menubar Group Tests
455    #[test]
456    fn test_menubar_group_creation() {
457        // Test that the component can be created
458    }
459
460    #[test]
461    fn test_menubar_group_with_class() {
462        // Test that the component can be created with class
463    }
464
465    #[test]
466    fn test_menubar_group_with_style() {
467        // Test that the component can be created with style
468    }
469
470    // Menubar Label Tests
471    #[test]
472    fn test_menubar_label_creation() {
473        // Test that the component can be created
474    }
475
476    #[test]
477    fn test_menubar_label_with_class() {
478        // Test that the component can be created with class
479    }
480
481    #[test]
482    fn test_menubar_label_with_style() {
483        // Test that the component can be created with style
484    }
485
486    // Menubar Orientation Tests
487    #[test]
488    fn test_menubar_orientation_default() {
489        let orientation = MenubarOrientation::default();
490        assert_eq!(orientation, MenubarOrientation::Horizontal);
491    }
492
493    #[test]
494    fn test_menubar_orientation_horizontal() {
495        let orientation = MenubarOrientation::Horizontal;
496        assert_eq!(orientation.to_class(), "horizontal");
497        assert_eq!(orientation.to_aria(), "horizontal");
498    }
499
500    #[test]
501    fn test_menubar_orientation_vertical() {
502        let orientation = MenubarOrientation::Vertical;
503        assert_eq!(orientation.to_class(), "vertical");
504        assert_eq!(orientation.to_aria(), "vertical");
505    }
506
507    // Helper Function Tests
508    #[test]
509    fn test_merge_classes_empty() {
510        let result = crate::utils::merge_classes(Vec::new());
511        assert_eq!(result, "");
512    }
513
514    #[test]
515    fn test_merge_classes_single() {
516        let result = crate::utils::merge_classes(vec!["class1"]);
517        assert_eq!(result, "class1");
518    }
519
520    #[test]
521    fn test_merge_classes_multiple() {
522        let result = crate::utils::merge_classes(vec!["class1", "class2", "class3"]);
523        assert_eq!(result, "class1 class2 class3");
524    }
525
526    #[test]
527    fn test_merge_classes_with_empty() {
528        let result = crate::utils::merge_classes(vec!["class1", "", "class3"]);
529        assert_eq!(result, "class1 class3");
530    }
531
532    // Property-based tests
533    #[test]
534    fn test_menubar_property_based() {
535        use proptest::prelude::*;
536
537        proptest!(|(____class in ".*", __style in ".*")| {
538            // Test that the component can be created with various class and style values
539
540        });
541    }
542
543    #[test]
544    fn test_menubar_menu_property_based() {
545        use proptest::prelude::*;
546
547        proptest!(|(____class in ".*", __style in ".*", __value in ".*")| {
548            // Test that the component can be created with various prop values
549
550        });
551    }
552
553    #[test]
554    fn test_menubar_trigger_property_based() {
555        use proptest::prelude::*;
556
557        proptest!(|(____class in ".*", __style in ".*")| {
558            // Test that the component can be created with various class and style values
559
560        });
561    }
562
563    #[test]
564    fn test_menubar_content_property_based() {
565        use proptest::prelude::*;
566
567        proptest!(|(____class in ".*", __style in ".*")| {
568            // Test that the component can be created with various class and style values
569
570        });
571    }
572
573    #[test]
574    fn test_menubar_item_property_based() {
575        use proptest::prelude::*;
576
577        proptest!(|(____class in ".*", __style in ".*")| {
578            // Test that the component can be created with various class and style values
579
580        });
581    }
582
583    #[test]
584    fn test_menubar_separator_property_based() {
585        use proptest::prelude::*;
586
587        proptest!(|(____class in ".*", __style in ".*")| {
588            // Test that the component can be created with various class and style values
589
590        });
591    }
592
593    #[test]
594    fn test_menubar_group_property_based() {
595        use proptest::prelude::*;
596
597        proptest!(|(____class in ".*", __style in ".*")| {
598            // Test that the component can be created with various class and style values
599
600        });
601    }
602
603    #[test]
604    fn test_menubar_label_property_based() {
605        use proptest::prelude::*;
606
607        proptest!(|(____class in ".*", __style in ".*")| {
608            // Test that the component can be created with various class and style values
609
610        });
611    }
612}