radix_leptos_primitives/components/
navigation_menu.rs1use crate::utils::merge_classes;
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5
6#[component]
10pub fn NavigationMenu(
11 #[prop(optional)] class: Option<String>,
12 #[prop(optional)] style: Option<String>,
13 #[prop(optional)] children: Option<Children>,
14 #[prop(optional)] orientation: Option<NavigationMenuOrientation>,
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 "navigation-menu",
42 &orientation.to_class(),
43 class.as_deref().unwrap_or(""),
44 ]);
45
46 view! {
47 <nav
48 class=class
49 style=style
50 role="navigation"
51 aria-orientation=orientation.to_aria()
52 >
53 {children.map(|c| c())}
54 </nav>
55 }
56}
57
58#[component]
60pub fn NavigationMenuList(
61 #[prop(optional)] class: Option<String>,
62 #[prop(optional)] style: Option<String>,
63 #[prop(optional)] children: Option<Children>,
64) -> impl IntoView {
65 let class = merge_classes(vec!["navigation-menu-list", class.as_deref().unwrap_or("")]);
66
67 view! {
68 <ul class=class style=style role="menubar">
69 {children.map(|c| c())}
70 </ul>
71 }
72}
73
74#[component]
76pub fn NavigationMenuItem(
77 #[prop(optional)] class: Option<String>,
78 #[prop(optional)] style: Option<String>,
79 #[prop(optional)] children: Option<Children>,
80 #[prop(optional)] value: Option<String>,
81 #[prop(optional)] disabled: Option<bool>,
82 #[prop(optional)] on_select: Option<Callback<()>>,
83) -> impl IntoView {
84 let disabled = disabled.unwrap_or(false);
85 let value = value.unwrap_or_default();
86
87 let class = merge_classes(vec!["navigation-menu-item"]);
88
89 let handle_keydown = move |ev: web_sys::KeyboardEvent| {
90 if !disabled && (ev.key() == "Enter" || ev.key() == " ") {
91 ev.prevent_default();
92 if let Some(on_select) = on_select {
93 on_select.run(());
94 }
95 }
96 };
97
98 view! {
99 <li
100 class=class
101 style=style
102 role="none"
103 >
104 <button
105 role="menuitem"
106 >
107 </button>
108 </li>
109 }
110}
111
112#[component]
114pub fn NavigationMenuTrigger(
115 #[prop(optional)] class: Option<String>,
116 #[prop(optional)] style: Option<String>,
117 #[prop(optional)] children: Option<Children>,
118 #[prop(optional)] disabled: Option<bool>,
119 #[prop(optional)] on_click: Option<Callback<()>>,
120) -> impl IntoView {
121 let disabled = disabled.unwrap_or(false);
122
123 let class = merge_classes(vec!["navigation-menu-trigger"]);
124
125 let handle_keydown = move |ev: web_sys::KeyboardEvent| {
126 if !disabled && (ev.key() == "Enter" || ev.key() == " ") {
127 ev.prevent_default();
128 if let Some(on_click) = on_click {
129 on_click.run(());
130 }
131 }
132 };
133
134 let handle_click = move |_| {
135 if !disabled {
136 if let Some(on_click) = on_click {
137 on_click.run(());
138 }
139 }
140 };
141
142 view! {
143 <button
144 class=class
145 style=style
146 disabled=disabled
147 on:click=handle_click
148 on:keydown=handle_keydown
149 aria-haspopup="true"
150 aria-expanded="false"
151 >
152 {children.map(|c| c())}
153 </button>
154 }
155}
156
157#[component]
159pub fn NavigationMenuContent(
160 #[prop(optional)] class: Option<String>,
161 #[prop(optional)] style: Option<String>,
162 #[prop(optional)] children: Option<Children>,
163 #[prop(optional)] visible: Option<ReadSignal<bool>>,
164) -> impl IntoView {
165 let visible = visible.map(|v| v.get()).unwrap_or(true);
166
167 if !visible {
168 return view! { <></> }.into_any();
169 }
170
171 let class = merge_classes(vec![
172 "navigation-menu-content",
173 class.as_deref().unwrap_or(""),
174 ]);
175
176 view! {
177 <div
178 class=class
179 style=style
180 role="menu"
181 aria-hidden="false"
182 >
183 {children.map(|c| c())}
184 </div>
185 }
186 .into_any()
187}
188
189#[component]
191pub fn NavigationMenuLink(
192 #[prop(optional)] class: Option<String>,
193 #[prop(optional)] style: Option<String>,
194 #[prop(optional)] children: Option<Children>,
195 #[prop(optional)] href: Option<String>,
196 #[prop(optional)] disabled: Option<bool>,
197 #[prop(optional)] active: Option<bool>,
198 #[prop(optional)] on_click: Option<Callback<()>>,
199) -> impl IntoView {
200 let disabled = disabled.unwrap_or(false);
201 let active = active.unwrap_or(false);
202
203 let class = merge_classes(vec!["navigation-menu-link", class.as_deref().unwrap_or("")]);
204
205 let handle_click = move |_| {
206 if !disabled {
207 if let Some(on_click) = on_click {
208 on_click.run(());
209 }
210 }
211 };
212
213 if let Some(href) = href {
214 view! {
215 <a
216 class=class
217 style=style
218 href=href
219 on:click=handle_click
220 >
221 {children.map(|c| c())}
222 </a>
223 }
224 .into_any()
225 } else {
226 view! {
227 <button
228 class=class
229 style=style
230 on:click=handle_click
231 >
232 {children.map(|c| c())}
233 </button>
234 }
235 .into_any()
236 }
237}
238
239#[component]
241pub fn NavigationMenuSeparator(
242 #[prop(optional)] class: Option<String>,
243 #[prop(optional)] style: Option<String>,
244) -> impl IntoView {
245 let class = merge_classes(vec![
246 "navigation-menu-separator",
247 class.as_deref().unwrap_or(""),
248 ]);
249
250 view! {
251 <div
252 class=class
253 style=style
254 role="separator"
255 aria-orientation="horizontal"
256 />
257 }
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
262pub enum NavigationMenuOrientation {
263 #[default]
264 Horizontal,
265 Vertical,
266}
267
268impl NavigationMenuOrientation {
269 pub fn to_class(&self) -> &'static str {
270 match self {
271 NavigationMenuOrientation::Horizontal => "horizontal",
272 NavigationMenuOrientation::Vertical => "vertical",
273 }
274 }
275
276 pub fn to_aria(&self) -> &'static str {
277 match self {
278 NavigationMenuOrientation::Horizontal => "horizontal",
279 NavigationMenuOrientation::Vertical => "vertical",
280 }
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use crate::utils::merge_classes;
287 use crate::NavigationMenuOrientation;
288 use wasm_bindgen_test::*;
289
290 wasm_bindgen_test_configure!(run_in_browser);
291
292 #[test]
294 fn test_navigation_menu_creation() {
295 }
297
298 #[test]
299 fn test_navigation_menu_with_class() {
300 }
302
303 #[test]
304 fn test_navigation_menu_with_style() {
305 }
307
308 #[test]
309 fn test_navigation_menu_horizontal_orientation() {
310 }
312
313 #[test]
314 fn test_navigation_menu_vertical_orientation() {
315 }
317
318 #[test]
319 fn test_navigation_menu_with_value() {
320 }
322
323 #[test]
324 fn test_navigation_menu_with_default_value() {
325 }
327
328 #[test]
329 fn test_navigation_menu_value_change_callback() {
330 }
332
333 #[test]
335 fn test_navigation_menu_list_creation() {
336 }
338
339 #[test]
340 fn test_navigation_menu_list_with_class() {
341 }
343
344 #[test]
345 fn test_navigation_menu_list_with_style() {
346 }
348
349 #[test]
351 fn test_navigation_menu_item_creation() {
352 }
354
355 #[test]
356 fn test_navigation_menu_item_with_class() {
357 }
359
360 #[test]
361 fn test_navigation_menu_item_with_style() {
362 }
364
365 #[test]
366 fn test_navigation_menu_item_with_value() {
367 }
369
370 #[test]
371 fn test_navigation_menu_itemdisabled() {
372 }
374
375 #[test]
376 fn test_navigation_menu_item_on_select() {
377 }
379
380 #[test]
382 fn test_navigation_menu_trigger_creation() {
383 }
385
386 #[test]
387 fn test_navigation_menu_trigger_with_class() {
388 }
390
391 #[test]
392 fn test_navigation_menu_trigger_with_style() {
393 }
395
396 #[test]
397 fn test_navigation_menu_triggerdisabled() {
398 }
400
401 #[test]
402 fn test_navigation_menu_trigger_on_click() {
403 }
405
406 #[test]
408 fn test_navigation_menu_content_creation() {
409 }
411
412 #[test]
413 fn test_navigation_menu_content_with_class() {
414 }
416
417 #[test]
418 fn test_navigation_menu_content_with_style() {
419 }
421
422 #[test]
423 fn test_navigation_menu_contentvisible() {
424 }
426
427 #[test]
428 fn test_navigation_menu_content_hidden() {
429 }
431
432 #[test]
434 fn test_navigation_menu_link_creation() {
435 }
437
438 #[test]
439 fn test_navigation_menu_link_with_class() {
440 }
442
443 #[test]
444 fn test_navigation_menu_link_with_style() {
445 }
447
448 #[test]
449 fn test_navigation_menu_link_with_href() {
450 }
452
453 #[test]
454 fn test_navigation_menu_link_without_href() {
455 }
457
458 #[test]
459 fn test_navigation_menu_linkdisabled() {
460 }
462
463 #[test]
464 fn test_navigation_menu_link_active() {
465 }
467
468 #[test]
469 fn test_navigation_menu_link_on_click() {
470 }
472
473 #[test]
475 fn test_navigation_menu_separator_creation() {
476 }
478
479 #[test]
480 fn test_navigation_menu_separator_with_class() {
481 }
483
484 #[test]
485 fn test_navigation_menu_separator_with_style() {
486 }
488
489 #[test]
491 fn test_navigation_menu_orientation_default() {
492 let orientation = NavigationMenuOrientation::default();
493 assert_eq!(orientation, NavigationMenuOrientation::Horizontal);
494 }
495
496 #[test]
497 fn test_navigation_menu_orientation_horizontal() {
498 let orientation = NavigationMenuOrientation::Horizontal;
499 assert_eq!(orientation.to_class(), "horizontal");
500 assert_eq!(orientation.to_aria(), "horizontal");
501 }
502
503 #[test]
504 fn test_navigation_menu_orientation_vertical() {
505 let orientation = NavigationMenuOrientation::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_navigation_menu_property_based() {
538 use proptest::prelude::*;
539
540 proptest!(|(____class in ".*", __style in ".*")| {
541 });
544 }
545
546 #[test]
547 fn test_navigation_menu_list_property_based() {
548 use proptest::prelude::*;
549
550 proptest!(|(____class in ".*", __style in ".*")| {
551 });
554 }
555
556 #[test]
557 fn test_navigation_menu_item_property_based() {
558 use proptest::prelude::*;
559
560 proptest!(|(____class in ".*", __style in ".*", __value in ".*")| {
561 });
564 }
565
566 #[test]
567 fn test_navigation_menu_trigger_property_based() {
568 use proptest::prelude::*;
569
570 proptest!(|(____class in ".*", __style in ".*")| {
571 });
574 }
575
576 #[test]
577 fn test_navigation_menu_content_property_based() {
578 use proptest::prelude::*;
579
580 proptest!(|(____class in ".*", __style in ".*")| {
581 });
584 }
585
586 #[test]
587 fn test_navigation_menu_link_property_based() {
588 use proptest::prelude::*;
589
590 proptest!(|(____class in ".*", __style in ".*", _href in ".*")| {
591 });
594 }
595
596 #[test]
597 fn test_navigation_menu_separator_property_based() {
598 use proptest::prelude::*;
599
600 proptest!(|(____class in ".*", __style in ".*")| {
601 });
604 }
605}