radix_leptos_primitives/components/
menubar.rs1use crate::utils::{merge_classes, generate_id};
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 {
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#[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#[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#[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#[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#[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 #[test]
293 fn test_menubar_creation() {
294 }
296
297 #[test]
298 fn test_menubar_with_class() {
299 }
301
302 #[test]
303 fn test_menubar_with_style() {
304 }
306
307 #[test]
308 fn test_menubar_horizontal_orientation() {
309 }
311
312 #[test]
313 fn test_menubar_vertical_orientation() {
314 }
316
317 #[test]
318 fn test_menubar_with_value() {
319 }
321
322 #[test]
323 fn test_menubar_with_default_value() {
324 }
326
327 #[test]
328 fn test_menubar_value_change_callback() {
329 }
331
332 #[test]
334 fn test_menubar_menu_creation() {
335 }
337
338 #[test]
339 fn test_menubar_menu_with_class() {
340 }
342
343 #[test]
344 fn test_menubar_menu_with_style() {
345 }
347
348 #[test]
349 fn test_menubar_menu_with_value() {
350 }
352
353 #[test]
354 fn test_menubar_menudisabled() {
355 }
357
358 #[test]
359 fn test_menubar_menu_on_select() {
360 }
362
363 #[test]
365 fn test_menubar_trigger_creation() {
366 }
368
369 #[test]
370 fn test_menubar_trigger_with_class() {
371 }
373
374 #[test]
375 fn test_menubar_trigger_with_style() {
376 }
378
379 #[test]
380 fn test_menubar_triggerdisabled() {
381 }
383
384 #[test]
385 fn test_menubar_trigger_on_click() {
386 }
388
389 #[test]
391 fn test_menubar_content_creation() {
392 }
394
395 #[test]
396 fn test_menubar_content_with_class() {
397 }
399
400 #[test]
401 fn test_menubar_content_with_style() {
402 }
404
405 #[test]
406 fn test_menubar_contentvisible() {
407 }
409
410 #[test]
411 fn test_menubar_content_hidden() {
412 }
414
415 #[test]
417 fn test_menubar_item_creation() {
418 }
420
421 #[test]
422 fn test_menubar_item_with_class() {
423 }
425
426 #[test]
427 fn test_menubar_item_with_style() {
428 }
430
431 #[test]
432 fn test_menubar_itemdisabled() {
433 }
435
436 #[test]
437 fn test_menubar_item_on_select() {
438 }
440
441 #[test]
443 fn test_menubar_separator_creation() {
444 }
446
447 #[test]
448 fn test_menubar_separator_with_class() {
449 }
451
452 #[test]
453 fn test_menubar_separator_with_style() {
454 }
456
457 #[test]
459 fn test_menubar_group_creation() {
460 }
462
463 #[test]
464 fn test_menubar_group_with_class() {
465 }
467
468 #[test]
469 fn test_menubar_group_with_style() {
470 }
472
473 #[test]
475 fn test_menubar_label_creation() {
476 }
478
479 #[test]
480 fn test_menubar_label_with_class() {
481 }
483
484 #[test]
485 fn test_menubar_label_with_style() {
486 }
488
489 #[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 #[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 #[test]
537 fn test_menubar_property_based() {
538 use proptest::prelude::*;
539
540 proptest!(|(____class in ".*", __style in ".*")| {
541 });
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 });
554 }
555
556 #[test]
557 fn test_menubar_trigger_property_based() {
558 use proptest::prelude::*;
559
560 proptest!(|(____class in ".*", __style in ".*")| {
561 });
564 }
565
566 #[test]
567 fn test_menubar_content_property_based() {
568 use proptest::prelude::*;
569
570 proptest!(|(____class in ".*", __style in ".*")| {
571 });
574 }
575
576 #[test]
577 fn test_menubar_item_property_based() {
578 use proptest::prelude::*;
579
580 proptest!(|(____class in ".*", __style in ".*")| {
581 });
584 }
585
586 #[test]
587 fn test_menubar_separator_property_based() {
588 use proptest::prelude::*;
589
590 proptest!(|(____class in ".*", __style in ".*")| {
591 });
594 }
595
596 #[test]
597 fn test_menubar_group_property_based() {
598 use proptest::prelude::*;
599
600 proptest!(|(____class in ".*", __style in ".*")| {
601 });
604 }
605
606 #[test]
607 fn test_menubar_label_property_based() {
608 use proptest::prelude::*;
609
610 proptest!(|(____class in ".*", __style in ".*")| {
611 });
614 }
615}