yew_nav_link/components/tab_item.rs
1//! # `NavTab`
2//!
3//! A single tab button within a [`NavTabs`](super::NavTabs) container.
4//! Renders a `<li>` with a `<button>` that has proper ARIA attributes
5//! (`role="tab"`, `aria-selected`, `aria-controls`).
6//!
7//! # Example
8//!
9//! ```rust
10//! use yew::prelude::*;
11//! use yew_nav_link::components::{NavTab, NavTabs};
12//!
13//! #[component]
14//! fn TabBar() -> Html {
15//! html! {
16//! <NavTabs id="my-tabs">
17//! <NavTab active=true id="tab-1" panel_id="panel-1" onclick={None}>
18//! { "Overview" }
19//! </NavTab>
20//! <NavTab active=false id="tab-2" panel_id="panel-2" onclick={None}>
21//! { "Details" }
22//! </NavTab>
23//! <NavTab active=false disabled=true onclick={None}>
24//! { "Disabled" }
25//! </NavTab>
26//! </NavTabs>
27//! }
28//! }
29//! ```
30//!
31//! # CSS Classes
32//!
33//! | Class | Condition |
34//! |-------|-----------|
35//! | `nav-tab` | Always applied |
36//! | `active` | Applied when `active` is `true` |
37//! | `disabled` | Applied when `disabled` is `true` |
38//!
39//! # Props
40//!
41//! | Prop | Type | Default | Description |
42//! |------|------|---------|-------------|
43//! | `active` | `bool` | — | Whether this tab is selected (required) |
44//! | `disabled` | `bool` | `false` | Whether this tab is disabled |
45//! | `id` | `Option<&'static str>` | `None` | Tab button id |
46//! | `panel_id` | `Option<&'static str>` | `None` | aria-controls target |
47//! | `onclick` | `Option<Callback<MouseEvent>>` | — | Click handler (required) |
48//! | `classes` | `Classes` | — | Additional CSS classes |
49//! | `children` | `Children` | — | Tab content |
50
51use yew::prelude::*;
52
53/// Properties for the [`NavTab`] component.
54///
55/// | Prop | Type | Default | Description |
56/// |------|------|---------|-------------|
57/// | `active` | `bool` | — | Whether this tab is selected (required) |
58/// | `disabled` | `bool` | `false` | Whether this tab is disabled |
59/// | `id` | `Option<&'static str>` | `None` | Tab button id |
60/// | `panel_id` | `Option<&'static str>` | `None` | aria-controls target |
61/// | `onclick` | `Option<Callback<MouseEvent>>` | — | Click handler (required) |
62/// | `classes` | `Classes` | — | Additional CSS classes |
63/// | `children` | `Children` | — | Tab content |
64#[derive(Properties, Clone, PartialEq, Debug)]
65pub struct NavTabProps {
66 /// Additional CSS classes applied to the tab.
67 #[prop_or_default]
68 pub classes: Classes,
69
70 /// Whether this tab is currently selected.
71 pub active: bool,
72
73 /// Whether this tab is disabled.
74 #[prop_or_default]
75 pub disabled: bool,
76
77 /// Optional `id` attribute for the tab button.
78 #[prop_or_default]
79 pub id: Option<&'static str>,
80
81 /// Optional `aria-controls` referencing the associated panel `id`.
82 #[prop_or_default]
83 pub panel_id: Option<&'static str>,
84
85 /// Content rendered inside the tab button.
86 #[prop_or_default]
87 pub children: Children,
88
89 /// Click handler invoked when the tab is selected.
90 pub onclick: Option<Callback<MouseEvent>>
91}
92
93/// A single tab button within a [`NavTabs`](super::NavTabs) container.
94///
95/// # CSS Classes
96///
97/// - `nav-tab` - Always applied
98/// - `active` - Applied when `active` is `true`
99/// - `disabled` - Applied when `disabled` is `true`
100#[function_component]
101pub fn NavTab(props: &NavTabProps) -> Html {
102 let mut classes = props.classes.clone();
103 classes.push("nav-tab");
104
105 if props.active {
106 classes.push("active");
107 }
108
109 if props.disabled {
110 classes.push("disabled");
111 }
112
113 let onclick = props.onclick.clone();
114 let onclick = onclick.map(|cb| {
115 move |e: MouseEvent| {
116 e.prevent_default();
117 cb.emit(e);
118 }
119 });
120
121 html! {
122 <li {classes} role="presentation">
123 <button
124 type="button"
125 role="tab"
126 id={props.id}
127 aria-selected={props.active.to_string()}
128 aria-controls={props.panel_id}
129 disabled={props.disabled}
130 onclick={onclick}
131 >
132 { for props.children.iter() }
133 </button>
134 </li>
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn nav_tab_active() {
144 let props = NavTabProps {
145 classes: Classes::default(),
146 active: true,
147 disabled: false,
148 id: None,
149 panel_id: None,
150 children: Children::new(vec![]),
151 onclick: None
152 };
153
154 assert!(props.active);
155 assert!(!props.disabled);
156 }
157
158 #[test]
159 fn nav_tab_disabled() {
160 let props = NavTabProps {
161 classes: Classes::default(),
162 active: false,
163 disabled: true,
164 id: None,
165 panel_id: None,
166 children: Children::new(vec![]),
167 onclick: None
168 };
169
170 assert!(props.disabled);
171 assert!(!props.active);
172 }
173
174 #[test]
175 fn nav_tab_with_id_and_panel_id() {
176 let props = NavTabProps {
177 classes: Classes::default(),
178 active: true,
179 disabled: false,
180 id: Some("tab-1"),
181 panel_id: Some("panel-1"),
182 children: Children::new(vec![]),
183 onclick: None
184 };
185
186 assert_eq!(props.id, Some("tab-1"));
187 assert_eq!(props.panel_id, Some("panel-1"));
188 }
189
190 #[test]
191 fn nav_tab_with_custom_classes() {
192 let mut classes = Classes::new();
193 classes.push("custom-tab");
194 let props = NavTabProps {
195 classes,
196 active: false,
197 disabled: false,
198 id: None,
199 panel_id: None,
200 children: Children::new(vec![]),
201 onclick: None
202 };
203
204 assert!(props.classes.contains("custom-tab"));
205 }
206
207 #[test]
208 fn nav_tab_with_callback() {
209 let callback = Callback::from(|_: MouseEvent| {});
210 let props = NavTabProps {
211 classes: Classes::default(),
212 active: false,
213 disabled: false,
214 id: None,
215 panel_id: None,
216 children: Children::new(vec![]),
217 onclick: Some(callback)
218 };
219
220 assert!(props.onclick.is_some());
221 }
222
223 #[test]
224 fn nav_tab_active_and_disabled() {
225 let props = NavTabProps {
226 classes: Classes::default(),
227 active: true,
228 disabled: true,
229 id: None,
230 panel_id: None,
231 children: Children::new(vec![]),
232 onclick: None
233 };
234
235 assert!(props.active);
236 assert!(props.disabled);
237 }
238}