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/// NavItem { NavLink { href: "/", active: true, "Home" } }
141/// NavItem { NavLink { href: "/about", "About" } }
142/// }
143/// }
144/// }
145/// # }
146/// ```
147#[derive(Clone, PartialEq, Props)]
148pub struct NavbarProps {
149 /// Navbar color scheme.
150 #[props(default)]
151 pub color: Option<Color>,
152 /// Responsive expand breakpoint.
153 #[props(default)]
154 pub expand: NavbarExpand,
155 /// Brand element (logo, app name).
156 #[props(default)]
157 pub brand: Option<Element>,
158 /// Additional CSS classes.
159 #[props(default)]
160 pub class: String,
161 /// Any additional HTML attributes.
162 #[props(extends = GlobalAttributes)]
163 attributes: Vec<Attribute>,
164 /// Child elements (nav items, collapse, etc.).
165 pub children: Element,
166}
167
168#[component]
169pub fn Navbar(props: NavbarProps) -> Element {
170 let mut classes = vec!["navbar".to_string(), props.expand.to_string()];
171
172 let is_dark = matches!(props.color.as_ref(), Some(Color::Dark));
173
174 if let Some(ref color) = props.color {
175 match color {
176 Color::Dark => {
177 classes.push("bg-dark".to_string());
178 }
179 Color::Light => {
180 classes.push("bg-light".to_string());
181 }
182 c => {
183 classes.push(format!("bg-{c}"));
184 }
185 }
186 }
187
188 if !props.class.is_empty() {
189 classes.push(props.class.clone());
190 }
191
192 let full_class = classes.join(" ");
193
194 rsx! {
195 nav {
196 class: "{full_class}",
197 // Bootstrap 5.3: dark theme via HTML attribute, not CSS class
198 "data-bs-theme": if is_dark { "dark" } else { "" },
199 ..props.attributes,
200 div { class: "container-fluid",
201 if let Some(brand) = props.brand {
202 {brand}
203 }
204 {props.children}
205 }
206 }
207 }
208}
209
210/// Navbar toggler button (hamburger menu) for responsive collapse.
211///
212/// # Bootstrap HTML → Dioxus
213///
214/// | HTML | Dioxus |
215/// |---|---|
216/// | `<button class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#nav">` | `NavbarToggler { collapsed: signal }` |
217///
218/// ```rust,no_run
219/// # use dioxus::prelude::*;
220/// # use dioxus_bootstrap_css::prelude::*;
221/// # fn _doctest() -> Element {
222/// # let collapsed_signal = use_signal(|| false);
223/// rsx! {
224/// NavbarToggler { collapsed: collapsed_signal }
225/// }
226/// # }
227/// ```
228#[derive(Clone, PartialEq, Props)]
229pub struct NavbarTogglerProps {
230 /// Signal to toggle — will invert the value on click.
231 pub collapsed: Signal<bool>,
232 /// Additional CSS classes.
233 #[props(default)]
234 pub class: String,
235 /// Any additional HTML attributes.
236 #[props(extends = GlobalAttributes)]
237 attributes: Vec<Attribute>,
238}
239
240#[component]
241pub fn NavbarToggler(props: NavbarTogglerProps) -> Element {
242 let is_collapsed = *props.collapsed.read();
243 let mut signal = props.collapsed;
244
245 let full_class = if props.class.is_empty() {
246 "navbar-toggler".to_string()
247 } else {
248 format!("navbar-toggler {}", props.class)
249 };
250
251 rsx! {
252 button {
253 class: "{full_class}",
254 r#type: "button",
255 "aria-expanded": if !is_collapsed { "true" } else { "false" },
256 "aria-label": "Toggle navigation",
257 onclick: move |_| signal.set(!is_collapsed),
258 ..props.attributes,
259 span { class: "navbar-toggler-icon" }
260 }
261 }
262}
263
264/// Navbar collapsible content area.
265///
266/// # Bootstrap HTML → Dioxus
267///
268/// | HTML | Dioxus |
269/// |---|---|
270/// | `<div class="collapse navbar-collapse" id="nav">` | `NavbarCollapse { collapsed: signal, ... }` |
271///
272/// ```rust,no_run
273/// # use dioxus::prelude::*;
274/// # use dioxus_bootstrap_css::prelude::*;
275/// # fn _doctest() -> Element {
276/// let collapsed = use_signal(|| true);
277/// rsx! {
278/// NavbarToggler { collapsed: collapsed }
279/// NavbarCollapse { collapsed: collapsed,
280/// NavItem { NavLink { href: "/", "Home" } }
281/// }
282/// }
283/// # }
284/// ```
285#[derive(Clone, PartialEq, Props)]
286pub struct NavbarCollapseProps {
287 /// Signal controlling collapsed state.
288 pub collapsed: Signal<bool>,
289 /// Additional CSS classes.
290 #[props(default)]
291 pub class: String,
292 /// Any additional HTML attributes.
293 #[props(extends = GlobalAttributes)]
294 attributes: Vec<Attribute>,
295 /// Child elements.
296 pub children: Element,
297}
298
299#[component]
300pub fn NavbarCollapse(props: NavbarCollapseProps) -> Element {
301 let is_collapsed = *props.collapsed.read();
302 let show = if !is_collapsed { " show" } else { "" };
303
304 let full_class = if props.class.is_empty() {
305 format!("collapse navbar-collapse{show}")
306 } else {
307 format!("collapse navbar-collapse{show} {}", props.class)
308 };
309
310 rsx! {
311 div { class: "{full_class}",
312 ..props.attributes,
313 {props.children}
314 }
315 }
316}
317
318/// Bootstrap NavItem component.
319///
320/// # Bootstrap HTML → Dioxus
321///
322/// | HTML | Dioxus |
323/// |---|---|
324/// | `<li class="nav-item">` | `NavItem { ... }` |
325#[derive(Clone, PartialEq, Props)]
326pub struct NavItemProps {
327 /// Additional CSS classes.
328 #[props(default)]
329 pub class: String,
330 /// Any additional HTML attributes.
331 #[props(extends = GlobalAttributes)]
332 attributes: Vec<Attribute>,
333 /// Child elements (NavLink).
334 pub children: Element,
335}
336
337#[component]
338pub fn NavItem(props: NavItemProps) -> Element {
339 let full_class = if props.class.is_empty() {
340 "nav-item".to_string()
341 } else {
342 format!("nav-item {}", props.class)
343 };
344
345 rsx! {
346 li { class: "{full_class}", ..props.attributes, {props.children} }
347 }
348}
349
350/// Bootstrap NavLink component.
351///
352/// # Bootstrap HTML → Dioxus
353///
354/// | HTML | Dioxus |
355/// |---|---|
356/// | `<a class="nav-link active" href="/">Home</a>` | `NavLink { href: "/", active: true, "Home" }` |
357/// | `<a class="nav-link disabled">Disabled</a>` | `NavLink { disabled: true, "Disabled" }` |
358///
359/// ```rust,no_run
360/// # use dioxus::prelude::*;
361/// # use dioxus_bootstrap_css::prelude::*;
362/// # fn _doctest() -> Element {
363/// rsx! {
364/// NavLink { href: "/dashboard", active: true, "Dashboard" }
365/// }
366/// # }
367/// ```
368#[derive(Clone, PartialEq, Props)]
369pub struct NavLinkProps {
370 /// Link href.
371 #[props(default = "#".to_string())]
372 pub href: String,
373 /// Active state.
374 #[props(default)]
375 pub active: bool,
376 /// Disabled state.
377 #[props(default)]
378 pub disabled: bool,
379 /// Click event handler.
380 #[props(default)]
381 pub onclick: Option<EventHandler<MouseEvent>>,
382 /// Additional CSS classes.
383 #[props(default)]
384 pub class: String,
385 /// Any additional HTML attributes.
386 #[props(extends = GlobalAttributes)]
387 attributes: Vec<Attribute>,
388 /// Child elements.
389 pub children: Element,
390}
391
392#[component]
393pub fn NavLink(props: NavLinkProps) -> Element {
394 let mut classes = vec!["nav-link".to_string()];
395 if props.active {
396 classes.push("active".to_string());
397 }
398 if props.disabled {
399 classes.push("disabled".to_string());
400 }
401 if !props.class.is_empty() {
402 classes.push(props.class.clone());
403 }
404 let full_class = classes.join(" ");
405
406 rsx! {
407 a {
408 class: "{full_class}",
409 href: "{props.href}",
410 "aria-current": if props.active { "page" } else { "" },
411 onclick: move |evt| {
412 if let Some(handler) = &props.onclick {
413 handler.call(evt);
414 }
415 },
416 ..props.attributes,
417 {props.children}
418 }
419 }
420}