patternfly_yew/components/tabs/
simple.rs1use 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#[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 #[prop_or_default]
46 pub detached: bool,
47 #[prop_or_default]
48 pub onselect: Callback<T>,
49
50 pub selected: T,
52
53 #[prop_or_default]
55 pub ouia_id: Option<String>,
56 #[prop_or(OUIA.component_type())]
58 pub ouia_type: OuiaComponentType,
59 #[prop_or(OuiaSafe::TRUE)]
61 pub ouia_safe: OuiaSafe,
62
63 #[prop_or_default]
65 pub scroll_button_ouia_id: Option<String>,
66 #[prop_or(OUIA_BUTTON.component_type())]
68 pub scroll_button_ouia_type: OuiaComponentType,
69 #[prop_or(OuiaSafe::TRUE)]
71 pub scroll_button_ouia_safe: OuiaSafe,
72}
73
74#[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 #[prop_or_default]
235 pub ouia_id: Option<String>,
236 #[prop_or(OUIA_ITEM.component_type())]
238 pub ouia_type: OuiaComponentType,
239 #[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#[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#[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}