Skip to main content

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}