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-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 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#[derive(Clone, Debug, PartialEq, Properties)]
183pub struct ExpandableSectionToggleProperties {
184 #[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}