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-v5-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
137            class="pf-v5-c-expandable-section__content" hidden={!expanded}
138        >{ props.children.clone() }</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!(
160            <>
161                {toggle} {content}
162            </>
163        ),
164        true => html!(
165            <>
166                 {content} {toggle}
167            </>
168        ),
169    };
170
171    html!(
172        <div
173            {class}
174            id={props.id.clone()}
175        >
176            { content }
177        </div>
178    )
179}
180
181/// Properties for [`ExpandableSectionToggle`]
182#[derive(Clone, Debug, PartialEq, Properties)]
183pub struct ExpandableSectionToggleProperties {
184    /// Alternate children
185    ///
186    /// Setting any children will disable the automatic toggle text from the properties
187    /// `toggle_text_hidden` and `toggle_text_expanded`.
188    #[prop_or_default]
189    pub children: Children,
190
191    #[prop_or("Show more".into())]
192    pub toggle_text_hidden: AttrValue,
193    #[prop_or("Show less".into())]
194    pub toggle_text_expanded: AttrValue,
195
196    pub expanded: bool,
197
198    #[prop_or(true)]
199    detached: bool,
200
201    #[prop_or_default]
202    pub direction: ExpandableSectionToggleDirection,
203
204    #[prop_or_default]
205    pub ontoggle: Callback<()>,
206
207    #[prop_or_default]
208    pub no_icon: bool,
209}
210
211#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
212pub enum ExpandableSectionToggleDirection {
213    #[default]
214    Down,
215    Up,
216}
217
218impl AsClasses for ExpandableSectionToggleDirection {
219    fn extend_classes(&self, classes: &mut Classes) {
220        match self {
221            Self::Down => {}
222            Self::Up => classes.push(classes!("pf-m-expand-top")),
223        }
224    }
225}
226
227#[function_component(ExpandableSectionToggle)]
228pub fn expandable_section_toggle(props: &ExpandableSectionToggleProperties) -> Html {
229    let mut class = classes!("pf-v5-c-expandable-section");
230
231    if props.expanded {
232        class.extend(classes!("pf-m-expanded"));
233    }
234
235    if props.detached {
236        class.extend(classes!("pf-m-detached"));
237    }
238
239    let onclick = use_callback(props.ontoggle.clone(), |_, ontoggle| {
240        ontoggle.emit(());
241    });
242
243    let mut toggle_icon_class = classes!("pf-v5-c-expandable-section__toggle-icon");
244    toggle_icon_class.extend_from(&props.direction);
245
246    let control = html!(
247        <button
248            type="button"
249            class="pf-v5-c-expandable-section__toggle"
250            aria-expanded={props.expanded.to_string()}
251            {onclick}
252        >
253            if !props.no_icon {
254                <span class={toggle_icon_class}>
255                    { Icon::AngleRight }
256                </span>
257            }
258            <span class="pf-v5-c-expandable-section__toggle-text">
259                if !props.children.is_empty() {
260                    { props.children.clone() }
261                } else {
262                    { if props.expanded { &props.toggle_text_expanded } else { &props.toggle_text_hidden } }
263                }
264            </span>
265        </button>
266    );
267
268    match props.detached {
269        true => html!(
270            <div {class}>{ control }</div>
271        ),
272        false => control,
273    }
274}