use dioxus::prelude::*;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum PopoverPosition {
Top,
TopStart,
TopEnd,
#[default]
Bottom,
BottomStart,
BottomEnd,
Left,
LeftStart,
LeftEnd,
Right,
RightStart,
RightEnd,
}
impl PopoverPosition {
#[must_use]
pub fn class_name(&self) -> &'static str {
match self {
Self::Top => "sd-popover-dropdown--top",
Self::TopStart => "sd-popover-dropdown--top-start",
Self::TopEnd => "sd-popover-dropdown--top-end",
Self::Bottom => "sd-popover-dropdown--bottom",
Self::BottomStart => "sd-popover-dropdown--bottom-start",
Self::BottomEnd => "sd-popover-dropdown--bottom-end",
Self::Left => "sd-popover-dropdown--left",
Self::LeftStart => "sd-popover-dropdown--left-start",
Self::LeftEnd => "sd-popover-dropdown--left-end",
Self::Right => "sd-popover-dropdown--right",
Self::RightStart => "sd-popover-dropdown--right-start",
Self::RightEnd => "sd-popover-dropdown--right-end",
}
}
}
#[derive(Clone, Copy, PartialEq)]
pub struct PopoverContext {
pub opened: Signal<bool>,
pub position: Signal<PopoverPosition>,
pub with_arrow: Signal<bool>,
pub close_on_click_outside: bool,
pub glow: Option<bool>,
}
#[component]
pub fn Popover(
opened: Signal<bool>,
#[props(default)]
position: PopoverPosition,
#[props(default = true)]
with_arrow: bool,
#[props(default = true)]
close_on_escape: bool,
#[props(default = true)]
close_on_click_outside: bool,
#[props(default)]
glow: Option<bool>,
#[props(default)]
class: String,
#[props(default)]
id: Option<String>,
children: Element,
) -> Element {
let mut position_sig = use_signal(|| position);
use_effect(use_reactive(&position, move |pos| {
position_sig.set(pos);
}));
let mut arrow_sig = use_signal(|| with_arrow);
use_effect(use_reactive(&with_arrow, move |arr| {
arrow_sig.set(arr);
}));
use_context_provider(|| PopoverContext {
opened,
position: position_sig,
with_arrow: arrow_sig,
close_on_click_outside,
glow,
});
let handle_keydown = move |e: KeyboardEvent| {
if close_on_escape && e.key() == Key::Escape && *opened.read() {
*opened.write() = false;
}
};
rsx! {
div {
class: "sd-popover {class}",
id: id,
tabindex: "-1",
onkeydown: handle_keydown,
{children}
}
}
}
#[component]
pub fn PopoverTrigger(
#[props(default)]
class: String,
children: Element,
) -> Element {
let mut ctx = use_context::<PopoverContext>();
rsx! {
div {
class: "sd-popover-trigger inline-flex cursor-pointer {class}",
"aria-haspopup": "dialog",
"aria-expanded": "{*ctx.opened.read()}",
onclick: move |_| {
let next = !*ctx.opened.read();
*ctx.opened.write() = next;
},
{children}
}
}
}
#[component]
pub fn PopoverDropdown(
#[props(default)]
class: String,
#[props(default)]
glow: Option<bool>,
children: Element,
) -> Element {
let mut ctx = use_context::<PopoverContext>();
let is_open = *ctx.opened.read();
if !is_open {
return rsx! {};
}
let pos_class = ctx.position.read().class_name();
let effective_glow = glow.or(ctx.glow);
#[cfg(feature = "theme")]
let theme_val = crate::theme::use_theme()();
#[cfg(feature = "theme")]
let glow_class = theme_val.resolve_elevation_class("popover", effective_glow);
#[cfg(not(feature = "theme"))]
let glow_class = if effective_glow == Some(true) {
"sd-popover-dropdown--glow"
} else {
""
};
#[cfg(feature = "theme")]
let base_override = theme_val.resolve_component_base("popover").unwrap_or("");
#[cfg(not(feature = "theme"))]
let base_override = "";
rsx! {
if ctx.close_on_click_outside {
div {
class: "sd-popover-backdrop",
onclick: move |e| {
e.stop_propagation();
*ctx.opened.write() = false;
},
}
}
div {
role: "dialog",
class: "sd-popover-dropdown {pos_class} {glow_class} {base_override} {class}",
onclick: move |e| e.stop_propagation(),
if *ctx.with_arrow.read() {
div { class: "sd-popover-arrow" }
}
{children}
}
}
}