radix_leptos_primitives/components/
menubar.rs

1use crate::utils::{merge_classes, generate_id};
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 {
150            let _: () = view! { <></> };
151            ().into_any()
152        };
153    }
154
155    let class = merge_classes(vec!["menubar-content", class.as_deref().unwrap_or("")]);
156
157    view! {
158        <div
159            class=class
160            style=style
161            role="menu"
162            aria-hidden="false"
163        >
164            {children.map(|c| c())}
165        </div>
166    }
167    .into_any()
168}
169
170/// Menubar Item component
171#[component]
172pub fn MenubarItem(
173    #[prop(optional)] class: Option<String>,
174    #[prop(optional)] style: Option<String>,
175    #[prop(optional)] children: Option<Children>,
176    #[prop(optional)] disabled: Option<bool>,
177    #[prop(optional)] on_select: Option<Callback<()>>,
178) -> impl IntoView {
179    let disabled = disabled.unwrap_or(false);
180
181    let class = merge_classes(vec!["menubar-item"]);
182
183    let handle_keydown = move |ev: web_sys::KeyboardEvent| {
184        if !disabled && (ev.key() == "Enter" || ev.key() == " ") {
185            ev.prevent_default();
186            if let Some(on_select) = on_select {
187                on_select.run(());
188            }
189        }
190    };
191
192    view! {
193        <div
194            class=class
195            style=style
196            role="menuitem"
197        >
198        </div>
199    }
200}
201
202/// Menubar Separator component
203#[component]
204pub fn MenubarSeparator(
205    #[prop(optional)] class: Option<String>,
206    #[prop(optional)] style: Option<String>,
207) -> impl IntoView {
208    let class = merge_classes(vec!["menubar-separator", class.as_deref().unwrap_or("")]);
209
210    view! {
211        <div
212            class=class
213            style=style
214            role="separator"
215            aria-orientation="horizontal"
216        />
217    }
218}
219
220/// Menubar Group component
221#[component]
222pub fn MenubarGroup(
223    #[prop(optional)] class: Option<String>,
224    #[prop(optional)] style: Option<String>,
225    #[prop(optional)] children: Option<Children>,
226) -> impl IntoView {
227    let class = merge_classes(vec!["menubar-group", class.as_deref().unwrap_or("")]);
228
229    view! {
230        <div
231            class=class
232            style=style
233            role="group"
234        >
235            {children.map(|c| c())}
236        </div>
237    }
238}
239
240/// Menubar Label component
241#[component]
242pub fn MenubarLabel(
243    #[prop(optional)] class: Option<String>,
244    #[prop(optional)] style: Option<String>,
245    #[prop(optional)] children: Option<Children>,
246) -> impl IntoView {
247    let class = merge_classes(vec!["menubar-label", class.as_deref().unwrap_or("")]);
248
249    view! {
250        <div
251            class=class
252            style=style
253            role="presentation"
254        >
255            {children.map(|c| c())}
256        </div>
257    }
258}
259
260/// Menubar Orientation enum
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
262pub enum MenubarOrientation {
263    #[default]
264    Horizontal,
265    Vertical,
266}
267
268impl MenubarOrientation {
269    pub fn to_class(&self) -> &'static str {
270        match self {
271            MenubarOrientation::Horizontal => "horizontal",
272            MenubarOrientation::Vertical => "vertical",
273        }
274    }
275
276    pub fn to_aria(&self) -> &'static str {
277        match self {
278            MenubarOrientation::Horizontal => "horizontal",
279            MenubarOrientation::Vertical => "vertical",
280        }
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use crate::MenubarOrientation;
287    use wasm_bindgen_test::*;
288
289    wasm_bindgen_test_configure!(run_in_browser);
290
291    // Menubar Tests
292    #[test]
293    fn test_menubar_creation() {
294        // Test that the component can be created
295    }
296
297    #[test]
298    fn test_menubar_with_class() {
299        // Test that the component can be created with class
300    }
301
302    #[test]
303    fn test_menubar_with_style() {
304        // Test that the component can be created with style
305    }
306
307    #[test]
308    fn test_menubar_horizontal_orientation() {
309        // Test horizontal orientation
310    }
311
312    #[test]
313    fn test_menubar_vertical_orientation() {
314        // Test vertical orientation
315    }
316
317    #[test]
318    fn test_menubar_with_value() {
319        // Test with controlled value
320    }
321
322    #[test]
323    fn test_menubar_with_default_value() {
324        // Test with default value
325    }
326
327    #[test]
328    fn test_menubar_value_change_callback() {
329        // Test value change callback
330    }
331
332    // Menubar Menu Tests
333    #[test]
334    fn test_menubar_menu_creation() {
335        // Test that the component can be created
336    }
337
338    #[test]
339    fn test_menubar_menu_with_class() {
340        // Test that the component can be created with class
341    }
342
343    #[test]
344    fn test_menubar_menu_with_style() {
345        // Test that the component can be created with style
346    }
347
348    #[test]
349    fn test_menubar_menu_with_value() {
350        // Test with value
351    }
352
353    #[test]
354    fn test_menubar_menudisabled() {
355        // Test disabled state
356    }
357
358    #[test]
359    fn test_menubar_menu_on_select() {
360        // Test on_select callback
361    }
362
363    // Menubar Trigger Tests
364    #[test]
365    fn test_menubar_trigger_creation() {
366        // Test that the component can be created
367    }
368
369    #[test]
370    fn test_menubar_trigger_with_class() {
371        // Test that the component can be created with class
372    }
373
374    #[test]
375    fn test_menubar_trigger_with_style() {
376        // Test that the component can be created with style
377    }
378
379    #[test]
380    fn test_menubar_triggerdisabled() {
381        // Test disabled state
382    }
383
384    #[test]
385    fn test_menubar_trigger_on_click() {
386        // Test on_click callback
387    }
388
389    // Menubar Content Tests
390    #[test]
391    fn test_menubar_content_creation() {
392        // Test that the component can be created
393    }
394
395    #[test]
396    fn test_menubar_content_with_class() {
397        // Test that the component can be created with class
398    }
399
400    #[test]
401    fn test_menubar_content_with_style() {
402        // Test that the component can be created with style
403    }
404
405    #[test]
406    fn test_menubar_contentvisible() {
407        // Test visible state
408    }
409
410    #[test]
411    fn test_menubar_content_hidden() {
412        // Test hidden state
413    }
414
415    // Menubar Item Tests
416    #[test]
417    fn test_menubar_item_creation() {
418        // Test that the component can be created
419    }
420
421    #[test]
422    fn test_menubar_item_with_class() {
423        // Test that the component can be created with class
424    }
425
426    #[test]
427    fn test_menubar_item_with_style() {
428        // Test that the component can be created with style
429    }
430
431    #[test]
432    fn test_menubar_itemdisabled() {
433        // Test disabled state
434    }
435
436    #[test]
437    fn test_menubar_item_on_select() {
438        // Test on_select callback
439    }
440
441    // Menubar Separator Tests
442    #[test]
443    fn test_menubar_separator_creation() {
444        // Test that the component can be created
445    }
446
447    #[test]
448    fn test_menubar_separator_with_class() {
449        // Test that the component can be created with class
450    }
451
452    #[test]
453    fn test_menubar_separator_with_style() {
454        // Test that the component can be created with style
455    }
456
457    // Menubar Group Tests
458    #[test]
459    fn test_menubar_group_creation() {
460        // Test that the component can be created
461    }
462
463    #[test]
464    fn test_menubar_group_with_class() {
465        // Test that the component can be created with class
466    }
467
468    #[test]
469    fn test_menubar_group_with_style() {
470        // Test that the component can be created with style
471    }
472
473    // Menubar Label Tests
474    #[test]
475    fn test_menubar_label_creation() {
476        // Test that the component can be created
477    }
478
479    #[test]
480    fn test_menubar_label_with_class() {
481        // Test that the component can be created with class
482    }
483
484    #[test]
485    fn test_menubar_label_with_style() {
486        // Test that the component can be created with style
487    }
488
489    // Menubar Orientation Tests
490    #[test]
491    fn test_menubar_orientation_default() {
492        let orientation = MenubarOrientation::default();
493        assert_eq!(orientation, MenubarOrientation::Horizontal);
494    }
495
496    #[test]
497    fn test_menubar_orientation_horizontal() {
498        let orientation = MenubarOrientation::Horizontal;
499        assert_eq!(orientation.to_class(), "horizontal");
500        assert_eq!(orientation.to_aria(), "horizontal");
501    }
502
503    #[test]
504    fn test_menubar_orientation_vertical() {
505        let orientation = MenubarOrientation::Vertical;
506        assert_eq!(orientation.to_class(), "vertical");
507        assert_eq!(orientation.to_aria(), "vertical");
508    }
509
510    // Helper Function Tests
511    #[test]
512    fn test_merge_classes_empty() {
513        let result = crate::utils::merge_classes(Vec::new());
514        assert_eq!(result, "");
515    }
516
517    #[test]
518    fn test_merge_classes_single() {
519        let result = crate::utils::merge_classes(vec!["class1"]);
520        assert_eq!(result, "class1");
521    }
522
523    #[test]
524    fn test_merge_classes_multiple() {
525        let result = crate::utils::merge_classes(vec!["class1", "class2", "class3"]);
526        assert_eq!(result, "class1 class2 class3");
527    }
528
529    #[test]
530    fn test_merge_classes_with_empty() {
531        let result = crate::utils::merge_classes(vec!["class1", "", "class3"]);
532        assert_eq!(result, "class1 class3");
533    }
534
535    // Property-based tests
536    #[test]
537    fn test_menubar_property_based() {
538        use proptest::prelude::*;
539
540        proptest!(|(____class in ".*", __style in ".*")| {
541            // Test that the component can be created with various class and style values
542
543        });
544    }
545
546    #[test]
547    fn test_menubar_menu_property_based() {
548        use proptest::prelude::*;
549
550        proptest!(|(____class in ".*", __style in ".*", __value in ".*")| {
551            // Test that the component can be created with various prop values
552
553        });
554    }
555
556    #[test]
557    fn test_menubar_trigger_property_based() {
558        use proptest::prelude::*;
559
560        proptest!(|(____class in ".*", __style in ".*")| {
561            // Test that the component can be created with various class and style values
562
563        });
564    }
565
566    #[test]
567    fn test_menubar_content_property_based() {
568        use proptest::prelude::*;
569
570        proptest!(|(____class in ".*", __style in ".*")| {
571            // Test that the component can be created with various class and style values
572
573        });
574    }
575
576    #[test]
577    fn test_menubar_item_property_based() {
578        use proptest::prelude::*;
579
580        proptest!(|(____class in ".*", __style in ".*")| {
581            // Test that the component can be created with various class and style values
582
583        });
584    }
585
586    #[test]
587    fn test_menubar_separator_property_based() {
588        use proptest::prelude::*;
589
590        proptest!(|(____class in ".*", __style in ".*")| {
591            // Test that the component can be created with various class and style values
592
593        });
594    }
595
596    #[test]
597    fn test_menubar_group_property_based() {
598        use proptest::prelude::*;
599
600        proptest!(|(____class in ".*", __style in ".*")| {
601            // Test that the component can be created with various class and style values
602
603        });
604    }
605
606    #[test]
607    fn test_menubar_label_property_based() {
608        use proptest::prelude::*;
609
610        proptest!(|(____class in ".*", __style in ".*")| {
611            // Test that the component can be created with various class and style values
612
613        });
614    }
615}