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