dioxus_bootstrap_css/tabs.rs
1use dioxus::prelude::*;
2
3use crate::types::Color;
4
5/// Definition for a single tab.
6///
7/// Used with [`TabList`] to define tab labels, icons, and content.
8///
9/// ```rust,no_run
10/// use dioxus_bootstrap_css::tabs::TabDef;
11///
12/// TabDef {
13/// label: "Home".into(),
14/// icon: Some("house".into()), // Bootstrap Icon name without "bi-" prefix
15/// content: rsx! { p { "Home content" } },
16/// }
17/// ```
18#[derive(Clone, PartialEq)]
19pub struct TabDef {
20 /// Tab button label.
21 pub label: String,
22 /// Optional Bootstrap icon name (without "bi-" prefix).
23 pub icon: Option<String>,
24 /// Tab content.
25 pub content: Element,
26}
27
28/// Bootstrap Tabs component — signal-driven, no JavaScript.
29///
30/// Produces pixel-perfect Bootstrap 5.3 HTML with separated `<ul class="nav nav-tabs">`
31/// and `<div class="tab-content">` areas. This is the **recommended** component for tabs.
32///
33/// # Bootstrap HTML → Dioxus
34///
35/// ```html
36/// <!-- Bootstrap HTML -->
37/// <ul class="nav nav-tabs" role="tablist">
38/// <li class="nav-item"><button class="nav-link active">Home</button></li>
39/// <li class="nav-item"><button class="nav-link">Profile</button></li>
40/// </ul>
41/// <div class="tab-content border border-top-0 rounded-bottom p-3">
42/// <div class="tab-pane fade show active">Home content</div>
43/// <div class="tab-pane fade">Profile content</div>
44/// </div>
45/// ```
46///
47/// ```rust,no_run
48/// use dioxus_bootstrap_css::tabs::TabDef;
49///
50/// let active = use_signal(|| 0usize);
51/// rsx! {
52/// TabList {
53/// active: active,
54/// content_class: "border border-top-0 rounded-bottom p-3",
55/// tabs: vec![
56/// TabDef { label: "Home".into(), icon: Some("house".into()),
57/// content: rsx! { p { "Home content" } } },
58/// TabDef { label: "Profile".into(), icon: Some("person".into()),
59/// content: rsx! { p { "Profile content" } } },
60/// ],
61/// }
62/// }
63/// ```
64///
65/// # Props
66///
67/// - `active` — `Signal<usize>` controlling active tab index
68/// - `tabs` — `Vec<TabDef>` defining each tab's label, icon, and content
69/// - `pills` — pill style instead of tabs
70/// - `fill` — fill available width
71/// - `justified` — equal-width items
72/// - `content_class` — additional CSS classes for the tab-content div
73/// (e.g., `"border border-top-0 rounded-bottom p-3"` for standard Bootstrap bordered tabs)
74#[derive(Clone, PartialEq, Props)]
75pub struct TabListProps {
76 /// Signal controlling the active tab index.
77 pub active: Signal<usize>,
78 /// Tab definitions.
79 pub tabs: Vec<TabDef>,
80 /// Use pill style.
81 #[props(default)]
82 pub pills: bool,
83 /// Fill available width.
84 #[props(default)]
85 pub fill: bool,
86 /// Justify items equally.
87 #[props(default)]
88 pub justified: bool,
89 /// Additional CSS classes for the nav.
90 #[props(default)]
91 pub class: String,
92 /// Additional CSS classes for the tab content area.
93 #[props(default)]
94 pub content_class: String,
95}
96
97#[component]
98pub fn TabList(props: TabListProps) -> Element {
99 let current = *props.active.read();
100 let mut active_signal = props.active;
101 let style = if props.pills { "nav-pills" } else { "nav-tabs" };
102
103 let mut nav_classes = vec![format!("nav {style}")];
104 if props.fill {
105 nav_classes.push("nav-fill".to_string());
106 }
107 if props.justified {
108 nav_classes.push("nav-justified".to_string());
109 }
110 if !props.class.is_empty() {
111 nav_classes.push(props.class.clone());
112 }
113 let nav_class = nav_classes.join(" ");
114
115 let content_class = if props.content_class.is_empty() {
116 "tab-content".to_string()
117 } else {
118 format!("tab-content {}", props.content_class)
119 };
120
121 rsx! {
122 ul { class: "{nav_class}", role: "tablist",
123 for (i, tab) in props.tabs.iter().enumerate() {
124 li { class: "nav-item", role: "presentation",
125 button {
126 class: if current == i { "nav-link active" } else { "nav-link" },
127 r#type: "button",
128 role: "tab",
129 "aria-selected": if current == i { "true" } else { "false" },
130 onclick: move |_| active_signal.set(i),
131 if let Some(ref icon) = tab.icon {
132 i { class: "bi bi-{icon} me-1" }
133 }
134 "{tab.label}"
135 }
136 }
137 }
138 }
139 div { class: "{content_class}",
140 for (i, tab) in props.tabs.iter().enumerate() {
141 div {
142 class: if current == i { "tab-pane fade show active" } else { "tab-pane fade" },
143 role: "tabpanel",
144 if current == i {
145 {tab.content.clone()}
146 }
147 }
148 }
149 }
150 }
151}
152
153/// Alias: `Tabs` works the same as `TabList`.
154///
155/// Both names produce identical output. `TabList` is the canonical name.
156#[component]
157pub fn Tabs(props: TabListProps) -> Element {
158 TabList(props)
159}
160
161/// Alias for TabListProps.
162pub type TabsProps = TabListProps;