use crate::icon::{Icon, IconKind};
use dioxus::prelude::*;
#[derive(Clone, Copy, PartialEq, Debug, Default)]
pub enum DropdownPlacement {
#[default]
BottomRight,
BottomLeft,
TopRight,
TopLeft,
}
impl DropdownPlacement {
#[must_use]
pub fn scss_class(&self) -> &'static str {
match self {
DropdownPlacement::BottomRight => "sd-dropdown-menu--bottom-right",
DropdownPlacement::BottomLeft => "sd-dropdown-menu--bottom-left",
DropdownPlacement::TopRight => "sd-dropdown-menu--top-right",
DropdownPlacement::TopLeft => "sd-dropdown-menu--top-left",
}
}
}
#[derive(Clone, Copy, PartialEq)]
pub struct DropdownCtx {
pub is_open: Signal<bool>,
pub close_on_click: bool,
}
#[component]
pub fn Dropdown(
is_open: Signal<bool>,
trigger: Element,
#[props(default)]
id: Option<String>,
#[props(default)]
placement: DropdownPlacement,
#[props(default = true)]
close_on_click: bool,
#[props(default)]
glow: Option<bool>,
#[props(default)]
class: String,
children: Element,
) -> Element {
let mut trigger_mount = use_signal(|| None::<std::rc::Rc<MountedData>>);
let open = *is_open.read();
use_context_provider(|| DropdownCtx { is_open, close_on_click });
#[cfg(feature = "theme")]
let theme_val = crate::theme::use_theme()();
#[cfg(feature = "theme")]
let base_override = theme_val.resolve_component_base("dropdown").unwrap_or("");
#[cfg(feature = "theme")]
let class_modifiers = theme_val.resolve_class_modifiers("dropdown", &class);
#[cfg(feature = "theme")]
let glow_class = theme_val.resolve_elevation_class("dropdown", glow);
#[cfg(not(feature = "theme"))]
let base_override = "";
#[cfg(not(feature = "theme"))]
let class_modifiers = String::new();
#[cfg(not(feature = "theme"))]
let glow_class = if glow == Some(true) {
"sd-dropdown-menu--glow".to_string()
} else {
String::new()
};
let dropdown_id = crate::a11y::use_unique_id("sd-dropdown", id);
let trigger_id = format!("{dropdown_id}-trigger");
let menu_id = format!("{dropdown_id}-menu");
let open_container_class = if open { "sd-dropdown-container--open" } else { "" };
let placement_class = placement.scss_class();
#[cfg(feature = "animation")]
let anim_class = crate::animation::Animations::DROPDOWN_ENTER;
#[cfg(not(feature = "animation"))]
let anim_class = "animate-dropdown-enter";
let focus_trigger = move || {
if let Some(m) = trigger_mount() {
spawn(async move {
let _ = m.set_focus(true).await;
});
}
};
let handle_menu_keydown = move |e: KeyboardEvent| {
match e.key() {
Key::Escape => {
e.prevent_default();
*is_open.write() = false;
focus_trigger();
}
Key::Tab => {
*is_open.write() = false;
}
_ => {}
}
};
rsx! {
div {
class: "sd-dropdown-container {open_container_class} {class_modifiers} {class}",
onfocusout: move |_| {
*is_open.write() = false;
},
div {
id: "{trigger_id}",
onmounted: move |m| trigger_mount.set(Some(m.data())),
onclick: move |_| {
let next = !*is_open.read();
*is_open.write() = next;
},
{trigger}
}
if open {
div {
role: "menu",
id: "{menu_id}",
tabindex: "-1",
"aria-labelledby": "{trigger_id}",
class: "sd-dropdown-menu {base_override} {placement_class} {glow_class} {anim_class}",
onclick: move |e| e.stop_propagation(),
onkeydown: handle_menu_keydown,
{children}
}
}
}
}
}
#[component]
pub fn DropdownItem(
#[props(default)]
icon: Option<IconKind>,
#[props(default)]
danger: bool,
#[props(default)]
disabled: bool,
#[props(default)]
class: String,
onclick: Option<EventHandler<MouseEvent>>,
children: Element,
) -> Element {
let ctx = try_use_context::<DropdownCtx>();
let danger_class = if danger { "sd-dropdown-item--danger" } else { "" };
let disabled_class = if disabled { "sd-dropdown-item--disabled" } else { "" };
#[cfg(feature = "theme")]
let theme_val = crate::theme::use_theme()();
#[cfg(feature = "theme")]
let base_override = theme_val.resolve_component_base("dropdown-item").unwrap_or("");
#[cfg(feature = "theme")]
let class_modifiers = theme_val.resolve_class_modifiers("dropdown-item", &class);
#[cfg(not(feature = "theme"))]
let base_override = "";
#[cfg(not(feature = "theme"))]
let class_modifiers = String::new();
rsx! {
button {
r#type: "button",
role: "menuitem",
tabindex: "-1",
disabled: disabled,
"aria-disabled": if disabled { "true" } else { "false" },
class: "sd-dropdown-item {base_override} {danger_class} {disabled_class} {class_modifiers} {class}",
onmousedown: move |e| e.prevent_default(),
onclick: move |e| {
if !disabled {
if let Some(h) = onclick.as_ref() {
h.call(e);
}
if let Some(mut c) = ctx {
if c.close_on_click {
*c.is_open.write() = false;
}
}
}
},
if let Some(i) = icon {
span {
class: "sd-dropdown-item__icon",
"aria-hidden": "true",
Icon { kind: i, size: 15 }
}
}
span { class: "sd-dropdown-item__text", {children} }
}
}
}
#[component]
pub fn DropdownDivider() -> Element {
rsx! {
div {
role: "separator",
class: "sd-dropdown-divider"
}
}
}