patternfly_yew/components/nav/
mod.rs

1//! Navigation controls
2#[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// nav
19
20/// Properties for [`Nav`]
21#[derive(Clone, Debug, PartialEq, Properties)]
22pub struct NavProperties {
23    #[prop_or_default]
24    pub children: Html,
25
26    /// OUIA Component id
27    #[prop_or_default]
28    pub ouia_id: Option<String>,
29    /// OUIA Component Type
30    #[prop_or(OUIA_NAV.component_type())]
31    pub ouia_type: OuiaComponentType,
32    /// OUIA Component Safe
33    #[prop_or(OuiaSafe::TRUE)]
34    pub ouia_safe: OuiaSafe,
35}
36
37/// A navigation component.
38#[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// nav list
57
58/// Properties for [`NavList`]
59#[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// nav group
75
76/// Properties for [`NavGroup`]
77#[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// nav item
98
99/// Properties for [`NavItem`]
100#[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    /// OUIA Component id
108    #[prop_or_default]
109    pub ouia_id: Option<String>,
110    /// OUIA Component Type
111    #[prop_or(OUIA_NAV_ITEM.component_type())]
112    pub ouia_type: OuiaComponentType,
113    /// OUIA Component Safe
114    #[prop_or(OuiaSafe::TRUE)]
115    pub ouia_safe: OuiaSafe,
116}
117
118/// A navigation item, which triggers a callback when clicked.
119#[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/// Properties for [`NavItem`]
143#[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/// A navigation item, which is a link.
154#[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// nav expandable
181
182/// Properties for [`NavExpandable`]
183#[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
193/// Expandable navigation group/section.
194pub 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            // if this is the first render, and we are expanded, we want to stay that way.
257            // Unless a user explicitly toggles.
258            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        // if we have a current state, that will always override.
303        self.expanded.unwrap_or_else(|| {
304            // if any child is currently active.
305            let active = !self.active.is_empty();
306
307            ctx.props().expanded || active
308        })
309    }
310}
311
312/// Access a wrapping [`Expandable`] content.
313#[hook]
314pub fn use_expandable() -> Option<Expandable> {
315    use_context::<Expandable>()
316}