Skip to main content

yew_nav_link/components/
icon.rs

1// SPDX-FileCopyrightText: 2024-2026 RAprogramm <andrey.rozanov-vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4//! # `NavIcon`
5//!
6//! Icon wrapper for embedding icons in navigation items.
7//! Renders an `<i>` element with `aria-hidden="true"` and a configurable size
8//! class.
9//!
10//! # Example
11//!
12//! ```rust
13//! use yew::prelude::*;
14//! use yew_nav_link::{
15//!     NavItem, NavLink, NavList,
16//!     components::{NavIcon, NavIconSize}
17//! };
18//! use yew_router::prelude::*;
19//!
20//! # #[derive(Clone, PartialEq, Routable)]
21//! # enum Route {
22//! #     #[at("/")]
23//! #     Home,
24//! # }
25//! #[component]
26//! fn Nav() -> Html {
27//!     html! {
28//!         <NavList>
29//!             <NavItem>
30//!                 <NavLink<Route> to={Route::Home}>
31//!                     <NavIcon name="home" size={NavIconSize::Small} />
32//!                     { " Home" }
33//!                 </NavLink<Route>>
34//!             </NavItem>
35//!         </NavList>
36//!     }
37//! }
38//! ```
39//!
40//! # CSS Classes
41//!
42//! | Class | Condition |
43//! |-------|-----------|
44//! | `nav-icon` | Always applied |
45//! | `nav-icon-sm` | Size is `Small` |
46//! | `nav-icon-md` | Size is `Medium` (default) |
47//! | `nav-icon-lg` | Size is `Large` |
48//! | `nav-link-with-icon` | Applied to `NavLinkWithIcon` |
49//!
50//! # Props
51//!
52//! **`NavIcon`:**
53//!
54//! | Prop | Type | Default | Description |
55//! |------|------|---------|-------------|
56//! | `name` | `Option<&'static str>` | `None` | Icon name |
57//! | `size` | `NavIconSize` | `Medium` | Size variant |
58//! | `classes` | `Classes` | — | Additional CSS classes |
59//! | `children` | `Children` | — | Content when `name` is `None` |
60//!
61//! **`NavLinkWithIcon`:**
62//!
63//! | Prop | Type | Default | Description |
64//! |------|------|---------|-------------|
65//! | `icon` | `NavIconSize` | — | Size of the icon (required) |
66//! | `classes` | `Classes` | — | Additional CSS classes |
67//! | `children` | `Children` | — | Content to wrap |
68
69use yew::prelude::*;
70
71/// Properties for the [`NavIcon`] component.
72///
73/// | Prop | Type | Default | Description |
74/// |------|------|---------|-------------|
75/// | `name` | `Option<&'static str>` | `None` | Icon name |
76/// | `size` | `NavIconSize` | `Medium` | Size variant |
77/// | `classes` | `Classes` | — | Additional CSS classes |
78/// | `children` | `Children` | — | Content when `name` is `None` |
79#[derive(Properties, Clone, PartialEq, Debug)]
80pub struct NavIconProps {
81    /// Additional CSS classes applied to the icon.
82    #[prop_or_default]
83    pub classes: Classes,
84
85    /// Optional icon name rendered as text content.
86    #[prop_or_default]
87    pub name: Option<&'static str>,
88
89    /// Size variant of the icon.
90    #[prop_or_default]
91    pub size: NavIconSize,
92
93    /// Content rendered inside the icon when `name` is `None`.
94    #[prop_or_default]
95    pub children: Children
96}
97
98/// Available size variants for [`NavIcon`].
99#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
100pub enum NavIconSize {
101    /// Small icon (`nav-icon-sm`).
102    Small,
103    /// Medium icon, the default size (`nav-icon-md`).
104    #[default]
105    Medium,
106    /// Large icon (`nav-icon-lg`).
107    Large
108}
109
110impl NavIconSize {
111    const fn as_class(self) -> &'static str {
112        match self {
113            Self::Small => "nav-icon-sm",
114            Self::Medium => "nav-icon-md",
115            Self::Large => "nav-icon-lg"
116        }
117    }
118}
119
120/// Icon component for embedding icons within navigation elements.
121///
122/// Renders an `<i>` element with `aria-hidden="true"` and a size class.
123///
124/// # CSS Classes
125///
126/// - `nav-icon` - Always applied
127/// - `nav-icon-sm`, `nav-icon-md`, `nav-icon-lg` - Size variant
128#[function_component]
129pub fn NavIcon(props: &NavIconProps) -> Html {
130    let mut classes = props.classes.clone();
131    classes.push("nav-icon");
132    classes.push(props.size.as_class());
133
134    if let Some(name) = props.name {
135        html! {
136            <i class={classes} aria-hidden="true">
137                { name }
138            </i>
139        }
140    } else {
141        html! {
142            <i class={classes} aria-hidden="true">
143                { for props.children.iter() }
144            </i>
145        }
146    }
147}
148
149/// Properties for the [`NavLinkWithIcon`] component.
150///
151/// | Prop | Type | Default | Description |
152/// |------|------|---------|-------------|
153/// | `icon` | `NavIconSize` | — | Size of the icon (required) |
154/// | `classes` | `Classes` | — | Additional CSS classes |
155/// | `children` | `Children` | — | Content to wrap |
156#[derive(Properties, Clone, PartialEq, Debug)]
157pub struct NavLinkWithIconProps {
158    /// Additional CSS classes applied to the wrapper.
159    #[prop_or_default]
160    pub classes: Classes,
161
162    /// Size of the associated icon.
163    pub icon: NavIconSize,
164
165    /// Content rendered inside the span.
166    pub children: Children
167}
168
169/// Wraps content alongside an icon within a navigation link.
170///
171/// # CSS Classes
172///
173/// - `nav-link-with-icon` - Always applied
174#[function_component]
175pub fn NavLinkWithIcon(props: &NavLinkWithIconProps) -> Html {
176    let mut classes = props.classes.clone();
177    classes.push("nav-link-with-icon");
178
179    html! {
180        <span {classes}>
181            { for props.children.iter() }
182        </span>
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn nav_icon_props_default() {
192        let props = NavIconProps {
193            classes:  Classes::default(),
194            name:     None,
195            size:     NavIconSize::default(),
196            children: Children::new(vec![])
197        };
198
199        assert!(props.name.is_none());
200        assert_eq!(props.size, NavIconSize::Medium);
201    }
202
203    #[test]
204    fn nav_icon_with_name() {
205        let props = NavIconProps {
206            classes:  Classes::default(),
207            name:     Some("home"),
208            size:     NavIconSize::Small,
209            children: Children::new(vec![])
210        };
211
212        assert_eq!(props.name, Some("home"));
213        assert_eq!(props.size, NavIconSize::Small);
214    }
215
216    #[test]
217    fn nav_icon_size_variants() {
218        assert_eq!(NavIconSize::Small.as_class(), "nav-icon-sm");
219        assert_eq!(NavIconSize::Medium.as_class(), "nav-icon-md");
220        assert_eq!(NavIconSize::Large.as_class(), "nav-icon-lg");
221    }
222
223    #[test]
224    fn nav_icon_default() {
225        assert_eq!(NavIconSize::default(), NavIconSize::Medium);
226    }
227}