yew_bootstrap/component/accordion.rs
1use std::rc::Rc;
2
3use yew::prelude::*;
4
5/// # Properties of [AccordionHeader]
6#[derive(Properties, Clone, PartialEq)]
7struct AccordionHeaderProps {
8 /// The html id of this component
9 #[prop_or_default]
10 heading_id: AttrValue,
11
12 /// The title displayed in the header
13 #[prop_or_default]
14 title: AttrValue,
15
16 /// Classes attached to the button holding the title
17 #[prop_or_default]
18 button_classes: Classes,
19
20 /// The html id of associated collapse for this [AccordionItem]
21 #[prop_or_default]
22 collapse_id: AttrValue,
23
24 /// If the associated accordion collapse is open
25 #[prop_or_default]
26 expanded: bool
27}
28
29/// # Accordion Header
30/// Used with [crate::component::AccordionItem] to create accordion drop downs
31/// This represents the title of the accordion item that is always visible
32///
33/// See [AccordionHeaderProps] for a listing of properties
34///
35/// This component is not meant to be used stand-alone as it's only rendered inside of Accordions
36#[function_component]
37fn AccordionHeader(props: &AccordionHeaderProps) -> Html {
38 html! {
39 <h2 class="accordion-header" id={props.heading_id.clone()}>
40 <button
41 class={props.button_classes.clone()}
42 type="button"
43 data-bs-toggle="collapse"
44 data-bs-target={format!("#{}", props.collapse_id)}
45 aria-expanded={props.expanded.to_string()}
46 aria-controls={props.collapse_id.clone()}
47 >
48 { props.title.clone() }
49 </button>
50 </h2>
51 }
52}
53
54/// # Properties of [AccordionCollapse]
55#[derive(Properties, Clone, PartialEq)]
56struct AccordionCollapseProps {
57 /// Parent [Accordion] html id attribute
58 #[prop_or(AttrValue::from("main-accordion"))]
59 parent_id: AttrValue,
60
61 /// Html id of this component
62 #[prop_or_default]
63 collapse_id: AttrValue,
64
65 /// Html id of associated header for this [AccordionItem]
66 #[prop_or_default]
67 heading_id: AttrValue,
68
69 /// Opening this item will close other items in the [Accordion]
70 #[prop_or_default]
71 stay_open: bool,
72
73 /// Classes attached to the div
74 #[prop_or_default]
75 class: Classes,
76
77 /// Inner components
78 #[prop_or_default]
79 children: Children,
80}
81
82/// # Accordion Collapse
83/// Used with [crate::component::AccordionItem] to create accordion drop downs
84/// This represents the body of the accordion item that can be opened/closed
85///
86/// See [AccordionCollapseProps] for a listing of properties
87///
88/// This component is not meant to be used stand-alone as it's only rendered inside of Accordions
89#[function_component]
90fn AccordionCollapse(props: &AccordionCollapseProps) -> Html {
91 if props.stay_open {
92 return html! {
93 <div id={props.collapse_id.clone()} class={props.class.clone()} aria-labelledby={props.heading_id.clone()}>
94 { for props.children.iter() }
95 </div>
96 }
97 }
98 html! {
99 <div id={props.collapse_id.clone()} class={props.class.clone()} aria-labelledby={props.heading_id.clone()} data-bs-parent={format!("#{}", props.parent_id)}>
100 { for props.children.iter() }
101 </div>
102 }
103}
104
105/// # Properties of [AccordionItem]
106#[derive(Properties, Clone, PartialEq)]
107pub struct AccordionItemProps {
108 /// Text displayed in this items heading
109 #[prop_or_default]
110 pub title: AttrValue,
111
112 /// Item is currently open
113 #[prop_or_default]
114 pub expanded: bool,
115
116 /// Inner components (displayed in the [AccordionCollapse])
117 #[prop_or_default]
118 pub children: Children,
119
120 /// Opening this item doesn't close other items
121 #[prop_or_default]
122 stay_open: bool,
123
124 /// Html id attribute of parent [Accordion]
125 #[prop_or(AttrValue::from("main-accordion"))]
126 parent_id: AttrValue,
127
128 /// Position in the parent [Accordion]
129 #[prop_or_default]
130 item_id: usize,
131}
132
133/// # A singular accordion item, child of [Accordion]
134/// Used as a child of [Accordion] to create an accordion menu.
135///
136/// Child components will be displayed in the body of the accordion item
137#[function_component]
138pub fn AccordionItem(props: &AccordionItemProps) -> Html {
139 let heading_id = format!("{}-heading-{}", props.parent_id, props.item_id);
140 let collapse_id = format!("{}-collapse-{}", props.parent_id, props.item_id);
141
142 let mut button_classes = classes!("accordion-button");
143 let mut collapse_classes = classes!("accordion-collapse", "collapse");
144
145 // TODO: Maybe hook up the `expanded` property to some state depending on `stay_open`
146 //
147 // I think in the bootstrap docs this is really only meant to show one item as expanded after loading the page
148 // However as it currently is, users may be able to set this on multiple items at once
149 // This is probably fine during initial page load since they can be closed individually
150 // But it acts weird if an end-user were to open another item as it would close all of them unless `stay_open` is true
151 //
152 // Additionally if some other part of the page is setup to use state to open an item
153 // This will cause 2 items to be open at once even if the `stay_open` flag is false
154 // There's no real harm putting the closing of accordion items on the user, but it would be nice if there were
155 // some sort of built in way to handle this
156 //
157 // I use ssr in my project so ideally this would also not interfere with rendering server side
158 if !props.expanded {
159 button_classes.push("collapsed");
160 } else {
161 collapse_classes.push("show");
162 }
163
164 html! {
165 <div class="accordion-item">
166 <AccordionHeader
167 title={props.title.clone()}
168 heading_id={heading_id.clone()}
169 button_classes={button_classes}
170 collapse_id={collapse_id.clone()}
171 expanded={props.expanded}
172 />
173 <AccordionCollapse
174 class={collapse_classes}
175 stay_open={props.stay_open}
176 heading_id={heading_id}
177 collapse_id={collapse_id.clone()}
178 parent_id={props.parent_id.clone()}
179 >
180 <div class="accordion-body">
181 { for props.children.iter() }
182 </div>
183 </AccordionCollapse>
184 </div>
185 }
186}
187
188/// # Properties of [Accordion]
189#[derive(Properties, Clone, PartialEq)]
190pub struct AccordionProps {
191 /// Html id of the accordion - should be unique within it's page
192 #[prop_or(AttrValue::from("main-accordion"))]
193 pub id: AttrValue,
194
195 /// Accordion is flush with the container and removes some styling elements
196 #[prop_or_default]
197 pub flush: bool,
198
199 /// Opening an item won't close other items in the accordion
200 #[prop_or_default]
201 pub stay_open: bool,
202
203 // The [AccordionItem] instances controlled by this accordion
204 #[prop_or_default]
205 pub children: ChildrenWithProps<AccordionItem>,
206}
207
208/// # Accordion
209/// [Accordion] is used to group several [crate::component::AccordionItem] instances together.
210///
211/// See [AccordionProps] for a listing of properties.
212///
213/// See [bootstrap docs](https://getbootstrap.com/docs/5.0/components/accordion/) for a full demo of accordions
214///
215/// Basic example of using an Accordion
216///
217/// ```rust
218/// use yew::prelude::*;
219/// use yew_bootstrap::component::{Accordion, AccordionItem};
220/// fn test() -> Html {
221/// html!{
222/// <Accordion>
223/// <AccordionItem title={"Heading 1"}>
224/// <p>{"Some text inside "}<strong>{"THE BODY"}</strong>{" of the accordion item"}</p>
225/// </AccordionItem>
226/// <AccordionItem title={"Heading 2"}>
227/// <h3>{"Some other text under another accordion"}</h3>
228/// <button>{"Button with some functionality"}</button>
229/// </AccordionItem>
230/// </Accordion>
231/// }
232/// }
233/// ```
234///
235///
236/// Example of using an Accordion while mapping a list to AccordionItem children
237///
238/// ```rust
239/// use yew::{prelude::*, virtual_dom::VChild};
240/// use yew_bootstrap::component::{Accordion, AccordionItem};
241/// fn test() -> Html {
242/// let items = vec![("title1", "body1"), ("title2", "body2")];
243/// html! {
244/// <Accordion id="features-and-challenges">
245/// {
246/// items.iter().map(|item| {
247/// html_nested! {
248/// <AccordionItem title={item.0.clone()}>
249/// {item.0.clone()}
250/// </AccordionItem>
251/// }
252/// }).collect::<Vec<VChild<AccordionItem>>>()
253/// }
254/// </Accordion>
255/// }
256/// }
257/// ```
258#[function_component]
259pub fn Accordion(props: &AccordionProps) -> Html {
260 let mut classes = classes!("accordion");
261 if props.flush {
262 classes.push("accordion-flush");
263 }
264
265 html! {
266 <div class={classes} id={props.id.clone()}>
267 {
268 for props.children.iter().enumerate().map(|(index, mut child)| {
269 let child_props = Rc::make_mut(&mut child.props);
270 child_props.item_id = index;
271 child_props.parent_id = props.id.clone();
272 child_props.stay_open = props.stay_open;
273 child
274 })
275 }
276 </div>
277 }
278}