shadow-design 0.4.0

Modular, high-performance UI component library for Dioxus 0.7 applications
use dioxus::prelude::*;

use crate::button::{Button, ButtonSize, ButtonVariant};
use crate::icon::{Icon, IconKind};

/// Expandable container component that clamps excessive vertical content
/// behind a customizable max-height threshold, featuring a bottom surface-fade gradient
/// and a toggle control button ("Mostra di più" / "Mostra meno").
#[component]
pub fn Spoiler(
    /// Maximum visible height in pixels when collapsed. Defaults to `120`.
    #[props(default = 120)]
    max_height: u32,
    /// Label for the toggle button when collapsed. Defaults to `"Mostra di più"`.
    #[props(default = "Mostra di più".to_string())]
    show_label: String,
    /// Label for the toggle button when expanded. Defaults to `"Mostra meno"`.
    #[props(default = "Mostra meno".to_string())]
    hide_label: String,
    /// Externally managed reactive signal for expanded state.
    #[props(default)]
    expanded: Option<Signal<bool>>,
    /// Initial expanded state if not externally managed. Defaults to `false`.
    #[props(default = false)]
    default_expanded: bool,
    /// Optional event handler triggered on expansion state change.
    #[props(default)]
    on_toggle: Option<EventHandler<bool>>,
    /// Additional CSS or SCSS classes for the container.
    #[props(default)]
    class: String,
    /// Content displayed inside the spoiler.
    children: Element,
) -> Element {
    let fallback_expanded = use_signal(|| default_expanded);
    let mut is_expanded_sig = expanded.unwrap_or(fallback_expanded);
    let is_expanded = *is_expanded_sig.read();

    let toggle_action = move |_: MouseEvent| {
        let next = !is_expanded;
        is_expanded_sig.set(next);
        if let Some(handler) = on_toggle {
            handler.call(next);
        }
    };

    let content_style = if is_expanded {
        "max-height: none;".to_string()
    } else {
        format!("max-height: {}px;", max_height)
    };

    #[cfg(feature = "theme")]
    let theme_val = crate::theme::use_theme()();
    #[cfg(feature = "theme")]
    let base_override = theme_val.resolve_component_base("spoiler").unwrap_or("");
    #[cfg(feature = "theme")]
    let class_modifiers = theme_val.resolve_class_modifiers("spoiler", &class);

    #[cfg(not(feature = "theme"))]
    let base_override = "";
    #[cfg(not(feature = "theme"))]
    let class_modifiers = String::new();

    let state_class = if is_expanded {
        "sd-spoiler--expanded"
    } else {
        "sd-spoiler--collapsed"
    };

    let label_text = if is_expanded { hide_label } else { show_label };

    rsx! {
        div {
            class: "sd-spoiler {state_class} {base_override} {class_modifiers} {class}",
            "data-expanded": "{is_expanded}",
            div {
                class: "sd-spoiler__content",
                style: "{content_style}",
                {children}
            }
            if !is_expanded {
                div { class: "sd-spoiler__overlay" }
            }
            div { class: "sd-spoiler__actions",
                Button {
                    variant: ButtonVariant::Ghost,
                    size: ButtonSize::Sm,
                    onclick: toggle_action,
                    class: "sd-spoiler__btn",
                    span { "{label_text}" }
                    Icon {
                        kind: if is_expanded { IconKind::ChevronUp } else { IconKind::ChevronDown },
                        size: 15,
                    }
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_spoiler_toggle_logic() {
        let mut dom = VirtualDom::new(|| {
            let mut expanded = use_signal(|| false);
            assert!(!*expanded.read());

            expanded.set(true);
            assert!(*expanded.read());

            rsx! { div {} }
        });
        dom.rebuild_in_place();
    }
}