use crate::util::TestAttr;
#[allow(unused_imports)]
use leptos::prelude::Effect;
use leptos::prelude::{
Children, ClassAttribute, CustomAttribute, ElementChild, Get, GlobalAttributes, IntoAny,
IntoView, Signal, StyleAttribute, component, view,
};
#[component]
pub fn Dropdown(
#[prop(optional, into)]
classes: Signal<String>,
#[prop(optional, into)]
hoverable: Signal<bool>,
#[prop(optional, into)]
button_classes: Signal<String>,
button: Children,
children: Children,
#[prop(optional, into)]
test_attr: Option<TestAttr>,
) -> impl IntoView {
let (is_active, set_is_active) = leptos::prelude::signal(false);
let class = {
let classes = classes.clone();
let hoverable = hoverable.clone();
move || {
let mut parts = vec!["dropdown".to_string()];
let extra = classes.get();
if !extra.trim().is_empty() {
parts.push(extra);
}
if hoverable.get() {
parts.push("is-hoverable".to_string());
}
if is_active.get() {
parts.push("is-active".to_string());
}
parts.join(" ")
}
};
let (data_testid, data_cy) = match &test_attr {
Some(attr) if attr.key == "data-testid" => (Some(attr.value.clone()), None),
Some(attr) if attr.key == "data-cy" => (None, Some(attr.value.clone())),
_ => (None, None),
};
view! {
<div
class=move || class()
attr:data-testid=move || data_testid.clone()
attr:data-cy=move || data_cy.clone()
>
{move || if is_active.get() && !hoverable.get() {
view! {
<div
style="z-index:10;background-color:rgba(0,0,0,0);position:fixed;top:0;bottom:0;left:0;right:0;"
></div>
}.into_any()
} else {
view! { <></> }.into_any()
}}
<div class="dropdown-trigger">
<button
class=move || {
let extra = button_classes.get();
if extra.trim().is_empty() {
"button".to_string()
} else {
format!("button {}", extra)
}
}
type="button"
>
{button()}
</button>
</div>
<div class="dropdown-menu" role="menu" style="position: relative; z-index: 20;">
<div class="dropdown-content">
{children()}
</div>
</div>
</div>
}
}
#[cfg(test)]
mod tests {
use super::*;
use leptos::prelude::RenderHtml;
#[test]
fn dropdown_renders_base_structure() {
let html = view! {
<Dropdown button=Box::new(|| view!{ "Open" }.into_any())>
<a class="dropdown-item">"Item 1"</a>
<a class="dropdown-item">"Item 2"</a>
</Dropdown>
}
.to_html();
assert!(
html.contains(r#"class="dropdown""#),
"expected base 'dropdown' class; got: {}",
html
);
assert!(
html.contains("dropdown-menu") && html.contains("dropdown-content"),
"expected dropdown structure; got: {}",
html
);
assert!(
html.contains("Open"),
"expected button content rendered; got: {}",
html
);
assert!(
html.contains("Item 1") && html.contains("Item 2"),
"expected children rendered; got: {}",
html
);
}
#[test]
fn dropdown_hoverable_adds_class() {
let html = view! {
<Dropdown hoverable=true button=Box::new(|| view!{ "Btn" }.into_any())>
<a class="dropdown-item">"X"</a>
</Dropdown>
}
.to_html();
assert!(
html.contains("is-hoverable"),
"expected is-hoverable class; got: {}",
html
);
}
}
#[cfg(all(test, target_arch = "wasm32"))]
mod wasm_tests {
use super::*;
use crate::util::TestAttr;
use leptos::prelude::*;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
fn trigger() -> Children {
Box::new(|| view! { "Open" }.into_any())
}
#[wasm_bindgen_test]
fn dropdown_renders_test_attr_as_data_testid() {
let html = view! {
<Dropdown
classes="is-right"
hoverable=true
button_classes="is-primary"
button=trigger()
test_attr="dropdown-test"
>
<a class="dropdown-item">"Item"</a>
</Dropdown>
}
.to_html();
assert!(
html.contains(r#"data-testid="dropdown-test""#),
"expected data-testid attribute on Dropdown; got: {}",
html
);
}
#[wasm_bindgen_test]
fn dropdown_no_test_attr_when_not_provided() {
let html = view! {
<Dropdown button=trigger()>
<a class="dropdown-item">"Item"</a>
</Dropdown>
}
.to_html();
assert!(
!html.contains("data-testid") && !html.contains("data-cy"),
"expected no test attribute on Dropdown when not provided; got: {}",
html
);
}
#[wasm_bindgen_test]
fn dropdown_accepts_custom_test_attr_key() {
let html = view! {
<Dropdown
classes="is-right"
hoverable=true
button_classes="is-primary"
button=trigger()
test_attr=TestAttr::new("data-cy", "dropdown-cy")
>
<a class="dropdown-item">"Item"</a>
</Dropdown>
}
.to_html();
assert!(
html.contains(r#"data-cy="dropdown-cy""#),
"expected custom data-cy attribute on Dropdown; got: {}",
html
);
}
}