radix_leptos_primitives/components/
navigation_menu.rs1use crate::utils::{merge_classes, generate_id};
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 {
169 let _: () = view! { <></> };
170 ().into_any()
171 };
172 }
173
174 let class = merge_classes(vec![
175 "navigation-menu-content",
176 class.as_deref().unwrap_or(""),
177 ]);
178
179 view! {
180 <div
181 class=class
182 style=style
183 role="menu"
184 aria-hidden="false"
185 >
186 {children.map(|c| c())}
187 </div>
188 }
189 .into_any()
190}
191
192#[component]
194pub fn NavigationMenuLink(
195 #[prop(optional)] class: Option<String>,
196 #[prop(optional)] style: Option<String>,
197 #[prop(optional)] children: Option<Children>,
198 #[prop(optional)] href: Option<String>,
199 #[prop(optional)] disabled: Option<bool>,
200 #[prop(optional)] active: Option<bool>,
201 #[prop(optional)] on_click: Option<Callback<()>>,
202) -> impl IntoView {
203 let disabled = disabled.unwrap_or(false);
204 let active = active.unwrap_or(false);
205
206 let class = merge_classes(vec!["navigation-menu-link", class.as_deref().unwrap_or("")]);
207
208 let handle_click = move |_| {
209 if !disabled {
210 if let Some(on_click) = on_click {
211 on_click.run(());
212 }
213 }
214 };
215
216 if let Some(href) = href {
217 view! {
218 <a
219 class=class
220 style=style
221 href=href
222 on:click=handle_click
223 >
224 {children.map(|c| c())}
225 </a>
226 }
227 .into_any()
228 } else {
229 view! {
230 <button
231 class=class
232 style=style
233 on:click=handle_click
234 >
235 {children.map(|c| c())}
236 </button>
237 }
238 .into_any()
239 }
240}
241
242#[component]
244pub fn NavigationMenuSeparator(
245 #[prop(optional)] class: Option<String>,
246 #[prop(optional)] style: Option<String>,
247) -> impl IntoView {
248 let class = merge_classes(vec![
249 "navigation-menu-separator",
250 class.as_deref().unwrap_or(""),
251 ]);
252
253 view! {
254 <div
255 class=class
256 style=style
257 role="separator"
258 aria-orientation="horizontal"
259 />
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
265pub enum NavigationMenuOrientation {
266 #[default]
267 Horizontal,
268 Vertical,
269}
270
271impl NavigationMenuOrientation {
272 pub fn to_class(&self) -> &'static str {
273 match self {
274 NavigationMenuOrientation::Horizontal => "horizontal",
275 NavigationMenuOrientation::Vertical => "vertical",
276 }
277 }
278
279 pub fn to_aria(&self) -> &'static str {
280 match self {
281 NavigationMenuOrientation::Horizontal => "horizontal",
282 NavigationMenuOrientation::Vertical => "vertical",
283 }
284 }
285}
286
287#[cfg(test)]
288mod tests {
289
290 use crate::NavigationMenuOrientation;
291 use wasm_bindgen_test::*;
292
293 wasm_bindgen_test_configure!(run_in_browser);
294
295 #[test]
297 fn test_navigation_menu_creation() {
298 }
300
301 #[test]
302 fn test_navigation_menu_with_class() {
303 }
305
306 #[test]
307 fn test_navigation_menu_with_style() {
308 }
310
311 #[test]
312 fn test_navigation_menu_horizontal_orientation() {
313 }
315
316 #[test]
317 fn test_navigation_menu_vertical_orientation() {
318 }
320
321 #[test]
322 fn test_navigation_menu_with_value() {
323 }
325
326 #[test]
327 fn test_navigation_menu_with_default_value() {
328 }
330
331 #[test]
332 fn test_navigation_menu_value_change_callback() {
333 }
335
336 #[test]
338 fn test_navigation_menu_list_creation() {
339 }
341
342 #[test]
343 fn test_navigation_menu_list_with_class() {
344 }
346
347 #[test]
348 fn test_navigation_menu_list_with_style() {
349 }
351
352 #[test]
354 fn test_navigation_menu_item_creation() {
355 }
357
358 #[test]
359 fn test_navigation_menu_item_with_class() {
360 }
362
363 #[test]
364 fn test_navigation_menu_item_with_style() {
365 }
367
368 #[test]
369 fn test_navigation_menu_item_with_value() {
370 }
372
373 #[test]
374 fn test_navigation_menu_itemdisabled() {
375 }
377
378 #[test]
379 fn test_navigation_menu_item_on_select() {
380 }
382
383 #[test]
385 fn test_navigation_menu_trigger_creation() {
386 }
388
389 #[test]
390 fn test_navigation_menu_trigger_with_class() {
391 }
393
394 #[test]
395 fn test_navigation_menu_trigger_with_style() {
396 }
398
399 #[test]
400 fn test_navigation_menu_triggerdisabled() {
401 }
403
404 #[test]
405 fn test_navigation_menu_trigger_on_click() {
406 }
408
409 #[test]
411 fn test_navigation_menu_content_creation() {
412 }
414
415 #[test]
416 fn test_navigation_menu_content_with_class() {
417 }
419
420 #[test]
421 fn test_navigation_menu_content_with_style() {
422 }
424
425 #[test]
426 fn test_navigation_menu_contentvisible() {
427 }
429
430 #[test]
431 fn test_navigation_menu_content_hidden() {
432 }
434
435 #[test]
437 fn test_navigation_menu_link_creation() {
438 }
440
441 #[test]
442 fn test_navigation_menu_link_with_class() {
443 }
445
446 #[test]
447 fn test_navigation_menu_link_with_style() {
448 }
450
451 #[test]
452 fn test_navigation_menu_link_with_href() {
453 }
455
456 #[test]
457 fn test_navigation_menu_link_without_href() {
458 }
460
461 #[test]
462 fn test_navigation_menu_linkdisabled() {
463 }
465
466 #[test]
467 fn test_navigation_menu_link_active() {
468 }
470
471 #[test]
472 fn test_navigation_menu_link_on_click() {
473 }
475
476 #[test]
478 fn test_navigation_menu_separator_creation() {
479 }
481
482 #[test]
483 fn test_navigation_menu_separator_with_class() {
484 }
486
487 #[test]
488 fn test_navigation_menu_separator_with_style() {
489 }
491
492 #[test]
494 fn test_navigation_menu_orientation_default() {
495 let orientation = NavigationMenuOrientation::default();
496 assert_eq!(orientation, NavigationMenuOrientation::Horizontal);
497 }
498
499 #[test]
500 fn test_navigation_menu_orientation_horizontal() {
501 let orientation = NavigationMenuOrientation::Horizontal;
502 assert_eq!(orientation.to_class(), "horizontal");
503 assert_eq!(orientation.to_aria(), "horizontal");
504 }
505
506 #[test]
507 fn test_navigation_menu_orientation_vertical() {
508 let orientation = NavigationMenuOrientation::Vertical;
509 assert_eq!(orientation.to_class(), "vertical");
510 assert_eq!(orientation.to_aria(), "vertical");
511 }
512
513 #[test]
515 fn test_merge_classes_empty() {
516 let result = crate::utils::merge_classes(Vec::new());
517 assert_eq!(result, "");
518 }
519
520 #[test]
521 fn test_merge_classes_single() {
522 let result = crate::utils::merge_classes(vec!["class1"]);
523 assert_eq!(result, "class1");
524 }
525
526 #[test]
527 fn test_merge_classes_multiple() {
528 let result = crate::utils::merge_classes(vec!["class1", "class2", "class3"]);
529 assert_eq!(result, "class1 class2 class3");
530 }
531
532 #[test]
533 fn test_merge_classes_with_empty() {
534 let result = crate::utils::merge_classes(vec!["class1", "", "class3"]);
535 assert_eq!(result, "class1 class3");
536 }
537
538 #[test]
540 fn test_navigation_menu_property_based() {
541 use proptest::prelude::*;
542
543 proptest!(|(____class in ".*", __style in ".*")| {
544 });
547 }
548
549 #[test]
550 fn test_navigation_menu_list_property_based() {
551 use proptest::prelude::*;
552
553 proptest!(|(____class in ".*", __style in ".*")| {
554 });
557 }
558
559 #[test]
560 fn test_navigation_menu_item_property_based() {
561 use proptest::prelude::*;
562
563 proptest!(|(____class in ".*", __style in ".*", __value in ".*")| {
564 });
567 }
568
569 #[test]
570 fn test_navigation_menu_trigger_property_based() {
571 use proptest::prelude::*;
572
573 proptest!(|(____class in ".*", __style in ".*")| {
574 });
577 }
578
579 #[test]
580 fn test_navigation_menu_content_property_based() {
581 use proptest::prelude::*;
582
583 proptest!(|(____class in ".*", __style in ".*")| {
584 });
587 }
588
589 #[test]
590 fn test_navigation_menu_link_property_based() {
591 use proptest::prelude::*;
592
593 proptest!(|(____class in ".*", __style in ".*", _href in ".*")| {
594 });
597 }
598
599 #[test]
600 fn test_navigation_menu_separator_property_based() {
601 use proptest::prelude::*;
602
603 proptest!(|(____class in ".*", __style in ".*")| {
604 });
607 }
608}