Skip to main content

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}