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