use dioxus::prelude::*;
#[derive(Clone, PartialEq, Props)]
pub struct DropdownProps {
pub open: Signal<bool>,
pub toggle: Element,
pub menu: Element,
#[props(default)]
pub class: String,
#[props(default)]
pub toggle_class: String,
#[props(default)]
pub direction: DropDirection,
#[props(default)]
pub align_end: bool,
#[props(default)]
pub split: bool,
#[props(default)]
pub color: Option<crate::types::Color>,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum DropDirection {
#[default]
Down,
Up,
Start,
End,
}
#[component]
pub fn Dropdown(props: DropdownProps) -> Element {
let is_open = *props.open.read();
let mut open_signal = props.open;
let dir_class = match props.direction {
DropDirection::Down => "dropdown",
DropDirection::Up => "dropup",
DropDirection::Start => "dropstart",
DropDirection::End => "dropend",
};
let container_class = if props.class.is_empty() {
dir_class.to_string()
} else {
format!("{dir_class} {}", props.class)
};
let color_name = match &props.color {
Some(c) => format!("{c}"),
None => "secondary".to_string(),
};
let toggle_class = if props.split {
format!("btn btn-{color_name} dropdown-toggle dropdown-toggle-split")
} else if props.toggle_class.is_empty() {
format!("btn btn-{color_name} dropdown-toggle")
} else {
format!("btn dropdown-toggle {}", props.toggle_class)
};
let menu_class = if is_open {
if props.align_end {
"dropdown-menu dropdown-menu-end show"
} else {
"dropdown-menu show"
}
} else if props.align_end {
"dropdown-menu dropdown-menu-end"
} else {
"dropdown-menu"
};
let spacer = match props.direction {
DropDirection::Down => "margin-top: var(--bs-dropdown-spacer, 0.125rem);",
DropDirection::Up => "margin-bottom: var(--bs-dropdown-spacer, 0.125rem);",
DropDirection::Start | DropDirection::End => "",
};
let menu_style = if props.align_end {
format!("{spacer} right: 0; left: auto;")
} else {
spacer.to_string()
};
rsx! {
if is_open {
div {
style: "position: fixed; inset: 0; z-index: 990;",
onclick: move |_| open_signal.set(false),
}
}
div { class: "{container_class}",
style: if is_open { "position: relative; z-index: 991;" } else { "" },
..props.attributes,
if props.split {
button {
class: "btn btn-{color_name}",
r#type: "button",
{props.toggle.clone()}
}
}
button {
class: "{toggle_class}",
r#type: "button",
"aria-expanded": if is_open { "true" } else { "false" },
onclick: move |evt| {
evt.stop_propagation();
open_signal.set(!is_open);
},
if !props.split {
{props.toggle}
}
if props.split {
span { class: "visually-hidden", "Toggle Dropdown" }
}
}
ul { class: "{menu_class}",
style: "{menu_style}",
onclick: move |_| open_signal.set(false),
{props.menu}
}
}
}
}
#[derive(Clone, PartialEq, Props)]
pub struct DropdownMenuProps {
#[props(default)]
pub show: bool,
#[props(default)]
pub align_end: bool,
#[props(default)]
pub class: String,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn DropdownMenu(props: DropdownMenuProps) -> Element {
let mut classes = vec!["dropdown-menu".to_string()];
if props.align_end {
classes.push("dropdown-menu-end".to_string());
}
if props.show {
classes.push("show".to_string());
}
if !props.class.is_empty() {
classes.push(props.class.clone());
}
let full_class = classes.join(" ");
let align_style = if props.align_end {
"right: 0; left: auto;"
} else {
""
};
rsx! {
ul { class: "{full_class}", style: "{align_style}", ..props.attributes, {props.children} }
}
}
#[derive(Clone, PartialEq, Props)]
pub struct DropdownItemProps {
#[props(default)]
pub active: bool,
#[props(default)]
pub disabled: bool,
#[props(default)]
pub href: Option<String>,
#[props(default)]
pub target: Option<String>,
#[props(default)]
pub onclick: Option<EventHandler<MouseEvent>>,
#[props(default)]
pub class: String,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
pub children: Element,
}
fn dropdown_item_class(active: bool, disabled: bool, class: &str) -> String {
let mut classes = vec!["dropdown-item".to_string()];
if active {
classes.push("active".to_string());
}
if disabled {
classes.push("disabled".to_string());
}
if !class.is_empty() {
classes.push(class.to_string());
}
classes.join(" ")
}
#[component]
pub fn DropdownItem(props: DropdownItemProps) -> Element {
let full_class = dropdown_item_class(props.active, props.disabled, &props.class);
if let Some(href) = props.href.clone() {
let target = props.target.clone();
return rsx! {
li {
a {
class: "{full_class}",
href: "{href}",
target: target,
"aria-disabled": if props.disabled { "true" } else { "" },
tabindex: if props.disabled { "-1" } else { "" },
onclick: move |evt| {
if let Some(handler) = &props.onclick {
handler.call(evt);
}
},
..props.attributes,
{props.children}
}
}
};
}
rsx! {
li {
button {
class: "{full_class}",
r#type: "button",
disabled: props.disabled,
onclick: move |evt| {
if let Some(handler) = &props.onclick {
handler.call(evt);
}
},
..props.attributes,
{props.children}
}
}
}
}
#[component]
pub fn DropdownDivider() -> Element {
rsx! {
li { hr { class: "dropdown-divider" } }
}
}
#[derive(Clone, PartialEq, Props)]
pub struct DropdownHeaderProps {
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn DropdownHeader(props: DropdownHeaderProps) -> Element {
rsx! {
li { h6 { class: "dropdown-header", ..props.attributes, {props.children} } }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dropdown_item_class_base() {
assert_eq!(dropdown_item_class(false, false, ""), "dropdown-item");
}
#[test]
fn dropdown_item_class_active_disabled_and_extra() {
assert_eq!(
dropdown_item_class(true, true, "text-danger"),
"dropdown-item active disabled text-danger"
);
}
#[test]
fn dropdown_item_class_active_only() {
assert_eq!(dropdown_item_class(true, false, ""), "dropdown-item active");
}
}