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