patternfly_yew/components/nav/
mod.rs1#[cfg(feature = "yew-nested-router")]
3mod router;
4
5#[cfg(feature = "yew-nested-router")]
6pub use router::*;
7use std::collections::HashSet;
8
9use crate::ouia;
10use crate::prelude::{Icon, Id, OuiaComponentType};
11use crate::utils::{Ouia, OuiaSafe};
12use std::fmt::Debug;
13use yew::prelude::*;
14
15const OUIA_NAV: Ouia = ouia!("Nav");
16const OUIA_NAV_ITEM: Ouia = ouia!("NavItem");
17
18#[derive(Clone, Debug, PartialEq, Properties)]
22pub struct NavProperties {
23 #[prop_or_default]
24 pub children: Html,
25
26 #[prop_or_default]
28 pub ouia_id: Option<String>,
29 #[prop_or(OUIA_NAV.component_type())]
31 pub ouia_type: OuiaComponentType,
32 #[prop_or(OuiaSafe::TRUE)]
34 pub ouia_safe: OuiaSafe,
35}
36
37#[function_component(Nav)]
39pub fn nav(props: &NavProperties) -> Html {
40 let ouia_id = use_memo(props.ouia_id.clone(), |id| {
41 id.clone().unwrap_or(OUIA_NAV.generated_id())
42 });
43 html! {
44 <nav
45 class="pf-v5-c-nav"
46 aria-label="Global"
47 data-ouia-component-id={(*ouia_id).clone()}
48 data-ouia-component-type={props.ouia_type}
49 data-ouia-safe={props.ouia_safe}
50 >
51 { props.children.clone() }
52 </nav>
53 }
54}
55
56#[derive(Clone, PartialEq, Properties)]
60pub struct NavListProperties {
61 #[prop_or_default]
62 pub children: Html,
63}
64
65#[function_component(NavList)]
66pub fn nav_list(props: &NavListProperties) -> Html {
67 html! {
68 <ul class="pf-v5-c-nav__list" role="list">
69 { props.children.clone() }
70 </ul>
71 }
72}
73
74#[derive(Clone, PartialEq, Properties)]
78pub struct NavGroupProperties {
79 #[prop_or_default]
80 pub children: Html,
81 #[prop_or_default]
82 pub title: String,
83}
84
85#[function_component(NavGroup)]
86pub fn nav_group(props: &NavGroupProperties) -> Html {
87 html! {
88 <section class="pf-v5-c-nav__section">
89 <h2 class="pf-v5-c-nav__section-title">{ props.title.clone() }</h2>
90 <NavList>
91 { props.children.clone() }
92 </NavList>
93 </section>
94 }
95}
96
97#[derive(Clone, PartialEq, Properties)]
101pub struct NavItemProperties {
102 #[prop_or_default]
103 pub children: Html,
104 #[prop_or_default]
105 pub onclick: Callback<()>,
106
107 #[prop_or_default]
109 pub ouia_id: Option<String>,
110 #[prop_or(OUIA_NAV_ITEM.component_type())]
112 pub ouia_type: OuiaComponentType,
113 #[prop_or(OuiaSafe::TRUE)]
115 pub ouia_safe: OuiaSafe,
116}
117
118#[function_component(NavItem)]
120pub fn nav_item(props: &NavItemProperties) -> Html {
121 let ouia_id = use_memo(props.ouia_id.clone(), |id| {
122 id.clone().unwrap_or(OUIA_NAV_ITEM.generated_id())
123 });
124 html! (
125 <li
126 class="pf-v5-c-nav__item"
127 data-ouia-component-id={(*ouia_id).clone()}
128 data-ouia-component-type={props.ouia_type}
129 data-ouia-safe={props.ouia_safe}
130 >
131 <a
132 href="#"
133 class="pf-v5-c-nav__link"
134 onclick={props.onclick.reform(|_|())}
135 >
136 { props.children.clone() }
137 </a>
138 </li>
139 )
140}
141
142#[derive(Clone, PartialEq, Properties)]
144pub struct NavLinkProperties {
145 #[prop_or_default]
146 pub children: Html,
147 #[prop_or_default]
148 pub href: AttrValue,
149 #[prop_or_default]
150 pub target: Option<AttrValue>,
151}
152
153#[function_component(NavLink)]
155pub fn nav_link(props: &NavLinkProperties) -> Html {
156 html! (
157 <li class="pf-v5-c-nav__item">
158 <a
159 href={&props.href}
160 class="pf-v5-c-nav__link"
161 target={&props.target}
162 >
163 { props.children.clone() }
164 </a>
165 </li>
166 )
167}
168
169#[derive(Clone, PartialEq)]
170pub struct Expandable {
171 callback: Callback<(Id, bool)>,
172}
173
174impl Expandable {
175 pub fn state(&self, id: Id, active: bool) {
176 self.callback.emit((id, active));
177 }
178}
179
180#[derive(Clone, PartialEq, Properties)]
184pub struct NavExpandableProperties {
185 #[prop_or_default]
186 pub children: Html,
187 #[prop_or_default]
188 pub title: String,
189 #[prop_or_default]
190 pub expanded: bool,
191}
192
193pub struct NavExpandable {
195 expanded: Option<bool>,
196 context: Expandable,
197 active: HashSet<Id>,
198}
199
200#[doc(hidden)]
201#[derive(Clone, Debug)]
202pub enum MsgExpandable {
203 Toggle,
204 ChildState(Id, bool),
205}
206
207impl Component for NavExpandable {
208 type Message = MsgExpandable;
209 type Properties = NavExpandableProperties;
210
211 fn create(ctx: &Context<Self>) -> Self {
212 let expanded = match ctx.props().expanded {
213 true => Some(true),
214 false => None,
215 };
216
217 log::debug!("Creating new NavExpandable");
218
219 let callback = ctx
220 .link()
221 .callback(|(id, state)| MsgExpandable::ChildState(id, state));
222
223 Self {
224 expanded,
225 active: Default::default(),
226 context: Expandable { callback },
227 }
228 }
229
230 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
231 match msg {
232 MsgExpandable::Toggle => {
233 self.expanded = Some(!self.is_expanded(ctx));
234 }
235 MsgExpandable::ChildState(id, state) => match state {
236 true => {
237 self.active.insert(id);
238 }
239 false => {
240 self.active.remove(&id);
241 }
242 },
243 }
244 true
245 }
246
247 fn changed(&mut self, ctx: &Context<Self>, _: &Self::Properties) -> bool {
248 if ctx.props().expanded {
249 self.expanded = Some(true);
250 }
251 true
252 }
253
254 fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
255 if first_render && self.expanded.is_none() && self.is_expanded(ctx) {
256 self.expanded = Some(true);
259 }
260 }
261
262 fn view(&self, ctx: &Context<Self>) -> Html {
263 let mut classes = Classes::from("pf-v5-c-nav__item pf-m-expandable");
264
265 let expanded = self.is_expanded(ctx);
266
267 if expanded {
268 classes.push("pf-m-expanded");
269 }
270
271 let context = self.context.clone();
272
273 html! {
274 <ContextProvider<Expandable> {context}>
275 <li class={classes}>
276 <button
277 class="pf-v5-c-nav__link"
278 aria-expanded={expanded.to_string()}
279 onclick={ctx.link().callback(|_|MsgExpandable::Toggle)}
280 >
281 { &ctx.props().title }
282 <span class="pf-v5-c-nav__toggle">
283 <span class="pf-v5-c-nav__toggle-icon">
284 { Icon::AngleRight }
285 </span>
286 </span>
287 </button>
288
289 <section class="pf-v5-c-nav__subnav" hidden={!expanded}>
290 <NavList>
291 { ctx.props().children.clone() }
292 </NavList>
293 </section>
294 </li>
295 </ContextProvider<Expandable>>
296 }
297 }
298}
299
300impl NavExpandable {
301 fn is_expanded(&self, ctx: &Context<Self>) -> bool {
302 self.expanded.unwrap_or_else(|| {
304 let active = !self.active.is_empty();
306
307 ctx.props().expanded || active
308 })
309 }
310}
311
312#[hook]
314pub fn use_expandable() -> Option<Expandable> {
315 use_context::<Expandable>()
316}