use icondata::{BsDashLg, BsPlusLg};
use leptos::html::*;
use leptos::prelude::*;
use leptos_icons::Icon;
use web_sys::CustomEvent;
use crate::utils::forms::fire_custom_bubbled_and_cancelable_event;
#[derive(Clone)]
pub struct PanelInfo {
pub title: ViewFn,
pub is_open: RwSignal<bool>,
pub children: ViewFn,
}
impl Default for PanelInfo {
fn default() -> Self {
Self {
title: ViewFn::from(|| view! {}),
is_open: RwSignal::new(false),
children: ViewFn::from(|| view! {}),
}
}
}
#[allow(dead_code)]
impl PanelInfo {
pub fn builder(title: ViewFn, children: ViewFn) -> PanelInfo {
PanelInfo {
title,
children,
..Default::default()
}
}
pub fn title(mut self, title: ViewFn) -> Self {
self.title = title;
self
}
pub fn is_open(mut self, is_open: RwSignal<bool>) -> Self {
self.is_open = is_open;
self
}
pub fn children(mut self, children: ViewFn) -> Self {
self.children = children;
self
}
pub fn build(self) -> PanelInfo {
PanelInfo {
title: self.title,
is_open: self.is_open,
children: self.children,
}
}
}
impl std::fmt::Debug for PanelInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PanelInfo")
.field("title", &"<ViewFn>")
.field("is_open", &self.is_open)
.field("children", &"<ViewFn>")
.finish()
}
}
#[component]
pub fn Panel(
title: ViewFn,
#[prop(optional)] children: Option<ChildrenFn>,
#[prop(into)] is_open: RwSignal<bool>,
#[prop(optional)] is_accordion: bool,
#[prop(into, optional)] ext_panel_title_styles: String,
) -> impl IntoView {
let panel_ref = NodeRef::new();
let (children, _set_children) = signal(children);
let toggle_content = move |_| {
if let Some(panel_element) = panel_ref.get() {
fire_custom_bubbled_and_cancelable_event("togglepanel", true, true, &panel_element);
}
if !is_accordion {
is_open.update(|value| *value = !*value);
}
};
view! {
<div node_ref=panel_ref>
<span
on:click=toggle_content
class=move || format!("flex flex-row items-center justify-between gap-4 mb-2 p-2 rounded cursor-pointer ring ring-primary hover:bg-primary hover:text-light-gray {} {}", ext_panel_title_styles, if is_open.get() { "bg-primary text-light-gray" } else { "" })
>
{title.run()}
{
move || {
if children.get().is_some() {
let icon_id = if is_open.get() {
BsDashLg
} else {
BsPlusLg
};
Some(view!{ <Icon icon=icon_id /> })
} else {
None
}
}
}
</span>
<div
class=move || {
if is_open.get() {
"transition-max-height duration-700 ease-in-out overflow-hidden max-h-svh p-2 ml-2"
} else {
"overflow-hidden h-0 transition-max-height duration-700 ease-in-out"
}
}
>
{move || children.get().map(|c| c())}
</div>
</div>
}
}
#[component]
pub fn Collapse(
#[prop(into)] panel_items: RwSignal<Vec<PanelInfo>>,
#[prop(default = false)] is_accordion: bool,
) -> impl IntoView {
let handle_panel_toggle = move |index| {
if is_accordion {
panel_items.update(|panels| {
let mut updated_panels = Vec::new();
for (i, panel) in panels.iter().enumerate() {
if i == index {
panel.is_open.update(|val| *val = !*val);
} else {
panel.is_open.set(false);
}
updated_panels.push(panel.clone());
}
*panels = updated_panels;
});
}
};
view! {
<div class="flex flex-col">
<For
each=move || panel_items.get().into_iter().enumerate()
key=|(index, _)| *index
let:((index, panel_item))
>
{
leptos::logging::log!("panel_item.is_open: {}", panel_item.is_open.get());
view! {
<Panel on:togglepanel=move |ev: CustomEvent| {
leptos::logging::log!("togglepanel event fired");
ev.stop_propagation();
handle_panel_toggle(index)
} title=panel_item.title.clone() is_open=panel_item.is_open is_accordion=is_accordion>
{panel_item.children.run()}
</Panel>
}
}
</For>
</div>
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn panel_info_default_is_closed() {
let owner = Owner::new();
owner.with(|| {
let panel = PanelInfo::default();
assert_eq!(panel.is_open.get(), false);
});
}
#[test]
fn panel_info_builder_sets_open_state() {
let owner = Owner::new();
owner.with(|| {
let is_open = RwSignal::new(true);
let panel = PanelInfo::builder(ViewFn::from(|| view! {}), ViewFn::from(|| view! {}))
.is_open(is_open)
.build();
assert_eq!(panel.is_open.get(), true);
});
}
#[test]
fn panel_info_clone_shares_signal() {
let owner = Owner::new();
owner.with(|| {
let panel = PanelInfo::default();
let cloned = panel.clone();
panel.is_open.set(true);
assert_eq!(cloned.is_open.get(), true);
});
}
#[test]
fn toggle_flips_is_open_when_not_accordion() {
let owner = Owner::new();
owner.with(|| {
let is_open = RwSignal::new(false);
is_open.update(|v| *v = !*v);
assert_eq!(is_open.get(), true);
is_open.update(|v| *v = !*v);
assert_eq!(is_open.get(), false);
});
}
#[test]
fn toggle_does_not_flip_when_accordion() {
let owner = Owner::new();
owner.with(|| {
let is_accordion = true;
let is_open = RwSignal::new(false);
if !is_accordion {
is_open.update(|v| *v = !*v);
}
assert_eq!(is_open.get(), false);
});
}
fn accordion_toggle(panels: &mut Vec<RwSignal<bool>>, index: usize) {
for (i, panel) in panels.iter().enumerate() {
if i == index {
panel.update(|v| *v = !*v);
} else {
panel.set(false);
}
}
}
#[test]
fn accordion_opens_target_and_closes_others() {
let owner = Owner::new();
owner.with(|| {
let mut panels = vec![
RwSignal::new(false),
RwSignal::new(true),
RwSignal::new(false),
];
accordion_toggle(&mut panels, 0);
assert_eq!(panels[0].get(), true);
assert_eq!(panels[1].get(), false);
assert_eq!(panels[2].get(), false);
});
}
#[test]
fn accordion_closes_already_open_panel() {
let owner = Owner::new();
owner.with(|| {
let mut panels = vec![RwSignal::new(true), RwSignal::new(false)];
accordion_toggle(&mut panels, 0);
assert_eq!(panels[0].get(), false);
assert_eq!(panels[1].get(), false);
});
}
#[test]
fn non_accordion_panels_are_independent() {
let owner = Owner::new();
owner.with(|| {
let a = RwSignal::new(false);
let b = RwSignal::new(false);
a.update(|v| *v = !*v);
b.update(|v| *v = !*v);
assert_eq!(a.get(), true);
assert_eq!(b.get(), true);
});
}
}