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-v6-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! { <ul class="pf-v6-c-nav__list" role="list">{ props.children.clone() }</ul> }
68}
69
70#[derive(Clone, PartialEq, Properties)]
74pub struct NavGroupProperties {
75 #[prop_or_default]
76 pub children: Html,
77 #[prop_or_default]
78 pub title: String,
79}
80
81#[function_component(NavGroup)]
82pub fn nav_group(props: &NavGroupProperties) -> Html {
83 html! {
84 <section class="pf-v6-c-nav__section">
85 <h2 class="pf-v6-c-nav__section-title">{ props.title.clone() }</h2>
86 <NavList>{ props.children.clone() }</NavList>
87 </section>
88 }
89}
90
91#[derive(Clone, PartialEq, Properties)]
95pub struct NavItemProperties {
96 #[prop_or_default]
97 pub children: Html,
98 #[prop_or_default]
99 pub onclick: Callback<()>,
100
101 #[prop_or_default]
103 pub ouia_id: Option<String>,
104 #[prop_or(OUIA_NAV_ITEM.component_type())]
106 pub ouia_type: OuiaComponentType,
107 #[prop_or(OuiaSafe::TRUE)]
109 pub ouia_safe: OuiaSafe,
110}
111
112#[function_component(NavItem)]
114pub fn nav_item(props: &NavItemProperties) -> Html {
115 let ouia_id = use_memo(props.ouia_id.clone(), |id| {
116 id.clone().unwrap_or(OUIA_NAV_ITEM.generated_id())
117 });
118 html! (
119 <li
120 class="pf-v6-c-nav__item"
121 data-ouia-component-id={(*ouia_id).clone()}
122 data-ouia-component-type={props.ouia_type}
123 data-ouia-safe={props.ouia_safe}
124 >
125 <a href="#" class="pf-v6-c-nav__link" onclick={props.onclick.reform(|_|())}>
126 { props.children.clone() }
127 </a>
128 </li>
129 )
130}
131
132#[derive(Clone, PartialEq, Properties)]
134pub struct NavLinkProperties {
135 #[prop_or_default]
136 pub children: Html,
137 #[prop_or_default]
138 pub href: AttrValue,
139 #[prop_or_default]
140 pub target: Option<AttrValue>,
141}
142
143#[function_component(NavLink)]
145pub fn nav_link(props: &NavLinkProperties) -> Html {
146 html! (
147 <li class="pf-v6-c-nav__item">
148 <a href={&props.href} class="pf-v6-c-nav__link" target={&props.target}>
149 { props.children.clone() }
150 </a>
151 </li>
152 )
153}
154
155#[derive(Clone, PartialEq)]
156pub struct Expandable {
157 callback: Callback<(Id, bool)>,
158}
159
160impl Expandable {
161 pub fn state(&self, id: Id, active: bool) {
162 self.callback.emit((id, active));
163 }
164}
165
166#[derive(Clone, PartialEq, Properties)]
170pub struct NavExpandableProperties {
171 #[prop_or_default]
172 pub children: Html,
173 #[prop_or_default]
174 pub title: String,
175 #[prop_or_default]
176 pub expanded: bool,
177}
178
179pub struct NavExpandable {
181 expanded: Option<bool>,
182 context: Expandable,
183 active: HashSet<Id>,
184}
185
186#[doc(hidden)]
187#[derive(Clone, Debug)]
188pub enum MsgExpandable {
189 Toggle,
190 ChildState(Id, bool),
191}
192
193impl Component for NavExpandable {
194 type Message = MsgExpandable;
195 type Properties = NavExpandableProperties;
196
197 fn create(ctx: &Context<Self>) -> Self {
198 let expanded = match ctx.props().expanded {
199 true => Some(true),
200 false => None,
201 };
202
203 log::debug!("Creating new NavExpandable");
204
205 let callback = ctx
206 .link()
207 .callback(|(id, state)| MsgExpandable::ChildState(id, state));
208
209 Self {
210 expanded,
211 active: Default::default(),
212 context: Expandable { callback },
213 }
214 }
215
216 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
217 match msg {
218 MsgExpandable::Toggle => {
219 self.expanded = Some(!self.is_expanded(ctx));
220 }
221 MsgExpandable::ChildState(id, state) => match state {
222 true => {
223 self.active.insert(id);
224 }
225 false => {
226 self.active.remove(&id);
227 }
228 },
229 }
230 true
231 }
232
233 fn changed(&mut self, ctx: &Context<Self>, _: &Self::Properties) -> bool {
234 if ctx.props().expanded {
235 self.expanded = Some(true);
236 }
237 true
238 }
239
240 fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
241 if first_render && self.expanded.is_none() && self.is_expanded(ctx) {
242 self.expanded = Some(true);
245 }
246 }
247
248 fn view(&self, ctx: &Context<Self>) -> Html {
249 let mut classes = Classes::from("pf-v6-c-nav__item pf-m-expandable");
250
251 let expanded = self.is_expanded(ctx);
252
253 if expanded {
254 classes.push("pf-m-expanded");
255 }
256
257 let context = self.context.clone();
258
259 html! {
260 <ContextProvider<Expandable> {context}>
261 <li class={classes}>
262 <button
263 class="pf-v6-c-nav__link"
264 aria-expanded={expanded.to_string()}
265 onclick={ctx.link().callback(|_|MsgExpandable::Toggle)}
266 >
267 { &ctx.props().title }
268 <span class="pf-v6-c-nav__toggle">
269 <span class="pf-v6-c-nav__toggle-icon">{ Icon::AngleRight }</span>
270 </span>
271 </button>
272 <section class="pf-v6-c-nav__subnav" hidden={!expanded}>
273 <NavList>{ ctx.props().children.clone() }</NavList>
274 </section>
275 </li>
276 </ContextProvider<Expandable>>
277 }
278 }
279}
280
281impl NavExpandable {
282 fn is_expanded(&self, ctx: &Context<Self>) -> bool {
283 self.expanded.unwrap_or_else(|| {
285 let active = !self.active.is_empty();
287
288 ctx.props().expanded || active
289 })
290 }
291}
292
293#[hook]
295pub fn use_expandable() -> Option<Expandable> {
296 use_context::<Expandable>()
297}