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