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