dioxus_bootstrap_css/nav.rs
1use dioxus::prelude::*;
2
3use crate::types::{Color, NavbarExpand};
4
5/// Bootstrap Nav component — standalone navigation (not inside a Navbar).
6///
7/// # Bootstrap HTML → Dioxus
8///
9/// ```html
10/// <!-- Bootstrap HTML -->
11/// <ul class="nav nav-pills nav-fill">
12/// <li class="nav-item"><a class="nav-link active" href="#">Home</a></li>
13/// <li class="nav-item"><a class="nav-link" href="#">Profile</a></li>
14/// <li class="nav-item"><a class="nav-link disabled">Disabled</a></li>
15/// </ul>
16/// ```
17///
18/// ```rust,no_run
19/// # use dioxus::prelude::*;
20/// # use dioxus_bootstrap_css::prelude::*;
21/// # fn _doctest() -> Element {
22/// rsx! {
23/// Nav { pills: true, fill: true,
24/// NavItem { NavLink { active: true, "Home" } }
25/// NavItem { NavLink { "Profile" } }
26/// NavItem { NavLink { disabled: true, "Disabled" } }
27/// }
28/// // Tabs style
29/// Nav { tabs: true, /* ... */ }
30/// // Underline style
31/// Nav { underline: true, /* ... */ }
32/// // Vertical with pills
33/// Nav { pills: true, vertical: true, /* ... */ }
34/// }
35/// # }
36/// ```
37///
38/// # Props
39///
40/// - `pills` — pill style
41/// - `tabs` — tab style
42/// - `underline` — underline style
43/// - `fill` — fill available width
44/// - `justified` — equal-width items
45/// - `vertical` — vertical layout
46#[derive(Clone, PartialEq, Props)]
47pub struct NavProps {
48 /// Use pill style.
49 #[props(default)]
50 pub pills: bool,
51 /// Use tab style.
52 #[props(default)]
53 pub tabs: bool,
54 /// Use underline style.
55 #[props(default)]
56 pub underline: bool,
57 /// Fill available width equally.
58 #[props(default)]
59 pub fill: bool,
60 /// Justify items to fill width (equal-width items).
61 #[props(default)]
62 pub justified: bool,
63 /// Vertical layout.
64 #[props(default)]
65 pub vertical: bool,
66 /// Additional CSS classes.
67 #[props(default)]
68 pub class: String,
69 /// Any additional HTML attributes.
70 #[props(extends = GlobalAttributes)]
71 attributes: Vec<Attribute>,
72 /// Child elements (NavItems).
73 pub children: Element,
74}
75
76#[component]
77pub fn Nav(props: NavProps) -> Element {
78 let mut classes = vec!["nav".to_string()];
79 if props.pills {
80 classes.push("nav-pills".to_string());
81 }
82 if props.tabs {
83 classes.push("nav-tabs".to_string());
84 }
85 if props.underline {
86 classes.push("nav-underline".to_string());
87 }
88 if props.fill {
89 classes.push("nav-fill".to_string());
90 }
91 if props.justified {
92 classes.push("nav-justified".to_string());
93 }
94 if props.vertical {
95 classes.push("flex-column".to_string());
96 }
97 if !props.class.is_empty() {
98 classes.push(props.class.clone());
99 }
100 let full_class = classes.join(" ");
101
102 rsx! {
103 ul { class: "{full_class}",
104 ..props.attributes,
105 {props.children}
106 }
107 }
108}
109
110/// Bootstrap Navbar component.
111///
112/// # Bootstrap HTML → Dioxus
113///
114/// ```html
115/// <!-- Bootstrap HTML -->
116/// <nav class="navbar navbar-expand-lg bg-dark" data-bs-theme="dark">
117/// <div class="container-fluid">
118/// <a class="navbar-brand" href="#">MyApp</a>
119/// <button class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#nav">
120/// <span class="navbar-toggler-icon"></span>
121/// </button>
122/// <div class="collapse navbar-collapse" id="nav">
123/// <ul class="navbar-nav"><li class="nav-item"><a class="nav-link" href="/">Home</a></li></ul>
124/// </div>
125/// </div>
126/// </nav>
127/// ```
128///
129/// ```rust,no_run
130/// # use dioxus::prelude::*;
131/// # use dioxus_bootstrap_css::prelude::*;
132/// # fn _doctest() -> Element {
133/// // Dioxus equivalent
134/// let collapsed = use_signal(|| true);
135/// rsx! {
136/// Navbar { expand: NavbarExpand::Lg, class: "bg-body sticky-top",
137/// brand: rsx! { a { class: "navbar-brand", href: "#", "MyApp" } },
138/// NavbarToggler { collapsed: collapsed }
139/// NavbarCollapse { collapsed: collapsed,
140/// NavbarNav {
141/// NavItem { NavLink { href: "/", active: true, "Home" } }
142/// NavItem { NavLink { href: "/about", "About" } }
143/// }
144/// }
145/// }
146/// }
147/// # }
148/// ```
149#[derive(Clone, PartialEq, Props)]
150pub struct NavbarProps {
151 /// Navbar color scheme.
152 #[props(default)]
153 pub color: Option<Color>,
154 /// Responsive expand breakpoint.
155 #[props(default)]
156 pub expand: NavbarExpand,
157 /// Brand element (logo, app name).
158 #[props(default)]
159 pub brand: Option<Element>,
160 /// Additional CSS classes.
161 #[props(default)]
162 pub class: String,
163 /// Any additional HTML attributes.
164 #[props(extends = GlobalAttributes)]
165 attributes: Vec<Attribute>,
166 /// Child elements (nav items, collapse, etc.).
167 pub children: Element,
168}
169
170#[component]
171pub fn Navbar(props: NavbarProps) -> Element {
172 let mut classes = vec!["navbar".to_string(), props.expand.to_string()];
173
174 let is_dark = matches!(props.color.as_ref(), Some(Color::Dark));
175
176 if let Some(ref color) = props.color {
177 match color {
178 Color::Dark => {
179 classes.push("bg-dark".to_string());
180 }
181 Color::Light => {
182 classes.push("bg-light".to_string());
183 }
184 c => {
185 classes.push(format!("bg-{c}"));
186 }
187 }
188 }
189
190 if !props.class.is_empty() {
191 classes.push(props.class.clone());
192 }
193
194 let full_class = classes.join(" ");
195
196 rsx! {
197 nav {
198 class: "{full_class}",
199 // Bootstrap 5.3: dark theme via HTML attribute, not CSS class
200 "data-bs-theme": if is_dark { "dark" } else { "" },
201 ..props.attributes,
202 div { class: "container-fluid",
203 if let Some(brand) = props.brand {
204 {brand}
205 }
206 {props.children}
207 }
208 }
209 }
210}
211
212/// Navbar toggler button (hamburger menu) for responsive collapse.
213///
214/// # Bootstrap HTML → Dioxus
215///
216/// | HTML | Dioxus |
217/// |---|---|
218/// | `<button class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#nav">` | `NavbarToggler { collapsed: signal }` |
219///
220/// ```rust,no_run
221/// # use dioxus::prelude::*;
222/// # use dioxus_bootstrap_css::prelude::*;
223/// # fn _doctest() -> Element {
224/// # let collapsed_signal = use_signal(|| false);
225/// rsx! {
226/// NavbarToggler { collapsed: collapsed_signal }
227/// }
228/// # }
229/// ```
230#[derive(Clone, PartialEq, Props)]
231pub struct NavbarTogglerProps {
232 /// Signal to toggle — will invert the value on click.
233 pub collapsed: Signal<bool>,
234 /// Additional CSS classes.
235 #[props(default)]
236 pub class: String,
237 /// Any additional HTML attributes.
238 #[props(extends = GlobalAttributes)]
239 attributes: Vec<Attribute>,
240}
241
242#[component]
243pub fn NavbarToggler(props: NavbarTogglerProps) -> Element {
244 let is_collapsed = *props.collapsed.read();
245 let mut signal = props.collapsed;
246
247 let full_class = if props.class.is_empty() {
248 "navbar-toggler".to_string()
249 } else {
250 format!("navbar-toggler {}", props.class)
251 };
252
253 rsx! {
254 button {
255 class: "{full_class}",
256 r#type: "button",
257 "aria-expanded": if !is_collapsed { "true" } else { "false" },
258 "aria-label": "Toggle navigation",
259 onclick: move |_| signal.set(!is_collapsed),
260 ..props.attributes,
261 span { class: "navbar-toggler-icon" }
262 }
263 }
264}
265
266/// Navbar collapsible content area.
267///
268/// # Bootstrap HTML → Dioxus
269///
270/// | HTML | Dioxus |
271/// |---|---|
272/// | `<div class="collapse navbar-collapse" id="nav">` | `NavbarCollapse { collapsed: signal, ... }` |
273///
274/// ```rust,no_run
275/// # use dioxus::prelude::*;
276/// # use dioxus_bootstrap_css::prelude::*;
277/// # fn _doctest() -> Element {
278/// let collapsed = use_signal(|| true);
279/// rsx! {
280/// NavbarToggler { collapsed: collapsed }
281/// NavbarCollapse { collapsed: collapsed,
282/// NavbarNav {
283/// NavItem { NavLink { href: "/", "Home" } }
284/// }
285/// }
286/// }
287/// # }
288/// ```
289#[derive(Clone, PartialEq, Props)]
290pub struct NavbarCollapseProps {
291 /// Signal controlling collapsed state.
292 pub collapsed: Signal<bool>,
293 /// Additional CSS classes.
294 #[props(default)]
295 pub class: String,
296 /// Any additional HTML attributes.
297 #[props(extends = GlobalAttributes)]
298 attributes: Vec<Attribute>,
299 /// Child elements.
300 pub children: Element,
301}
302
303#[component]
304pub fn NavbarCollapse(props: NavbarCollapseProps) -> Element {
305 let is_collapsed = *props.collapsed.read();
306 let show = if !is_collapsed { " show" } else { "" };
307
308 let full_class = if props.class.is_empty() {
309 format!("collapse navbar-collapse{show}")
310 } else {
311 format!("collapse navbar-collapse{show} {}", props.class)
312 };
313
314 rsx! {
315 div { class: "{full_class}",
316 ..props.attributes,
317 {props.children}
318 }
319 }
320}
321
322/// Bootstrap navbar navigation list.
323///
324/// Use this inside [`NavbarCollapse`] to render Bootstrap's required
325/// `<ul class="navbar-nav">` wrapper around navbar [`NavItem`] children.
326///
327/// # Bootstrap HTML → Dioxus
328///
329/// | HTML | Dioxus |
330/// |---|---|
331/// | `<ul class="navbar-nav">...</ul>` | `NavbarNav { ... }` |
332/// | `<ul class="navbar-nav navbar-nav-scroll">...</ul>` | `NavbarNav { scroll: true, ... }` |
333///
334/// ```rust,no_run
335/// # use dioxus::prelude::*;
336/// # use dioxus_bootstrap_css::prelude::*;
337/// # fn _doctest() -> Element {
338/// rsx! {
339/// NavbarNav {
340/// NavItem { NavLink { active: true, href: "#", "Home" } }
341/// NavItem { NavLink { href: "#docs", "Docs" } }
342/// }
343/// }
344/// # }
345/// ```
346#[derive(Clone, PartialEq, Props)]
347pub struct NavbarNavProps {
348 /// Enable Bootstrap navbar scroll behavior.
349 #[props(default)]
350 pub scroll: bool,
351 /// Additional CSS classes.
352 #[props(default)]
353 pub class: String,
354 /// Any additional HTML attributes.
355 #[props(extends = GlobalAttributes)]
356 attributes: Vec<Attribute>,
357 /// Child elements (NavItem).
358 pub children: Element,
359}
360
361#[component]
362pub fn NavbarNav(props: NavbarNavProps) -> Element {
363 let full_class = navbar_nav_class(props.scroll, &props.class);
364
365 rsx! {
366 ul {
367 class: "{full_class}",
368 ..props.attributes,
369 {props.children}
370 }
371 }
372}
373
374fn navbar_nav_class(scroll: bool, class: &str) -> String {
375 let mut classes = vec!["navbar-nav".to_string()];
376 if scroll {
377 classes.push("navbar-nav-scroll".to_string());
378 }
379 if !class.is_empty() {
380 classes.push(class.to_string());
381 }
382 classes.join(" ")
383}
384
385/// Bootstrap NavItem component.
386///
387/// # Bootstrap HTML → Dioxus
388///
389/// | HTML | Dioxus |
390/// |---|---|
391/// | `<li class="nav-item">` | `NavItem { ... }` |
392#[derive(Clone, PartialEq, Props)]
393pub struct NavItemProps {
394 /// Additional CSS classes.
395 #[props(default)]
396 pub class: String,
397 /// Any additional HTML attributes.
398 #[props(extends = GlobalAttributes)]
399 attributes: Vec<Attribute>,
400 /// Child elements (NavLink).
401 pub children: Element,
402}
403
404#[component]
405pub fn NavItem(props: NavItemProps) -> Element {
406 let full_class = if props.class.is_empty() {
407 "nav-item".to_string()
408 } else {
409 format!("nav-item {}", props.class)
410 };
411
412 rsx! {
413 li { class: "{full_class}", ..props.attributes, {props.children} }
414 }
415}
416
417/// Bootstrap NavLink component.
418///
419/// # Bootstrap HTML → Dioxus
420///
421/// | HTML | Dioxus |
422/// |---|---|
423/// | `<a class="nav-link active" href="/">Home</a>` | `NavLink { href: "/", active: true, "Home" }` |
424/// | `<a class="nav-link disabled">Disabled</a>` | `NavLink { disabled: true, "Disabled" }` |
425///
426/// ```rust,no_run
427/// # use dioxus::prelude::*;
428/// # use dioxus_bootstrap_css::prelude::*;
429/// # fn _doctest() -> Element {
430/// rsx! {
431/// NavLink { href: "/dashboard", active: true, "Dashboard" }
432/// }
433/// # }
434/// ```
435#[derive(Clone, PartialEq, Props)]
436pub struct NavLinkProps {
437 /// Link href.
438 #[props(default = "#".to_string())]
439 pub href: String,
440 /// Active state.
441 #[props(default)]
442 pub active: bool,
443 /// Disabled state.
444 #[props(default)]
445 pub disabled: bool,
446 /// Click event handler.
447 #[props(default)]
448 pub onclick: Option<EventHandler<MouseEvent>>,
449 /// Additional CSS classes.
450 #[props(default)]
451 pub class: String,
452 /// Any additional HTML attributes.
453 #[props(extends = GlobalAttributes)]
454 attributes: Vec<Attribute>,
455 /// Child elements.
456 pub children: Element,
457}
458
459#[component]
460pub fn NavLink(props: NavLinkProps) -> Element {
461 let mut classes = vec!["nav-link".to_string()];
462 if props.active {
463 classes.push("active".to_string());
464 }
465 if props.disabled {
466 classes.push("disabled".to_string());
467 }
468 if !props.class.is_empty() {
469 classes.push(props.class.clone());
470 }
471 let full_class = classes.join(" ");
472
473 rsx! {
474 a {
475 class: "{full_class}",
476 href: "{props.href}",
477 "aria-current": if props.active { "page" } else { "" },
478 onclick: move |evt| {
479 if let Some(handler) = &props.onclick {
480 handler.call(evt);
481 }
482 },
483 ..props.attributes,
484 {props.children}
485 }
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 #[test]
494 fn navbar_nav_class_base() {
495 assert_eq!(navbar_nav_class(false, ""), "navbar-nav");
496 }
497
498 #[test]
499 fn navbar_nav_class_scroll_and_extra_classes() {
500 assert_eq!(
501 navbar_nav_class(true, "ms-auto"),
502 "navbar-nav navbar-nav-scroll ms-auto"
503 );
504 }
505}