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 <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 #[prop_or_default]
239 pub ouia_id: Option<String>,
240 #[prop_or(OUIA_ITEM.component_type())]
242 pub ouia_type: OuiaComponentType,
243 #[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#[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#[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}