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-v5-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-v5-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-v5-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.to_html() }
177                            </TabHeaderItem<T>>
178                        )
179                    }) }
180                </ul>
181                <button
182                    class="pf-v5-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
191            if !props.detached {
192                { for props.children.iter() }
193            }
194        </ContextProvider<TabsContext<T>>>
195    )
196}
197
198#[derive(Clone, Debug, PartialEq)]
199pub enum TabInset {
200    Inset(WithBreakpoints<Inset>),
201    Page,
202}
203
204impl AsClasses for TabInset {
205    fn extend_classes(&self, classes: &mut Classes) {
206        match self {
207            Self::Page => classes.push("pf-m-page-insets"),
208            Self::Inset(insets) => {
209                insets.extend_classes(classes);
210            }
211        }
212    }
213}
214
215#[derive(Clone, Debug, Properties, PartialEq)]
216struct TabHeaderItemProperties<T>
217where
218    T: PartialEq + Eq + Clone + 'static,
219{
220    #[prop_or_default]
221    pub children: Html,
222
223    #[prop_or_default]
224    pub icon: Option<Icon>,
225
226    #[prop_or_default]
227    pub onselect: Callback<T>,
228
229    pub index: T,
230
231    #[prop_or_default]
232    pub id: Option<AttrValue>,
233
234    /// OUIA Component id
235    #[prop_or_default]
236    pub ouia_id: Option<String>,
237    /// OUIA Component Type
238    #[prop_or(OUIA_ITEM.component_type())]
239    pub ouia_type: OuiaComponentType,
240    /// OUIA Component Safe
241    #[prop_or(OuiaSafe::TRUE)]
242    pub ouia_safe: OuiaSafe,
243}
244
245#[function_component(TabHeaderItem)]
246fn tab_header_item<T>(props: &TabHeaderItemProperties<T>) -> Html
247where
248    T: PartialEq + Eq + Clone + 'static,
249{
250    let ouia_id = use_memo(props.ouia_id.clone(), |id| {
251        id.clone().unwrap_or(OUIA_ITEM.generated_id())
252    });
253    let context = use_context::<TabsContext<T>>();
254    let current = context
255        .map(|context| context.selected == props.index)
256        .unwrap_or_default();
257
258    let mut class = Classes::from("pf-v5-c-tabs__item");
259
260    if current {
261        class.push("pf-m-current");
262    }
263
264    let onclick = use_callback(
265        (props.index.clone(), props.onselect.clone()),
266        |_, (index, onselect)| {
267            onselect.emit(index.clone());
268        },
269    );
270
271    html!(
272        <li
273            {class}
274            id={props.id.clone()}
275            data-ouia-component-id={(*ouia_id).clone()}
276            data-ouia-component-type={props.ouia_type}
277            data-ouia-safe={props.ouia_safe}
278        >
279            <button class="pf-v5-c-tabs__link" {onclick}>
280                if let Some(icon) = props.icon {
281                    <span class="pf-v5-c-tabs__item-icon" aria_hidden={true.to_string()}> { icon } </span>
282                }
283                <span class="pf-v5-c-tabs__item-text">
284                    { props.children.clone() }
285                </span>
286            </button>
287        </li>
288    )
289}
290
291#[derive(Clone, PartialEq)]
292pub enum TabTitle {
293    String(Cow<'static, str>),
294    Html(Html),
295}
296
297impl IntoPropValue<TabTitle> for String {
298    fn into_prop_value(self) -> TabTitle {
299        TabTitle::String(self.into())
300    }
301}
302
303impl IntoPropValue<TabTitle> for &'static str {
304    fn into_prop_value(self) -> TabTitle {
305        TabTitle::String(self.into())
306    }
307}
308
309impl IntoPropValue<TabTitle> for Html {
310    fn into_prop_value(self) -> TabTitle {
311        TabTitle::Html(self)
312    }
313}
314
315impl ToHtml for TabTitle {
316    fn to_html(&self) -> Html {
317        match self {
318            TabTitle::String(s) => s.into(),
319            TabTitle::Html(html) => html.clone(),
320        }
321    }
322
323    fn into_html(self) -> Html
324    where
325        Self: Sized,
326    {
327        match self {
328            TabTitle::String(s) => s.into(),
329            TabTitle::Html(html) => html,
330        }
331    }
332}
333
334/// Properties for [`Tab`]
335#[derive(Properties, PartialEq)]
336pub struct TabProperties<T>
337where
338    T: PartialEq + Eq + Clone + 'static,
339{
340    pub title: TabTitle,
341
342    #[prop_or_default]
343    pub icon: Option<Icon>,
344
345    #[prop_or_default]
346    pub children: Html,
347
348    pub index: T,
349
350    #[prop_or_default]
351    pub id: Option<AttrValue>,
352
353    #[prop_or_default]
354    pub class: Classes,
355
356    #[prop_or_default]
357    pub style: Option<AttrValue>,
358}
359
360/// A tab in a [`Tabs`] component
361#[function_component(Tab)]
362pub fn tab<T>(props: &TabProperties<T>) -> Html
363where
364    T: PartialEq + Eq + Clone + 'static,
365{
366    let context = use_context::<TabsContext<T>>();
367    let current = context
368        .map(|context| context.selected == props.index)
369        .unwrap_or_default();
370
371    html!(
372        <TabContent
373            hidden={!current}
374            id={props.id.clone()}
375            class={props.class.clone()}
376            style={props.style.clone()}
377        >
378            { props.children.clone() }
379        </TabContent>
380    )
381}