radix_leptos_primitives/components/
menubar.rs1use leptos::*;
2use leptos::prelude::*;
3
4#[component]
8pub fn Menubar(
9 #[prop(optional)] class: Option<String>,
10 #[prop(optional)] style: Option<String>,
11 #[prop(optional)] children: Option<Children>,
12 #[prop(optional)] orientation: Option<MenubarOrientation>,
13 #[prop(optional)] default_value: Option<String>,
14 #[prop(optional)] value: Option<ReadSignal<String>>,
15 #[prop(optional)] on_value_change: Option<Callback<String>>,
16) -> impl IntoView {
17 let orientation = orientation.unwrap_or_default();
18 let (current_value, set_current_value) = signal(
19 value.map(|v| v.get()).unwrap_or_else(|| default_value.unwrap_or_default())
20 );
21
22 if let Some(on_change) = on_value_change {
24 Effect::new(move |_| {
25 on_change.run(current_value.get());
26 });
27 }
28
29 if let Some(external_value) = value {
31 Effect::new(move |_| {
32 set_current_value.set(external_value.get());
33 });
34 }
35
36 let class = merge_classes(vec![
37 "menubar",
38 &orientation.to_class(),
39 class.as_deref().unwrap_or(""),
40 ]);
41
42 view! {
43 <div
44 class=class
45 style=style
46 role="menubar"
47 aria-orientation=orientation.to_aria()
48 >
49 {children.map(|c| c())}
50 </div>
51 }
52}
53
54#[component]
56pub fn MenubarMenu(
57 #[prop(optional)] class: Option<String>,
58 #[prop(optional)] style: Option<String>,
59 #[prop(optional)] children: Option<Children>,
60 #[prop(optional)] value: Option<String>,
61 #[prop(optional)] disabled: Option<bool>,
62 #[prop(optional)] on_select: Option<Callback<()>>,
63) -> impl IntoView {
64 let disabled = disabled.unwrap_or(false);
65 let value = value.unwrap_or_default();
66
67 let class = merge_classes(vec![
68 "menubar-menu",
69 if disabled { "disabled" } else { "" },
70 class.as_deref().unwrap_or(""),
71 ]);
72
73 let handle_click = move |_| {
74 if !disabled {
75 if let Some(on_select) = on_select {
76 on_select.run(());
77 }
78 }
79 };
80
81 let handle_keydown = move |ev: web_sys::KeyboardEvent| {
82 if !disabled && (ev.key() == "Enter" || ev.key() == " ") {
83 ev.prevent_default();
84 if let Some(on_select) = on_select {
85 on_select.run(());
86 }
87 }
88 };
89
90 view! {
91 <div
92 class=class
93 style=style
94 role="none"
95 tabindex=if disabled { -1 } else { 0 }
96 on:click=handle_click
97 on:keydown=handle_keydown
98 data-value=value
99 >
100 {children.map(|c| c())}
101 </div>
102 }
103}
104
105#[component]
107pub fn MenubarTrigger(
108 #[prop(optional)] class: Option<String>,
109 #[prop(optional)] style: Option<String>,
110 #[prop(optional)] children: Option<Children>,
111 #[prop(optional)] disabled: Option<bool>,
112 #[prop(optional)] on_click: Option<Callback<()>>,
113) -> impl IntoView {
114 let disabled = disabled.unwrap_or(false);
115
116 let class = merge_classes(vec![
117 "menubar-trigger",
118 if disabled { "disabled" } else { "" },
119 class.as_deref().unwrap_or(""),
120 ]);
121
122 let handle_click = move |_| {
123 if !disabled {
124 if let Some(on_click) = on_click {
125 on_click.run(());
126 }
127 }
128 };
129
130 let handle_keydown = move |ev: web_sys::KeyboardEvent| {
131 if !disabled && (ev.key() == "Enter" || ev.key() == " ") {
132 ev.prevent_default();
133 if let Some(on_click) = on_click {
134 on_click.run(());
135 }
136 }
137 };
138
139 view! {
140 <button
141 class=class
142 style=style
143 disabled=disabled
144 on:click=handle_click
145 on:keydown=handle_keydown
146 role="menuitem"
147 aria-haspopup="true"
148 aria-expanded="false"
149 >
150 {children.map(|c| c())}
151 </button>
152 }
153}
154
155#[component]
157pub fn MenubarContent(
158 #[prop(optional)] class: Option<String>,
159 #[prop(optional)] style: Option<String>,
160 #[prop(optional)] children: Option<Children>,
161 #[prop(optional)] visible: Option<ReadSignal<bool>>,
162) -> impl IntoView {
163 let visible = visible.map(|v| v.get()).unwrap_or(true);
164
165 if !visible {
166 return view! { <></> }.into_any();
167 }
168
169 let class = merge_classes(vec![
170 "menubar-content",
171 class.as_deref().unwrap_or(""),
172 ]);
173
174 view! {
175 <div
176 class=class
177 style=style
178 role="menu"
179 aria-hidden="false"
180 >
181 {children.map(|c| c())}
182 </div>
183 }.into_any()
184}
185
186#[component]
188pub fn MenubarItem(
189 #[prop(optional)] class: Option<String>,
190 #[prop(optional)] style: Option<String>,
191 #[prop(optional)] children: Option<Children>,
192 #[prop(optional)] disabled: Option<bool>,
193 #[prop(optional)] on_select: Option<Callback<()>>,
194) -> impl IntoView {
195 let disabled = disabled.unwrap_or(false);
196
197 let class = merge_classes(vec![
198 "menubar-item",
199 if disabled { "disabled" } else { "" },
200 class.as_deref().unwrap_or(""),
201 ]);
202
203 let handle_click = move |_| {
204 if !disabled {
205 if let Some(on_select) = on_select {
206 on_select.run(());
207 }
208 }
209 };
210
211 let handle_keydown = move |ev: web_sys::KeyboardEvent| {
212 if !disabled && (ev.key() == "Enter" || ev.key() == " ") {
213 ev.prevent_default();
214 if let Some(on_select) = on_select {
215 on_select.run(());
216 }
217 }
218 };
219
220 view! {
221 <div
222 class=class
223 style=style
224 role="menuitem"
225 tabindex=if disabled { -1 } else { 0 }
226 on:click=handle_click
227 on:keydown=handle_keydown
228 >
229 {children.map(|c| c())}
230 </div>
231 }
232}
233
234#[component]
236pub fn MenubarSeparator(
237 #[prop(optional)] class: Option<String>,
238 #[prop(optional)] style: Option<String>,
239) -> impl IntoView {
240 let class = merge_classes(vec![
241 "menubar-separator",
242 class.as_deref().unwrap_or(""),
243 ]);
244
245 view! {
246 <div
247 class=class
248 style=style
249 role="separator"
250 aria-orientation="horizontal"
251 />
252 }
253}
254
255#[component]
257pub fn MenubarGroup(
258 #[prop(optional)] class: Option<String>,
259 #[prop(optional)] style: Option<String>,
260 #[prop(optional)] children: Option<Children>,
261) -> impl IntoView {
262 let class = merge_classes(vec![
263 "menubar-group",
264 class.as_deref().unwrap_or(""),
265 ]);
266
267 view! {
268 <div
269 class=class
270 style=style
271 role="group"
272 >
273 {children.map(|c| c())}
274 </div>
275 }
276}
277
278#[component]
280pub fn MenubarLabel(
281 #[prop(optional)] class: Option<String>,
282 #[prop(optional)] style: Option<String>,
283 #[prop(optional)] children: Option<Children>,
284) -> impl IntoView {
285 let class = merge_classes(vec![
286 "menubar-label",
287 class.as_deref().unwrap_or(""),
288 ]);
289
290 view! {
291 <div
292 class=class
293 style=style
294 role="presentation"
295 >
296 {children.map(|c| c())}
297 </div>
298 }
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
303pub enum MenubarOrientation {
304 #[default]
305 Horizontal,
306 Vertical,
307}
308
309impl MenubarOrientation {
310 pub fn to_class(&self) -> &'static str {
311 match self {
312 MenubarOrientation::Horizontal => "horizontal",
313 MenubarOrientation::Vertical => "vertical",
314 }
315 }
316
317 pub fn to_aria(&self) -> &'static str {
318 match self {
319 MenubarOrientation::Horizontal => "horizontal",
320 MenubarOrientation::Vertical => "vertical",
321 }
322 }
323}
324
325fn merge_classes(classes: Vec<&str>) -> String {
327 classes
328 .into_iter()
329 .filter(|c| !c.is_empty())
330 .collect::<Vec<_>>()
331 .join(" ")
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use wasm_bindgen_test::*;
338
339 wasm_bindgen_test_configure!(run_in_browser);
340
341 #[test]
343 fn test_menubar_creation() {
344 assert!(true);
346 }
347
348 #[test]
349 fn test_menubar_with_class() {
350 assert!(true);
352 }
353
354 #[test]
355 fn test_menubar_with_style() {
356 assert!(true);
358 }
359
360 #[test]
361 fn test_menubar_horizontal_orientation() {
362 assert!(true);
364 }
365
366 #[test]
367 fn test_menubar_vertical_orientation() {
368 assert!(true);
370 }
371
372 #[test]
373 fn test_menubar_with_value() {
374 assert!(true);
376 }
377
378 #[test]
379 fn test_menubar_with_default_value() {
380 assert!(true);
382 }
383
384 #[test]
385 fn test_menubar_value_change_callback() {
386 assert!(true);
388 }
389
390 #[test]
392 fn test_menubar_menu_creation() {
393 assert!(true);
395 }
396
397 #[test]
398 fn test_menubar_menu_with_class() {
399 assert!(true);
401 }
402
403 #[test]
404 fn test_menubar_menu_with_style() {
405 assert!(true);
407 }
408
409 #[test]
410 fn test_menubar_menu_with_value() {
411 assert!(true);
413 }
414
415 #[test]
416 fn test_menubar_menu_disabled() {
417 assert!(true);
419 }
420
421 #[test]
422 fn test_menubar_menu_on_select() {
423 assert!(true);
425 }
426
427 #[test]
429 fn test_menubar_trigger_creation() {
430 assert!(true);
432 }
433
434 #[test]
435 fn test_menubar_trigger_with_class() {
436 assert!(true);
438 }
439
440 #[test]
441 fn test_menubar_trigger_with_style() {
442 assert!(true);
444 }
445
446 #[test]
447 fn test_menubar_trigger_disabled() {
448 assert!(true);
450 }
451
452 #[test]
453 fn test_menubar_trigger_on_click() {
454 assert!(true);
456 }
457
458 #[test]
460 fn test_menubar_content_creation() {
461 assert!(true);
463 }
464
465 #[test]
466 fn test_menubar_content_with_class() {
467 assert!(true);
469 }
470
471 #[test]
472 fn test_menubar_content_with_style() {
473 assert!(true);
475 }
476
477 #[test]
478 fn test_menubar_content_visible() {
479 assert!(true);
481 }
482
483 #[test]
484 fn test_menubar_content_hidden() {
485 assert!(true);
487 }
488
489 #[test]
491 fn test_menubar_item_creation() {
492 assert!(true);
494 }
495
496 #[test]
497 fn test_menubar_item_with_class() {
498 assert!(true);
500 }
501
502 #[test]
503 fn test_menubar_item_with_style() {
504 assert!(true);
506 }
507
508 #[test]
509 fn test_menubar_item_disabled() {
510 assert!(true);
512 }
513
514 #[test]
515 fn test_menubar_item_on_select() {
516 assert!(true);
518 }
519
520 #[test]
522 fn test_menubar_separator_creation() {
523 assert!(true);
525 }
526
527 #[test]
528 fn test_menubar_separator_with_class() {
529 assert!(true);
531 }
532
533 #[test]
534 fn test_menubar_separator_with_style() {
535 assert!(true);
537 }
538
539 #[test]
541 fn test_menubar_group_creation() {
542 assert!(true);
544 }
545
546 #[test]
547 fn test_menubar_group_with_class() {
548 assert!(true);
550 }
551
552 #[test]
553 fn test_menubar_group_with_style() {
554 assert!(true);
556 }
557
558 #[test]
560 fn test_menubar_label_creation() {
561 assert!(true);
563 }
564
565 #[test]
566 fn test_menubar_label_with_class() {
567 assert!(true);
569 }
570
571 #[test]
572 fn test_menubar_label_with_style() {
573 assert!(true);
575 }
576
577 #[test]
579 fn test_menubar_orientation_default() {
580 let orientation = MenubarOrientation::default();
581 assert_eq!(orientation, MenubarOrientation::Horizontal);
582 }
583
584 #[test]
585 fn test_menubar_orientation_horizontal() {
586 let orientation = MenubarOrientation::Horizontal;
587 assert_eq!(orientation.to_class(), "horizontal");
588 assert_eq!(orientation.to_aria(), "horizontal");
589 }
590
591 #[test]
592 fn test_menubar_orientation_vertical() {
593 let orientation = MenubarOrientation::Vertical;
594 assert_eq!(orientation.to_class(), "vertical");
595 assert_eq!(orientation.to_aria(), "vertical");
596 }
597
598 #[test]
600 fn test_merge_classes_empty() {
601 let result = merge_classes(vec![]);
602 assert_eq!(result, "");
603 }
604
605 #[test]
606 fn test_merge_classes_single() {
607 let result = merge_classes(vec!["class1"]);
608 assert_eq!(result, "class1");
609 }
610
611 #[test]
612 fn test_merge_classes_multiple() {
613 let result = merge_classes(vec!["class1", "class2", "class3"]);
614 assert_eq!(result, "class1 class2 class3");
615 }
616
617 #[test]
618 fn test_merge_classes_with_empty() {
619 let result = merge_classes(vec!["class1", "", "class3"]);
620 assert_eq!(result, "class1 class3");
621 }
622
623 #[test]
625 fn test_menubar_property_based() {
626 use proptest::prelude::*;
627
628 proptest!(|(class in ".*", style in ".*")| {
629 assert!(true);
631 });
632 }
633
634 #[test]
635 fn test_menubar_menu_property_based() {
636 use proptest::prelude::*;
637
638 proptest!(|(class in ".*", style in ".*", value in ".*")| {
639 assert!(true);
641 });
642 }
643
644 #[test]
645 fn test_menubar_trigger_property_based() {
646 use proptest::prelude::*;
647
648 proptest!(|(class in ".*", style in ".*")| {
649 assert!(true);
651 });
652 }
653
654 #[test]
655 fn test_menubar_content_property_based() {
656 use proptest::prelude::*;
657
658 proptest!(|(class in ".*", style in ".*")| {
659 assert!(true);
661 });
662 }
663
664 #[test]
665 fn test_menubar_item_property_based() {
666 use proptest::prelude::*;
667
668 proptest!(|(class in ".*", style in ".*")| {
669 assert!(true);
671 });
672 }
673
674 #[test]
675 fn test_menubar_separator_property_based() {
676 use proptest::prelude::*;
677
678 proptest!(|(class in ".*", style in ".*")| {
679 assert!(true);
681 });
682 }
683
684 #[test]
685 fn test_menubar_group_property_based() {
686 use proptest::prelude::*;
687
688 proptest!(|(class in ".*", style in ".*")| {
689 assert!(true);
691 });
692 }
693
694 #[test]
695 fn test_menubar_label_property_based() {
696 use proptest::prelude::*;
697
698 proptest!(|(class in ".*", style in ".*")| {
699 assert!(true);
701 });
702 }
703}