yew_nav_link/components/tab_item.rs
1// SPDX-FileCopyrightText: 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">
21//! { "Overview" }
22//! </NavTab>
23//! <NavTab active=false id="tab-2" panel_id="panel-2">
24//! { "Details" }
25//! </NavTab>
26//! <NavTab active=false disabled=true>
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<AttrValue>` | `None` | Tab button id |
49//! | `panel_id` | `Option<AttrValue>` | `None` | aria-controls target |
50//! | `onclick` | `Option<Callback<MouseEvent>>` | `None` | Click handler |
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<AttrValue>` | `None` | Tab button id |
63/// | `panel_id` | `Option<AttrValue>` | `None` | aria-controls target |
64/// | `onclick` | `Option<Callback<MouseEvent>>` | `None` | Click handler |
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<AttrValue>,
83
84 /// Optional `aria-controls` referencing the associated panel `id`.
85 #[prop_or_default]
86 pub panel_id: Option<AttrValue>,
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 #[prop_or_default]
94 pub onclick: Option<Callback<MouseEvent>>
95}
96
97/// A single tab button within a [`NavTabs`](super::NavTabs) container.
98///
99/// The rendered button participates in the tabs-pattern roving tabindex:
100/// the active tab carries `tabindex="0"` and every other tab `tabindex="-1"`,
101/// so `Tab` enters the tablist on the selected tab and arrow keys (handled
102/// by [`NavTabs`](super::NavTabs)) move within it.
103///
104/// # CSS Classes
105///
106/// - `nav-tab` - Always applied
107/// - `active` - Applied when `active` is `true`
108/// - `disabled` - Applied when `disabled` is `true`
109#[function_component]
110pub fn NavTab(props: &NavTabProps) -> Html {
111 let mut classes = props.classes.clone();
112 classes.push("nav-tab");
113
114 if props.active {
115 classes.push("active");
116 }
117
118 if props.disabled {
119 classes.push("disabled");
120 }
121
122 let onclick = props.onclick.clone();
123 let onclick = onclick.map(|cb| {
124 move |e: MouseEvent| {
125 e.prevent_default();
126 cb.emit(e);
127 }
128 });
129
130 html! {
131 <li class={classes} role="presentation">
132 <button
133 type="button"
134 role="tab"
135 id={props.id.clone()}
136 aria-selected={if props.active { "true" } else { "false" }}
137 tabindex={if props.active { "0" } else { "-1" }}
138 aria-controls={props.panel_id.clone()}
139 disabled={props.disabled}
140 onclick={onclick}
141 >
142 { for props.children.iter() }
143 </button>
144 </li>
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn nav_tab_active() {
154 let props = NavTabProps {
155 classes: Classes::default(),
156 active: true,
157 disabled: false,
158 id: None,
159 panel_id: None,
160 children: Children::new(vec![]),
161 onclick: None
162 };
163
164 assert!(props.active);
165 assert!(!props.disabled);
166 }
167
168 #[test]
169 fn nav_tab_disabled() {
170 let props = NavTabProps {
171 classes: Classes::default(),
172 active: false,
173 disabled: true,
174 id: None,
175 panel_id: None,
176 children: Children::new(vec![]),
177 onclick: None
178 };
179
180 assert!(props.disabled);
181 assert!(!props.active);
182 }
183
184 #[test]
185 fn nav_tab_with_id_and_panel_id() {
186 let props = NavTabProps {
187 classes: Classes::default(),
188 active: true,
189 disabled: false,
190 id: Some(AttrValue::Static("tab-1")),
191 panel_id: Some(AttrValue::Static("panel-1")),
192 children: Children::new(vec![]),
193 onclick: None
194 };
195
196 assert_eq!(props.id.as_deref(), Some("tab-1"));
197 assert_eq!(props.panel_id.as_deref(), Some("panel-1"));
198 }
199
200 #[test]
201 fn nav_tab_with_custom_classes() {
202 let mut classes = Classes::new();
203 classes.push("custom-tab");
204 let props = NavTabProps {
205 classes,
206 active: false,
207 disabled: false,
208 id: None,
209 panel_id: None,
210 children: Children::new(vec![]),
211 onclick: None
212 };
213
214 assert!(props.classes.contains("custom-tab"));
215 }
216
217 #[test]
218 fn nav_tab_with_callback() {
219 let callback = Callback::from(|_: MouseEvent| {});
220 let props = NavTabProps {
221 classes: Classes::default(),
222 active: false,
223 disabled: false,
224 id: None,
225 panel_id: None,
226 children: Children::new(vec![]),
227 onclick: Some(callback)
228 };
229
230 assert!(props.onclick.is_some());
231 }
232
233 #[test]
234 fn nav_tab_active_and_disabled() {
235 let props = NavTabProps {
236 classes: Classes::default(),
237 active: true,
238 disabled: true,
239 id: None,
240 panel_id: None,
241 children: Children::new(vec![]),
242 onclick: None
243 };
244
245 assert!(props.active);
246 assert!(props.disabled);
247 }
248}