patternfly_yew/components/
expandable_section.rs1use crate::prelude::{AsClasses, ExtendClasses, Icon};
3use yew::prelude::*;
4
5#[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#[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 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#[derive(Clone, Debug, PartialEq, Properties)]
168pub struct ExpandableSectionToggleProperties {
169 #[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}