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}