Skip to main content

patternfly_yew/components/tabs/
simple.rs

1use super::TabContent;
2use crate::ouia;
3use crate::prelude::{AsClasses, ExtendClasses, Icon, Inset, OuiaComponentType, WithBreakpoints};
4use crate::utils::{Ouia, OuiaSafe};
5use std::borrow::Cow;
6use yew::html::IntoPropValue;
7use yew::prelude::*;
8
9const OUIA: Ouia = ouia!("Tabs");
10const OUIA_BUTTON: Ouia = ouia!("TabsButton");
11const OUIA_ITEM: Ouia = ouia!("TabsItem");
12
13#[derive(PartialEq, Eq, Clone)]
14pub struct TabsContext<T>
15where
16    T: PartialEq + Eq + Clone + 'static,
17{
18    pub selected: T,
19}
20
21/// Properties for [`Tabs`]
22#[derive(Clone, Debug, Properties, PartialEq)]
23pub struct TabsProperties<T>
24where
25    T: PartialEq + Eq + Clone + 'static,
26{
27    #[prop_or_default]
28    pub children: ChildrenWithProps<Tab<T>>,
29
30    #[prop_or_default]
31    pub id: String,
32    #[prop_or_default]
33    pub r#box: bool,
34    #[prop_or_default]
35    pub vertical: bool,
36    #[prop_or_default]
37    pub filled: bool,
38
39    #[prop_or_default]
40    pub inset: Option<TabInset>,
41
42    /// Enable "detached" mode
43    ///
44    /// If enabled, the content of tabs will not be rendered.
45    #[prop_or_default]
46    pub detached: bool,
47    #[prop_or_default]
48    pub onselect: Callback<T>,
49
50    /// Set the current active tab, overrides the internal state.
51    pub selected: T,
52
53    /// OUIA Component id
54    #[prop_or_default]
55    pub ouia_id: Option<String>,
56    /// OUIA Component Type
57    #[prop_or(OUIA.component_type())]
58    pub ouia_type: OuiaComponentType,
59    /// OUIA Component Safe
60    #[prop_or(OuiaSafe::TRUE)]
61    pub ouia_safe: OuiaSafe,
62
63    /// OUIA Component id
64    #[prop_or_default]
65    pub scroll_button_ouia_id: Option<String>,
66    /// OUIA Component Type
67    #[prop_or(OUIA_BUTTON.component_type())]
68    pub scroll_button_ouia_type: OuiaComponentType,
69    /// OUIA Component Safe
70    #[prop_or(OuiaSafe::TRUE)]
71    pub scroll_button_ouia_safe: OuiaSafe,
72}
73
74/// Tabs component
75///
76/// > **Tabs** allow users to navigate between views within the same page or context.
77///
78/// See: <https://www.patternfly.org/components/tabs>
79///
80/// ## Properties
81///
82/// Defined by [`TabsProperties`].
83///
84/// ## Example
85///
86/// ```rust
87/// use yew::prelude::*;
88/// use patternfly_yew::prelude::*;
89///
90/// #[function_component(Example)]
91/// fn example() -> Html {
92///   #[derive(Clone, Copy, PartialEq, Eq)]
93///   enum MyIndex {
94///     Foo,
95///     Bar,
96///   }
97///
98///   let selected = use_state_eq(|| MyIndex::Foo);
99///   let onselect = use_callback(selected.clone(), |index, selected| selected.set(index));
100///
101///   html!(
102///     <Tabs<MyIndex> selected={*selected} {onselect}>
103///       <Tab<MyIndex> index={MyIndex::Foo} title="Foo">
104///         {"Foo"}
105///       </Tab<MyIndex>>
106///       <Tab<MyIndex> index={MyIndex::Bar} title="Bar">
107///         {"Bar"}
108///       </Tab<MyIndex>>
109///     </Tabs<MyIndex>>
110///   )
111/// }
112/// ```
113///
114/// For more examples, see the PatternFly Yew Quickstart project.
115#[function_component(Tabs)]
116pub fn tabs<T>(props: &TabsProperties<T>) -> Html
117where
118    T: PartialEq + Eq + Clone + 'static,
119{
120    let ouia_id = use_memo(props.ouia_id.clone(), |id| {
121        id.clone().unwrap_or(OUIA.generated_id())
122    });
123    let mut class = classes!("pf-v6-c-tabs");
124
125    if props.r#box {
126        class.push(classes!("pf-m-box"));
127    }
128
129    if props.vertical {
130        class.push(classes!("pf-m-vertical"));
131    }
132
133    if props.filled {
134        class.push(classes!("pf-m-fill"));
135    }
136
137    class.extend_from(&props.inset);
138
139    let context = TabsContext {
140        selected: props.selected.clone(),
141    };
142
143    let button_ouia_id = use_memo(props.scroll_button_ouia_id.clone(), |id| {
144        id.clone().unwrap_or(OUIA.generated_id())
145    });
146
147    html!(
148        <ContextProvider<TabsContext<T>> {context}>
149            <div
150                {class}
151                id={props.id.clone()}
152                data-ouia-component-id={(*ouia_id).clone()}
153                data-ouia-component-type={props.ouia_type}
154                data-ouia-safe={props.ouia_safe}
155            >
156                <button
157                    class="pf-v6-c-tabs__scroll-button"
158                    disabled=true
159                    aria-hidden="true"
160                    aria-label="Scroll left"
161                    data-ouia-component-type={props.scroll_button_ouia_type}
162                    data-ouia-safe={props.scroll_button_ouia_safe}
163                    data-ouia-component-id={(*button_ouia_id).clone()}
164                >
165                    { Icon::AngleLeft }
166                </button>
167                <ul class="pf-v6-c-tabs__list">
168                    { for props.children.iter().map(|c| {
169                        let onselect = props.onselect.clone();
170                        html!(
171                            <TabHeaderItem<T>
172                                icon={c.props.icon}
173                                index={c.props.index.clone()}
174                                {onselect}
175                            >
176                                { c.props.title.clone() }
177                            </TabHeaderItem<T>>
178                        )
179                    }) }
180                </ul>
181                <button
182                    class="pf-v6-c-tabs__scroll-button"
183                    disabled=true
184                    aria-hidden="true"
185                    aria-label="Scroll right"
186                >
187                    { Icon::AngleRight }
188                </button>
189            </div>
190            if !props.detached {
191                { for props.children.iter() }
192            }
193        </ContextProvider<TabsContext<T>>>
194    )
195}
196
197#[derive(Clone, Debug, PartialEq)]
198pub enum TabInset {
199    Inset(WithBreakpoints<Inset>),
200    Page,
201}
202
203impl AsClasses for TabInset {
204    fn extend_classes(&self, classes: &mut Classes) {
205        match self {
206            Self::Page => classes.push("pf-m-page-insets"),
207            Self::Inset(insets) => {
208                insets.extend_classes(classes);
209            }
210        }
211    }
212}
213
214#[derive(Clone, Debug, Properties, PartialEq)]
215struct TabHeaderItemProperties<T>
216where
217    T: PartialEq + Eq + Clone + 'static,
218{
219    #[prop_or_default]
220    pub children: Html,
221
222    #[prop_or_default]
223    pub icon: Option<Icon>,
224
225    #[prop_or_default]
226    pub onselect: Callback<T>,
227
228    pub index: T,
229
230    #[prop_or_default]
231    pub id: Option<AttrValue>,
232
233    /// OUIA Component id
234    #[prop_or_default]
235    pub ouia_id: Option<String>,
236    /// OUIA Component Type
237    #[prop_or(OUIA_ITEM.component_type())]
238    pub ouia_type: OuiaComponentType,
239    /// OUIA Component Safe
240    #[prop_or(OuiaSafe::TRUE)]
241    pub ouia_safe: OuiaSafe,
242}
243
244#[function_component(TabHeaderItem)]
245fn tab_header_item<T>(props: &TabHeaderItemProperties<T>) -> Html
246where
247    T: PartialEq + Eq + Clone + 'static,
248{
249    let ouia_id = use_memo(props.ouia_id.clone(), |id| {
250        id.clone().unwrap_or(OUIA_ITEM.generated_id())
251    });
252    let context = use_context::<TabsContext<T>>();
253    let current = context
254        .map(|context| context.selected == props.index)
255        .unwrap_or_default();
256
257    let mut class = Classes::from("pf-v6-c-tabs__item");
258
259    if current {
260        class.push("pf-m-current");
261    }
262
263    let onclick = use_callback(
264        (props.index.clone(), props.onselect.clone()),
265        |_, (index, onselect)| {
266            onselect.emit(index.clone());
267        },
268    );
269
270    html!(
271        <li
272            {class}
273            id={props.id.clone()}
274            data-ouia-component-id={(*ouia_id).clone()}
275            data-ouia-component-type={props.ouia_type}
276            data-ouia-safe={props.ouia_safe}
277        >
278            <button class="pf-v6-c-tabs__link" {onclick}>
279                if let Some(icon) = props.icon {
280                    <span class="pf-v6-c-tabs__item-icon" aria_hidden={true.to_string()}>
281                        { icon }
282                    </span>
283                }
284                <span
285                    class="pf-v6-c-tabs__item-text"
286                >
287                    { props.children.clone() }
288                </span>
289            </button>
290        </li>
291    )
292}
293
294#[derive(Clone, PartialEq)]
295pub enum TabTitle {
296    String(Cow<'static, str>),
297    Html(Html),
298}
299
300impl IntoPropValue<TabTitle> for String {
301    fn into_prop_value(self) -> TabTitle {
302        TabTitle::String(self.into())
303    }
304}
305
306impl IntoPropValue<TabTitle> for &'static str {
307    fn into_prop_value(self) -> TabTitle {
308        TabTitle::String(self.into())
309    }
310}
311
312impl IntoPropValue<TabTitle> for Html {
313    fn into_prop_value(self) -> TabTitle {
314        TabTitle::Html(self)
315    }
316}
317
318impl IntoPropValue<Html> for TabTitle {
319    fn into_prop_value(self) -> Html {
320        match self {
321            TabTitle::String(s) => s.into(),
322            TabTitle::Html(html) => html.clone(),
323        }
324    }
325}
326
327/// Properties for [`Tab`]
328#[derive(Properties, PartialEq)]
329pub struct TabProperties<T>
330where
331    T: PartialEq + Eq + Clone + 'static,
332{
333    pub title: TabTitle,
334
335    #[prop_or_default]
336    pub icon: Option<Icon>,
337
338    #[prop_or_default]
339    pub children: Html,
340
341    pub index: T,
342
343    #[prop_or_default]
344    pub id: Option<AttrValue>,
345
346    #[prop_or_default]
347    pub class: Classes,
348
349    #[prop_or_default]
350    pub style: Option<AttrValue>,
351}
352
353/// A tab in a [`Tabs`] component
354#[function_component(Tab)]
355pub fn tab<T>(props: &TabProperties<T>) -> Html
356where
357    T: PartialEq + Eq + Clone + 'static,
358{
359    let context = use_context::<TabsContext<T>>();
360    let current = context
361        .map(|context| context.selected == props.index)
362        .unwrap_or_default();
363
364    html!(
365        <TabContent
366            hidden={!current}
367            id={props.id.clone()}
368            class={props.class.clone()}
369            style={props.style.clone()}
370        >
371            { props.children.clone() }
372        </TabContent>
373    )
374}