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/// rsx! {
20/// Nav { pills: true, fill: true,
21/// NavItem { NavLink { active: true, "Home" } }
22/// NavItem { NavLink { "Profile" } }
23/// NavItem { NavLink { disabled: true, "Disabled" } }
24/// }
25/// // Tabs style
26/// Nav { tabs: true, /* ... */ }
27/// // Underline style
28/// Nav { underline: true, /* ... */ }
29/// // Vertical with pills
30/// Nav { pills: true, vertical: true, /* ... */ }
31/// }
32/// ```
33///
34/// # Props
35///
36/// - `pills` — pill style
37/// - `tabs` — tab style
38/// - `underline` — underline style
39/// - `fill` — fill available width
40/// - `justified` — equal-width items
41/// - `vertical` — vertical layout
42#[derive(Clone, PartialEq, Props)]
43pub struct NavProps {
44 /// Use pill style.
45 #[props(default)]
46 pub pills: bool,
47 /// Use tab style.
48 #[props(default)]
49 pub tabs: bool,
50 /// Use underline style.
51 #[props(default)]
52 pub underline: bool,
53 /// Fill available width equally.
54 #[props(default)]
55 pub fill: bool,
56 /// Justify items to fill width (equal-width items).
57 #[props(default)]
58 pub justified: bool,
59 /// Vertical layout.
60 #[props(default)]
61 pub vertical: bool,
62 /// Additional CSS classes.
63 #[props(default)]
64 pub class: String,
65 /// Any additional HTML attributes.
66 #[props(extends = GlobalAttributes)]
67 attributes: Vec<Attribute>,
68 /// Child elements (NavItems).
69 pub children: Element,
70}
71
72#[component]
73pub fn Nav(props: NavProps) -> Element {
74 let mut classes = vec!["nav".to_string()];
75 if props.pills {
76 classes.push("nav-pills".to_string());
77 }
78 if props.tabs {
79 classes.push("nav-tabs".to_string());
80 }
81 if props.underline {
82 classes.push("nav-underline".to_string());
83 }
84 if props.fill {
85 classes.push("nav-fill".to_string());
86 }
87 if props.justified {
88 classes.push("nav-justified".to_string());
89 }
90 if props.vertical {
91 classes.push("flex-column".to_string());
92 }
93 if !props.class.is_empty() {
94 classes.push(props.class.clone());
95 }
96 let full_class = classes.join(" ");
97
98 rsx! {
99 ul { class: "{full_class}",
100 ..props.attributes,
101 {props.children}
102 }
103 }
104}
105
106/// Bootstrap Navbar component.
107///
108/// # Bootstrap HTML → Dioxus
109///
110/// ```html
111/// <!-- Bootstrap HTML -->
112/// <nav class="navbar navbar-expand-lg bg-dark" data-bs-theme="dark">
113/// <div class="container-fluid">
114/// <a class="navbar-brand" href="#">MyApp</a>
115/// <button class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#nav">
116/// <span class="navbar-toggler-icon"></span>
117/// </button>
118/// <div class="collapse navbar-collapse" id="nav">
119/// <ul class="navbar-nav"><li class="nav-item"><a class="nav-link" href="/">Home</a></li></ul>
120/// </div>
121/// </div>
122/// </nav>
123/// ```
124///
125/// ```rust,no_run
126/// // Dioxus equivalent
127/// let collapsed = use_signal(|| true);
128/// rsx! {
129/// Navbar { expand: NavbarExpand::Lg, class: "bg-body sticky-top",
130/// brand: rsx! { a { class: "navbar-brand", href: "#", "MyApp" } },
131/// NavbarToggler { collapsed: collapsed }
132/// NavbarCollapse { collapsed: collapsed,
133/// NavItem { NavLink { href: "/", active: true, "Home" } }
134/// NavItem { NavLink { href: "/about", "About" } }
135/// }
136/// }
137/// }
138/// ```
139#[derive(Clone, PartialEq, Props)]
140pub struct NavbarProps {
141 /// Navbar color scheme.
142 #[props(default)]
143 pub color: Option<Color>,
144 /// Responsive expand breakpoint.
145 #[props(default)]
146 pub expand: NavbarExpand,
147 /// Brand element (logo, app name).
148 #[props(default)]
149 pub brand: Option<Element>,
150 /// Additional CSS classes.
151 #[props(default)]
152 pub class: String,
153 /// Any additional HTML attributes.
154 #[props(extends = GlobalAttributes)]
155 attributes: Vec<Attribute>,
156 /// Child elements (nav items, collapse, etc.).
157 pub children: Element,
158}
159
160#[component]
161pub fn Navbar(props: NavbarProps) -> Element {
162 let mut classes = vec!["navbar".to_string(), props.expand.to_string()];
163
164 let is_dark = matches!(props.color.as_ref(), Some(Color::Dark));
165
166 if let Some(ref color) = props.color {
167 match color {
168 Color::Dark => {
169 classes.push("bg-dark".to_string());
170 }
171 Color::Light => {
172 classes.push("bg-light".to_string());
173 }
174 c => {
175 classes.push(format!("bg-{c}"));
176 }
177 }
178 }
179
180 if !props.class.is_empty() {
181 classes.push(props.class.clone());
182 }
183
184 let full_class = classes.join(" ");
185
186 rsx! {
187 nav {
188 class: "{full_class}",
189 // Bootstrap 5.3: dark theme via HTML attribute, not CSS class
190 "data-bs-theme": if is_dark { "dark" } else { "" },
191 ..props.attributes,
192 div { class: "container-fluid",
193 if let Some(brand) = props.brand {
194 {brand}
195 }
196 {props.children}
197 }
198 }
199 }
200}
201
202/// Navbar toggler button (hamburger menu) for responsive collapse.
203///
204/// # Bootstrap HTML → Dioxus
205///
206/// | HTML | Dioxus |
207/// |---|---|
208/// | `<button class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#nav">` | `NavbarToggler { collapsed: signal }` |
209///
210/// ```rust
211/// rsx! {
212/// NavbarToggler { collapsed: collapsed_signal }
213/// }
214/// ```
215#[derive(Clone, PartialEq, Props)]
216pub struct NavbarTogglerProps {
217 /// Signal to toggle — will invert the value on click.
218 pub collapsed: Signal<bool>,
219 /// Additional CSS classes.
220 #[props(default)]
221 pub class: String,
222 /// Any additional HTML attributes.
223 #[props(extends = GlobalAttributes)]
224 attributes: Vec<Attribute>,
225}
226
227#[component]
228pub fn NavbarToggler(props: NavbarTogglerProps) -> Element {
229 let is_collapsed = *props.collapsed.read();
230 let mut signal = props.collapsed;
231
232 let full_class = if props.class.is_empty() {
233 "navbar-toggler".to_string()
234 } else {
235 format!("navbar-toggler {}", props.class)
236 };
237
238 rsx! {
239 button {
240 class: "{full_class}",
241 r#type: "button",
242 "aria-expanded": if !is_collapsed { "true" } else { "false" },
243 "aria-label": "Toggle navigation",
244 onclick: move |_| signal.set(!is_collapsed),
245 ..props.attributes,
246 span { class: "navbar-toggler-icon" }
247 }
248 }
249}
250
251/// Navbar collapsible content area.
252///
253/// # Bootstrap HTML → Dioxus
254///
255/// | HTML | Dioxus |
256/// |---|---|
257/// | `<div class="collapse navbar-collapse" id="nav">` | `NavbarCollapse { collapsed: signal, ... }` |
258///
259/// ```rust
260/// let collapsed = use_signal(|| true);
261/// rsx! {
262/// NavbarToggler { collapsed: collapsed }
263/// NavbarCollapse { collapsed: collapsed,
264/// NavItem { NavLink { href: "/", "Home" } }
265/// }
266/// }
267/// ```
268#[derive(Clone, PartialEq, Props)]
269pub struct NavbarCollapseProps {
270 /// Signal controlling collapsed state.
271 pub collapsed: Signal<bool>,
272 /// Additional CSS classes.
273 #[props(default)]
274 pub class: String,
275 /// Any additional HTML attributes.
276 #[props(extends = GlobalAttributes)]
277 attributes: Vec<Attribute>,
278 /// Child elements.
279 pub children: Element,
280}
281
282#[component]
283pub fn NavbarCollapse(props: NavbarCollapseProps) -> Element {
284 let is_collapsed = *props.collapsed.read();
285 let show = if !is_collapsed { " show" } else { "" };
286
287 let full_class = if props.class.is_empty() {
288 format!("collapse navbar-collapse{show}")
289 } else {
290 format!("collapse navbar-collapse{show} {}", props.class)
291 };
292
293 rsx! {
294 div { class: "{full_class}",
295 ..props.attributes,
296 {props.children}
297 }
298 }
299}
300
301/// Bootstrap NavItem component.
302///
303/// # Bootstrap HTML → Dioxus
304///
305/// | HTML | Dioxus |
306/// |---|---|
307/// | `<li class="nav-item">` | `NavItem { ... }` |
308#[derive(Clone, PartialEq, Props)]
309pub struct NavItemProps {
310 /// Additional CSS classes.
311 #[props(default)]
312 pub class: String,
313 /// Any additional HTML attributes.
314 #[props(extends = GlobalAttributes)]
315 attributes: Vec<Attribute>,
316 /// Child elements (NavLink).
317 pub children: Element,
318}
319
320#[component]
321pub fn NavItem(props: NavItemProps) -> Element {
322 let full_class = if props.class.is_empty() {
323 "nav-item".to_string()
324 } else {
325 format!("nav-item {}", props.class)
326 };
327
328 rsx! {
329 li { class: "{full_class}", ..props.attributes, {props.children} }
330 }
331}
332
333/// Bootstrap NavLink component.
334///
335/// # Bootstrap HTML → Dioxus
336///
337/// | HTML | Dioxus |
338/// |---|---|
339/// | `<a class="nav-link active" href="/">Home</a>` | `NavLink { href: "/", active: true, "Home" }` |
340/// | `<a class="nav-link disabled">Disabled</a>` | `NavLink { disabled: true, "Disabled" }` |
341///
342/// ```rust
343/// rsx! {
344/// NavLink { href: "/dashboard", active: true, "Dashboard" }
345/// }
346/// ```
347#[derive(Clone, PartialEq, Props)]
348pub struct NavLinkProps {
349 /// Link href.
350 #[props(default = "#".to_string())]
351 pub href: String,
352 /// Active state.
353 #[props(default)]
354 pub active: bool,
355 /// Disabled state.
356 #[props(default)]
357 pub disabled: bool,
358 /// Click event handler.
359 #[props(default)]
360 pub onclick: Option<EventHandler<MouseEvent>>,
361 /// Additional CSS classes.
362 #[props(default)]
363 pub class: String,
364 /// Any additional HTML attributes.
365 #[props(extends = GlobalAttributes)]
366 attributes: Vec<Attribute>,
367 /// Child elements.
368 pub children: Element,
369}
370
371#[component]
372pub fn NavLink(props: NavLinkProps) -> Element {
373 let mut classes = vec!["nav-link".to_string()];
374 if props.active {
375 classes.push("active".to_string());
376 }
377 if props.disabled {
378 classes.push("disabled".to_string());
379 }
380 if !props.class.is_empty() {
381 classes.push(props.class.clone());
382 }
383 let full_class = classes.join(" ");
384
385 rsx! {
386 a {
387 class: "{full_class}",
388 href: "{props.href}",
389 "aria-current": if props.active { "page" } else { "" },
390 onclick: move |evt| {
391 if let Some(handler) = &props.onclick {
392 handler.call(evt);
393 }
394 },
395 ..props.attributes,
396 {props.children}
397 }
398 }
399}