yew_bootstrap/component/
navbar.rs

1use yew::prelude::*;
2use super::Container;
3use crate::util::Dimension;
4use crate::icons::BI;
5
6/// # A singular dropdown item, child of [NavDropdown]
7/// Used as a child of [NavDropdown] to create a dropdown menu.
8///
9/// See [NavDropdownItemProps] for a listing of properties.
10pub struct NavDropdownItem { }
11
12/// # Properties for [NavDropdown]
13#[derive(Properties, Clone, PartialEq)]
14pub struct NavDropdownItemProps {
15    /// Item text
16    #[prop_or_default]
17    pub text: AttrValue,
18    /// Link for the item
19    #[prop_or_default]
20    pub url: Option<AttrValue>,
21    /// Callback when clicked.
22    ///
23    /// **Tip:** To make browsers show a "link" mouse cursor for the
24    /// [NavDropdownItem], set `url="#"` and call [`Event::prevent_default()`]
25    /// from your callback.
26    #[prop_or_default]
27    pub onclick: Callback<MouseEvent>,
28    /// Optional icon
29    #[prop_or_default]
30    pub icon: Option<&'static BI>,
31}
32
33impl Component for NavDropdownItem {
34    type Message = ();
35    type Properties = NavDropdownItemProps;
36
37    fn create(_ctx: &Context<Self>) -> Self {
38        Self {}
39    }
40
41    fn view(&self, ctx: &Context<Self>) -> Html {
42        let props = ctx.props();
43
44        html! {
45            <li>
46                <a
47                    class="dropdown-item"
48                    href={props.url.clone()}
49                    onclick={props.onclick.clone()}
50                >
51                    if let Some(icon) = props.icon {
52                        {icon}{" "} // add a space after the icon, otherwise it looks squished
53                    }
54                    {props.text.clone()}
55                </a>
56            </li>
57        }
58    }
59}
60
61/// A dropdown menu, child of [NavBar]. See [NavDropdownProps] for a listing of properties.
62#[derive(Clone, PartialEq, Eq)]
63pub struct NavDropdown { }
64
65/// Properties for [NavDropdown]
66#[derive(Properties, Clone, PartialEq)]
67pub struct NavDropdownProps {
68    #[prop_or_default]
69    pub children: Children,
70    /// the id of the link with the dropdown-toggle class, referenced by aria-labelledby
71    #[prop_or_default]
72    pub id: AttrValue,
73    /// If true, menu is expanded (ie visible)
74    #[prop_or_default]
75    pub expanded: bool,
76    /// the text of the link with the dropdown-toggle class
77    #[prop_or_default]
78    pub text: AttrValue,
79    /// Top level path is the currently active one
80    #[prop_or_default]
81    pub active: bool,
82    /// Optional icon
83    #[prop_or_default]
84    pub icon: Option<&'static BI>,
85}
86
87impl Component for NavDropdown {
88    type Message = ();
89    type Properties = NavDropdownProps;
90
91    fn create(_ctx: &Context<Self>) -> Self {
92        Self { }
93    }
94
95    fn view(&self, ctx: &Context<Self>) -> Html {
96        let props = ctx.props();
97
98        let expanded = String::from(match props.expanded {
99            true => "true",
100            false => "false"
101        });
102
103
104        let mut dropdown_toggle_classes = Classes::new();
105        dropdown_toggle_classes.push(String::from("nav-link"));
106        dropdown_toggle_classes.push(String::from("dropdown-toggle"));
107
108        if props.active {
109            dropdown_toggle_classes.push(String::from("active"));
110        }
111
112        html! {
113            <li class="nav-item dropdown">
114                <a class={dropdown_toggle_classes} href="#" id={props.id.clone()} role="button" data-bs-toggle="dropdown" aria-expanded={expanded}>
115                    if let Some(icon) = props.icon {
116                        {icon}{" "}
117                    }
118                    {props.text.clone()}
119                </a>
120                <ul class="dropdown-menu" aria-labelledby={props.id.clone()}>
121                    { for props.children.iter() }
122                </ul>
123            </li>
124        }
125    }
126}
127
128/// # Item of a [NavBar]
129/// This typically contains text inside a link
130///
131/// Refer to [NavItemProperties] for a listing of properties
132pub struct NavItem { }
133
134/// Properties for NavItem
135#[derive(Properties, Clone, PartialEq)]
136pub struct NavItemProperties {
137    /// If provided, text is inside a link
138    #[prop_or_default]
139    pub url: Option<AttrValue>,
140    /// Link is the currently active one
141    #[prop_or_default]
142    pub active: bool,
143    /// Link is disabled
144    #[prop_or_default]
145    pub disabled: bool,
146    /// Text of the item, ignored if dropdown is Some
147    #[prop_or_default]
148    pub text: AttrValue,
149    /// required for dropdowns
150    #[prop_or_default]
151    pub id: AttrValue,
152    /// dropdown items
153    #[prop_or_default]
154    pub children: Children,
155    /// Callback when clicked.
156    ///
157    /// **Tip:** To make browsers show a "link" mouse cursor for the [NavItem],
158    /// set `url="#"` and call [`Event::prevent_default()`] from your callback.
159    #[prop_or_default]
160    pub onclick: Callback<MouseEvent>,
161    /// Optional icon
162    #[prop_or_default]
163    pub icon: Option<&'static BI>,
164}
165
166impl Component for NavItem {
167    type Message = ();
168    type Properties = NavItemProperties;
169
170    fn create(_ctx: &Context<Self>) -> Self {
171        Self {}
172    }
173
174    fn view(&self, ctx: &Context<Self>) -> Html {
175        let props = ctx.props();
176
177        match &props.children.is_empty() {
178            true => {
179                let mut classes = Classes::new();
180                classes.push(String::from("nav-link"));
181
182                if props.active {
183                    classes.push(String::from("active"));
184                }
185
186                if props.disabled {
187                    classes.push(String::from("disabled"));
188                }
189
190                match props.disabled {
191                    true => {
192                        html! {
193                            <li class="nav-item">
194                                <a
195                                    class={classes}
196                                    tabindex="-1"
197                                    aria-disabled="true"
198                                    href={props.url.clone()}
199                                    onclick={props.onclick.clone()}
200                                >
201                                    if let Some(icon) = props.icon {
202                                        {icon}{" "}
203                                    }
204                                    {props.text.clone()}
205                                </a>
206                            </li>
207                        }
208                    },
209                    false => {
210                        html! {
211                            <li class="nav-item">
212                                <a
213                                    class={classes}
214                                    href={props.url.clone()}
215                                    onclick={props.onclick.clone()}
216                                >
217                                    if let Some(icon) = props.icon {
218                                        {icon}{" "}
219                                    }
220                                    {props.text.clone()}
221                                </a>
222                            </li>
223                        }
224                    }
225                }
226            },
227            false => {
228                html! {
229                    <NavDropdown text={props.text.clone()} id={props.id.clone()} active={props.active}>
230                        { for props.children.iter() }
231                    </NavDropdown>
232                }
233            }
234        }
235    }
236}
237
238/// # Brand type for a [NavBar]
239///
240/// This can contain a text, icon, image or combined (text and image)
241#[derive(Clone, PartialEq, Eq)]
242pub enum BrandType {
243    /// Text with optional link
244    BrandSimple {
245        text: AttrValue, url: Option<AttrValue> },
246    /// a brand icon is a bootstrap icon, requiring bootstrap-icons to be imported;
247    /// see [crate::icons]
248    BrandIcon { icon: BI, text: AttrValue, url: Option<AttrValue> },
249    /// Image with optional dimensions, link and descriptive text
250    BrandImage {
251        /// browser-accessible url to the brand image
252        image_url: AttrValue,
253        /// descriptive text for screen reader users
254        alt: AttrValue,
255        dimension: Option<Dimension>
256    },
257    /// Combined image and text with URL
258    BrandCombined {
259        text: AttrValue,
260        /// hyperlink destination for brand text
261        url: Option<AttrValue>,
262        /// browser-accessible url to the brand image
263        image_url: AttrValue,
264        /// descriptive text for screen reader users
265        alt: AttrValue,
266        dimension: Option<Dimension>
267    }
268}
269
270/// # Navbar component, parent of [NavItem], [NavDropdown], and [NavDropdownItem]
271/// The navbar is a responsive horizontal menu bar that can contain links, dropdowns, and text.
272/// We have broken up this component into several sub-components to make it easier to use: [NavItem], [NavDropdown], and [NavDropdownItem].
273/// The brand property is set using the [BrandType] enum.
274///
275/// See [NavBarProps] for more information on properties supported by this component.
276/// # Example
277/// ```rust
278/// use yew::prelude::*;
279/// use yew_bootstrap::component::{BrandType, NavBar, NavDropdownItem, NavItem};
280///
281/// fn test() -> Html {
282///     let brand = BrandType::BrandSimple {
283///         text: AttrValue::from("Yew Bootstrap"),
284///         url: Some(AttrValue::from("https://yew.rs"))
285///     };
286///     html!{
287///         <NavBar nav_id={"test-nav"} class="navbar-expand-lg navbar-light bg-light" brand={brand}>
288///             <NavItem text="Home" url={AttrValue::from("/")} />
289///             <NavItem text="more">
290///                 <NavDropdownItem text="dropdown item 1" url={AttrValue::from("/dropdown1")} />
291///             </NavItem>
292///         </NavBar>
293///     }
294/// }
295/// ```
296pub struct NavBar { }
297
298/// Properties for [NavBar]
299#[derive(Properties, Clone, PartialEq)]
300pub struct NavBarProps {
301    #[prop_or_default]
302    pub children: Children,
303    /// CSS class
304    #[prop_or_default]
305    pub class: AttrValue,
306
307    /// the id of the div that contains the nav-items
308    #[prop_or_default]
309    pub nav_id: AttrValue,
310
311    /// Navbar is expanded. Used to notify assitive technologies via aria-expanded
312    #[prop_or_default]
313    pub expanded: bool,
314
315    /// Brand type, see [BrandType]
316    #[prop_or_default]
317    pub brand: Option<BrandType>,
318
319    /// Callback when brand is clicked
320    #[prop_or_default]
321    pub brand_callback: Callback<MouseEvent>
322}
323
324impl Component for NavBar {
325    type Message = ();
326    type Properties = NavBarProps;
327
328    fn create(_ctx: &Context<Self>) -> Self {
329        Self {}
330    }
331
332    fn view(&self, ctx: &Context<Self>) -> Html {
333        let props = ctx.props();
334
335        let expanded = String::from(match &props.expanded {
336            true => {
337                "true"
338            },
339            false => {
340                "false"
341            }
342        });
343
344        let mut classes = Classes::new();
345        classes.push("navbar");
346        classes.push(props.class.to_string());
347
348        let brand = match &props.brand {
349            None => html!{},
350            Some(b) => {
351                match b {
352                    BrandType::BrandSimple{text, url} => {
353                        let url = match url {
354                            Some(u) => u.clone(),
355                            None => AttrValue::from("#")
356                        };
357
358                        html!{
359                            <a class="navbar-brand" href={url} onclick={props.brand_callback.clone()}>
360                                {text.clone()}
361                            </a>
362                        }
363                    },
364                    BrandType::BrandIcon { text, icon, url } => {
365                        let url = match url {
366                            Some(u) => u.clone(),
367                            None => AttrValue::from("#")
368                        };
369                        html! {
370                            <a class="navbar-brand" href={url} onclick={props.brand_callback.clone()}>
371                                {icon}
372                                {text.clone()}
373                            </a>
374                        }
375                    }
376                    BrandType::BrandImage { image_url, alt, dimension } => {
377                        match dimension {
378                            None => {
379                                html! {
380                                    <a class="navbar-brand" href={"#"} onclick={props.brand_callback.clone()}>
381                                        <img src={image_url.clone()} alt={alt.clone()} class="d-inline-block align-text-top" />
382                                    </a>
383                                }
384                            }
385                            Some(Dimension{width, height}) => {
386                                html! {
387                                    <a class="navbar-brand" href={"#"} onclick={props.brand_callback.clone()}>
388                                        <img src={image_url.clone()} alt={alt.clone()} width={width.clone()} height={height.clone()} class="d-inline-block align-text-top" />
389                                    </a>
390                                }
391                            }
392                        }
393                    }
394                    BrandType::BrandCombined { text, url, image_url, alt, dimension } => {
395                        let url = match url {
396                            Some(u) => u.clone(),
397                            None => AttrValue::from("#")
398                        };
399                        match dimension {
400                            None => {
401                                html! {
402                                    <a class="navbar-brand" href={url} onclick={props.brand_callback.clone()}>
403                                        <img src={image_url.clone()} alt={alt.clone()} class="d-inline-block align-text-top" />
404                                        {text.clone()}
405                                    </a>
406                                }
407                            },
408                            Some(Dimension{width, height}) => {
409                                html! {
410                                    <a class="navbar-brand" href={url} onclick={props.brand_callback.clone()}>
411                                        <img src={image_url.clone()} alt={alt.clone()} width={width.clone()} height={height.clone()} class="d-inline-block align-text-top" />
412                                        {text.clone()}
413                                    </a>
414                                }
415                            }
416                        }
417                    }
418                }
419            }
420        };
421
422        html! {
423            <nav class={classes}>
424                <Container fluid=true>
425                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target={format!("#{}", props.nav_id.clone())} aria-controls={props.nav_id.clone()} aria-expanded={expanded} aria-label="Toggle navigation">
426                        <span class="navbar-toggler-icon"></span>
427                    </button>
428                    {brand}
429                    <div class="collapse navbar-collapse" id={props.nav_id.clone()}>
430                        <ul class="navbar-nav">
431                            { for props.children.clone() }
432                        </ul>
433                    </div>
434                </Container>
435            </nav>
436        }
437    }
438}