Skip to main content

patternfly_yew/components/
expandable_section.rs

1//! Expandable section
2use crate::prelude::{AsClasses, ExtendClasses, Icon};
3use yew::prelude::*;
4
5/// Properties for [`ExpandableSection`]
6#[derive(Clone, Debug, PartialEq, Properties)]
7pub struct ExpandableSectionProperties {
8    #[prop_or_default]
9    pub children: Html,
10
11    #[prop_or_default]
12    pub id: Option<AttrValue>,
13
14    #[prop_or("Show more".into())]
15    pub toggle_text_hidden: AttrValue,
16    #[prop_or("Show less".into())]
17    pub toggle_text_expanded: AttrValue,
18
19    #[prop_or_default]
20    pub initially_open: bool,
21    #[prop_or_default]
22    pub expanded: Option<bool>,
23
24    #[prop_or_default]
25    pub ontoggle: Callback<bool>,
26
27    #[prop_or_default]
28    pub indented: bool,
29    #[prop_or_default]
30    pub width_limited: bool,
31
32    #[prop_or_default]
33    pub display_size: ExpandableSectionSize,
34
35    #[prop_or_default]
36    pub variant: ExpandableSectionVariant,
37
38    #[prop_or_default]
39    pub detached: bool,
40}
41
42#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
43pub enum ExpandableSectionSize {
44    #[default]
45    Default,
46    Large,
47}
48
49impl AsClasses for ExpandableSectionSize {
50    fn extend_classes(&self, classes: &mut Classes) {
51        match self {
52            Self::Default => {}
53            Self::Large => {
54                classes.push(classes!("pf-m-display-lg"));
55            }
56        }
57    }
58}
59
60#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
61pub enum ExpandableSectionVariant {
62    #[default]
63    Default,
64    Truncate,
65}
66
67impl AsClasses for ExpandableSectionVariant {
68    fn extend_classes(&self, classes: &mut Classes) {
69        match self {
70            Self::Default => {}
71            Self::Truncate => {
72                classes.push(classes!("pf-m-truncate"));
73            }
74        }
75    }
76}
77
78/// Expandable Section component
79///
80/// > An **expandable section** component is used to support progressive disclosure in a form or page by hiding additional content when you don't want it to be shown by default. An expandable section can contain any type of content such as plain text, form inputs, and charts.
81///
82/// See: <https://www.patternfly.org/components/expandable-section>
83///
84/// ## Properties
85///
86/// Defined by [`ExpandableSectionProperties`]
87///
88/// ### Detached
89///
90/// If the `detached` property is `true`, the component will neither create a
91/// [`ExpandableSectionToggle`] as part of its children, not track the state change through the
92/// toggle.
93///
94/// However, you can manually place the toggle in a different position.
95///
96/// TIP: See the quickstart project for an example.
97///
98/// ## Children
99///
100/// The section will simpl show or hide its children based on the "expanded" state. If the
101/// component is not "detached" then a [`ExpandableSectionToggle`] component will automatically
102/// be part of its children.
103#[function_component(ExpandableSection)]
104pub fn expandable_section(props: &ExpandableSectionProperties) -> Html {
105    let expanded = use_state_eq(|| props.initially_open);
106
107    let mut class = classes!("pf-v6-c-expandable-section");
108
109    class.extend_from(&props.variant);
110    class.extend_from(&props.display_size);
111
112    if props.indented {
113        class.push(classes!("pf-m-indented"));
114    }
115
116    if props.width_limited {
117        class.push(classes!("pf-m-limit-width"));
118    }
119
120    let ontoggle = use_callback(
121        (props.ontoggle.clone(), expanded.clone()),
122        move |(), (ontoggle, expanded)| {
123            let new_state = !**expanded;
124            expanded.set(new_state);
125            ontoggle.emit(new_state);
126        },
127    );
128
129    let expanded = props.expanded.unwrap_or(*expanded);
130
131    if expanded {
132        class.extend(classes!("pf-m-expanded"));
133    }
134
135    let content = html!(
136        <div class="pf-v6-c-expandable-section__content" hidden={!expanded}>
137            { props.children.clone() }
138        </div>
139    );
140
141    let truncating = props.variant == ExpandableSectionVariant::Truncate;
142    let toggle = match props.detached {
143        true => html!(),
144        false => html!(
145            <ExpandableSectionToggle
146                {ontoggle}
147                {expanded}
148                toggle_text_hidden={&props.toggle_text_hidden}
149                toggle_text_expanded={&props.toggle_text_expanded}
150                detached=false
151                direction={ExpandableSectionToggleDirection::Down}
152                no_icon={truncating}
153            />
154        ),
155    };
156
157    // when using the truncating variant, the toggle is below the content
158    let content = match truncating {
159        false => html!(<>{ toggle }{ content }</>),
160        true => html!(<>{ content }{ toggle }</>),
161    };
162
163    html!(<div {class} id={props.id.clone()}>{ content }</div>)
164}
165
166/// Properties for [`ExpandableSectionToggle`]
167#[derive(Clone, Debug, PartialEq, Properties)]
168pub struct ExpandableSectionToggleProperties {
169    /// Alternate children
170    ///
171    /// Setting any children will disable the automatic toggle text from the properties
172    /// `toggle_text_hidden` and `toggle_text_expanded`.
173    #[prop_or_default]
174    pub children: Children,
175
176    #[prop_or("Show more".into())]
177    pub toggle_text_hidden: AttrValue,
178    #[prop_or("Show less".into())]
179    pub toggle_text_expanded: AttrValue,
180
181    pub expanded: bool,
182
183    #[prop_or(true)]
184    detached: bool,
185
186    #[prop_or_default]
187    pub direction: ExpandableSectionToggleDirection,
188
189    #[prop_or_default]
190    pub ontoggle: Callback<()>,
191
192    #[prop_or_default]
193    pub no_icon: bool,
194}
195
196#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
197pub enum ExpandableSectionToggleDirection {
198    #[default]
199    Down,
200    Up,
201}
202
203impl AsClasses for ExpandableSectionToggleDirection {
204    fn extend_classes(&self, classes: &mut Classes) {
205        match self {
206            Self::Down => {}
207            Self::Up => classes.push(classes!("pf-m-expand-top")),
208        }
209    }
210}
211
212#[function_component(ExpandableSectionToggle)]
213pub fn expandable_section_toggle(props: &ExpandableSectionToggleProperties) -> Html {
214    let mut class = classes!("pf-v6-c-expandable-section");
215
216    if props.expanded {
217        class.extend(classes!("pf-m-expanded"));
218    }
219
220    if props.detached {
221        class.extend(classes!("pf-m-detached"));
222    }
223
224    let onclick = use_callback(props.ontoggle.clone(), |_, ontoggle| {
225        ontoggle.emit(());
226    });
227
228    let mut toggle_icon_class = classes!("pf-v6-c-expandable-section__toggle-icon");
229    toggle_icon_class.extend_from(&props.direction);
230
231    let control = html!(
232        <button
233            type="button"
234            class="pf-v6-c-expandable-section__toggle"
235            aria-expanded={props.expanded.to_string()}
236            {onclick}
237        >
238            if !props.no_icon {
239                <span class={toggle_icon_class}>{ Icon::AngleRight }</span>
240            }
241            <span
242                class="pf-v6-c-expandable-section__toggle-text"
243            >
244                if !props.children.is_empty() {
245                    { props.children.clone() }
246                } else {
247                    { if props.expanded { &props.toggle_text_expanded } else { &props.toggle_text_hidden } }
248                }
249            </span>
250        </button>
251    );
252
253    match props.detached {
254        true => html!(<div {class}>{ control }</div>),
255        false => control,
256    }
257}