radix_leptos_primitives/components/
menubar.rs1use crate::utils::merge_classes;
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5
6#[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 if let Some(on_change) = on_value_change {
28 Effect::new(move |_| {
29 on_change.run(current_value.get());
30 });
31 }
32
33 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#[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#[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#[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#[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#[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#[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#[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#[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 #[test]
290 fn test_menubar_creation() {
291 }
293
294 #[test]
295 fn test_menubar_with_class() {
296 }
298
299 #[test]
300 fn test_menubar_with_style() {
301 }
303
304 #[test]
305 fn test_menubar_horizontal_orientation() {
306 }
308
309 #[test]
310 fn test_menubar_vertical_orientation() {
311 }
313
314 #[test]
315 fn test_menubar_with_value() {
316 }
318
319 #[test]
320 fn test_menubar_with_default_value() {
321 }
323
324 #[test]
325 fn test_menubar_value_change_callback() {
326 }
328
329 #[test]
331 fn test_menubar_menu_creation() {
332 }
334
335 #[test]
336 fn test_menubar_menu_with_class() {
337 }
339
340 #[test]
341 fn test_menubar_menu_with_style() {
342 }
344
345 #[test]
346 fn test_menubar_menu_with_value() {
347 }
349
350 #[test]
351 fn test_menubar_menudisabled() {
352 }
354
355 #[test]
356 fn test_menubar_menu_on_select() {
357 }
359
360 #[test]
362 fn test_menubar_trigger_creation() {
363 }
365
366 #[test]
367 fn test_menubar_trigger_with_class() {
368 }
370
371 #[test]
372 fn test_menubar_trigger_with_style() {
373 }
375
376 #[test]
377 fn test_menubar_triggerdisabled() {
378 }
380
381 #[test]
382 fn test_menubar_trigger_on_click() {
383 }
385
386 #[test]
388 fn test_menubar_content_creation() {
389 }
391
392 #[test]
393 fn test_menubar_content_with_class() {
394 }
396
397 #[test]
398 fn test_menubar_content_with_style() {
399 }
401
402 #[test]
403 fn test_menubar_contentvisible() {
404 }
406
407 #[test]
408 fn test_menubar_content_hidden() {
409 }
411
412 #[test]
414 fn test_menubar_item_creation() {
415 }
417
418 #[test]
419 fn test_menubar_item_with_class() {
420 }
422
423 #[test]
424 fn test_menubar_item_with_style() {
425 }
427
428 #[test]
429 fn test_menubar_itemdisabled() {
430 }
432
433 #[test]
434 fn test_menubar_item_on_select() {
435 }
437
438 #[test]
440 fn test_menubar_separator_creation() {
441 }
443
444 #[test]
445 fn test_menubar_separator_with_class() {
446 }
448
449 #[test]
450 fn test_menubar_separator_with_style() {
451 }
453
454 #[test]
456 fn test_menubar_group_creation() {
457 }
459
460 #[test]
461 fn test_menubar_group_with_class() {
462 }
464
465 #[test]
466 fn test_menubar_group_with_style() {
467 }
469
470 #[test]
472 fn test_menubar_label_creation() {
473 }
475
476 #[test]
477 fn test_menubar_label_with_class() {
478 }
480
481 #[test]
482 fn test_menubar_label_with_style() {
483 }
485
486 #[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 #[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 #[test]
534 fn test_menubar_property_based() {
535 use proptest::prelude::*;
536
537 proptest!(|(____class in ".*", __style in ".*")| {
538 });
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 });
551 }
552
553 #[test]
554 fn test_menubar_trigger_property_based() {
555 use proptest::prelude::*;
556
557 proptest!(|(____class in ".*", __style in ".*")| {
558 });
561 }
562
563 #[test]
564 fn test_menubar_content_property_based() {
565 use proptest::prelude::*;
566
567 proptest!(|(____class in ".*", __style in ".*")| {
568 });
571 }
572
573 #[test]
574 fn test_menubar_item_property_based() {
575 use proptest::prelude::*;
576
577 proptest!(|(____class in ".*", __style in ".*")| {
578 });
581 }
582
583 #[test]
584 fn test_menubar_separator_property_based() {
585 use proptest::prelude::*;
586
587 proptest!(|(____class in ".*", __style in ".*")| {
588 });
591 }
592
593 #[test]
594 fn test_menubar_group_property_based() {
595 use proptest::prelude::*;
596
597 proptest!(|(____class in ".*", __style in ".*")| {
598 });
601 }
602
603 #[test]
604 fn test_menubar_label_property_based() {
605 use proptest::prelude::*;
606
607 proptest!(|(____class in ".*", __style in ".*")| {
608 });
611 }
612}