pub(crate) mod control;
pub(crate) mod item;
pub(crate) mod panel;
pub use control::*;
pub use item::*;
pub use panel::*;
use dioxus::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AccordionVariant {
#[default]
Default,
Contained,
Separated,
}
impl AccordionVariant {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Default => "sd-accordion--default",
Self::Contained => "sd-accordion--contained",
Self::Separated => "sd-accordion--separated",
}
}
}
#[derive(Clone, PartialEq)]
pub struct AccordionContext {
pub multiple: Signal<bool>,
pub value: Signal<Vec<String>>,
pub variant: AccordionVariant,
pub base_id: String,
}
impl AccordionContext {
#[must_use]
pub fn is_expanded(&self, val: &str) -> bool {
self.value.read().contains(&val.to_string())
}
pub fn toggle(&mut self, val: &str) {
let mut list = self.value.write();
if *self.multiple.read() {
if let Some(pos) = list.iter().position(|x| x == val) {
list.remove(pos);
} else {
list.push(val.to_string());
}
} else if list.contains(&val.to_string()) {
list.clear();
} else {
*list = vec![val.to_string()];
}
}
}
#[derive(Clone, PartialEq)]
pub struct AccordionItemContext {
pub value: String,
pub disabled: bool,
pub is_expanded: bool,
pub control_id: String,
pub panel_id: String,
}
#[component]
pub fn Accordion(
#[props(default)]
multiple: bool,
#[props(default)]
value: Option<Signal<Vec<String>>>,
#[props(default)]
default_value: Option<Vec<String>>,
#[props(default)]
variant: AccordionVariant,
#[props(default)]
id: Option<String>,
#[props(default)]
class: String,
children: Element,
) -> Element {
let base_id = crate::a11y::use_unique_id("sd-accordion", id);
let fallback_signal = use_signal(|| default_value.unwrap_or_default());
let active_value = value.unwrap_or(fallback_signal);
let mut multiple_sig = use_signal(|| multiple);
if *multiple_sig.read() != multiple {
*multiple_sig.write() = multiple;
}
let ctx = AccordionContext {
multiple: multiple_sig,
value: active_value,
variant,
base_id: base_id.clone(),
};
provide_context(ctx);
#[cfg(feature = "theme")]
let theme_val = crate::theme::use_theme()();
#[cfg(feature = "theme")]
let base_override = theme_val.resolve_component_base("accordion").unwrap_or("");
#[cfg(feature = "theme")]
let class_modifiers = theme_val.resolve_class_modifiers("accordion", &class);
#[cfg(not(feature = "theme"))]
let base_override = "";
#[cfg(not(feature = "theme"))]
let class_modifiers = String::new();
let variant_class = variant.as_str();
rsx! {
div {
id: "{base_id}",
class: "sd-accordion {variant_class} {base_override} {class_modifiers} {class}",
{children}
}
}
}
#[component]
pub fn Collapsible(
open: Signal<bool>,
#[props(default)]
class: String,
children: Element,
) -> Element {
let is_open = *open.read();
#[cfg(feature = "theme")]
let theme_val = crate::theme::use_theme()();
#[cfg(feature = "theme")]
let base_override = theme_val
.resolve_component_base("collapsible")
.unwrap_or("");
#[cfg(feature = "theme")]
let class_modifiers = theme_val.resolve_class_modifiers("collapsible", &class);
#[cfg(not(feature = "theme"))]
let base_override = "";
#[cfg(not(feature = "theme"))]
let class_modifiers = String::new();
let state_class = if is_open {
"sd-collapsible--open"
} else {
"sd-collapsible--closed"
};
rsx! {
div {
class: "sd-collapsible {state_class} {base_override} {class_modifiers} {class}",
"aria-hidden": if !is_open { "true" } else { "false" },
div { class: "sd-collapsible__inner",
{children}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_accordion_variant_classes() {
assert_eq!(AccordionVariant::Default.as_str(), "sd-accordion--default");
assert_eq!(
AccordionVariant::Contained.as_str(),
"sd-accordion--contained"
);
assert_eq!(
AccordionVariant::Separated.as_str(),
"sd-accordion--separated"
);
}
#[test]
fn test_single_expansion_toggle() {
let mut dom = VirtualDom::new(|| {
let val = use_signal(|| vec!["item-1".to_string()]);
let mult = use_signal(|| false);
let mut ctx = AccordionContext {
multiple: mult,
value: val,
variant: AccordionVariant::Default,
base_id: "test".into(),
};
assert!(ctx.is_expanded("item-1"));
assert!(!ctx.is_expanded("item-2"));
ctx.toggle("item-2");
assert!(!ctx.is_expanded("item-1"));
assert!(ctx.is_expanded("item-2"));
ctx.toggle("item-2");
assert!(!ctx.is_expanded("item-2"));
rsx! { div {} }
});
dom.rebuild_in_place();
}
#[test]
fn test_multiple_expansion_toggle() {
let mut dom = VirtualDom::new(|| {
let val = use_signal(|| vec!["item-1".to_string()]);
let mult = use_signal(|| true);
let mut ctx = AccordionContext {
multiple: mult,
value: val,
variant: AccordionVariant::Separated,
base_id: "test".into(),
};
assert!(ctx.is_expanded("item-1"));
assert!(!ctx.is_expanded("item-2"));
ctx.toggle("item-2");
assert!(ctx.is_expanded("item-1"));
assert!(ctx.is_expanded("item-2"));
ctx.toggle("item-1");
assert!(!ctx.is_expanded("item-1"));
assert!(ctx.is_expanded("item-2"));
rsx! { div {} }
});
dom.rebuild_in_place();
}
}