Skip to main content

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-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// 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! { <ul class="pf-v6-c-nav__list" role="list">{ props.children.clone() }</ul> }
68}
69
70// nav group
71
72/// Properties for [`NavGroup`]
73#[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// nav item
92
93/// Properties for [`NavItem`]
94#[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    /// OUIA Component id
102    #[prop_or_default]
103    pub ouia_id: Option<String>,
104    /// OUIA Component Type
105    #[prop_or(OUIA_NAV_ITEM.component_type())]
106    pub ouia_type: OuiaComponentType,
107    /// OUIA Component Safe
108    #[prop_or(OuiaSafe::TRUE)]
109    pub ouia_safe: OuiaSafe,
110}
111
112/// A navigation item, which triggers a callback when clicked.
113#[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/// Properties for [`NavItem`]
133#[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/// A navigation item, which is a link.
144#[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// nav expandable
167
168/// Properties for [`NavExpandable`]
169#[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
179/// Expandable navigation group/section.
180pub 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            // if this is the first render, and we are expanded, we want to stay that way.
243            // Unless a user explicitly toggles.
244            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        // if we have a current state, that will always override.
284        self.expanded.unwrap_or_else(|| {
285            // if any child is currently active.
286            let active = !self.active.is_empty();
287
288            ctx.props().expanded || active
289        })
290    }
291}
292
293/// Access a wrapping [`Expandable`] content.
294#[hook]
295pub fn use_expandable() -> Option<Expandable> {
296    use_context::<Expandable>()
297}