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>
    }
}