mod menu_item;
pub use menu_item::*;
use leptos::{
context::Provider,
either::Either,
ev::{self, on},
html::Div,
leptos_dom::helpers::TimeoutHandle,
prelude::*,
tachys::html::{class::class as tachys_class, node_ref::node_ref},
};
use std::time::Duration;
use thaw_components::{Follower, FollowerPlacement};
use thaw_utils::{class_list, mount_style, on_click_outside, ArcOneCallback, BoxOneCallback};
#[slot]
pub struct MenuTrigger<T> {
children: TypedChildren<T>,
}
#[component]
pub fn Menu<T>(
#[prop(optional, into)] class: MaybeProp<String>,
menu_trigger: MenuTrigger<T>,
#[prop(optional)]
trigger_type: MenuTriggerType,
#[prop(optional)]
position: MenuPosition,
#[prop(into)]
on_select: BoxOneCallback<String>,
#[prop(optional, into)] appearance: MaybeProp<MenuAppearance>,
children: Children,
) -> impl IntoView
where
T: AddAnyAttr + IntoView + Send + 'static,
{
mount_style("menu", include_str!("./menu.css"));
let menu_ref = NodeRef::<Div>::new();
let is_show_menu = RwSignal::new(false);
let show_menu_handle = StoredValue::new(None::<TimeoutHandle>);
let on_mouse_enter = move |_| {
if trigger_type != MenuTriggerType::Hover {
return;
}
show_menu_handle.update_value(|handle| {
if let Some(handle) = handle.take() {
handle.clear();
}
});
is_show_menu.set(true);
};
let on_mouse_leave = move |_| {
if trigger_type != MenuTriggerType::Hover {
return;
}
show_menu_handle.update_value(|handle| {
if let Some(handle) = handle.take() {
handle.clear();
}
*handle = set_timeout_with_handle(
move || {
is_show_menu.set(false);
},
Duration::from_millis(100),
)
.ok();
});
};
let MenuTrigger {
children: trigger_children,
} = menu_trigger;
let trigger_children = trigger_children.into_inner()()
.into_inner()
.add_any_attr(tachys_class(("thaw-menu-trigger", true)));
let trigger_children = match trigger_type {
MenuTriggerType::Click => {
let trigger_ref = NodeRef::<thaw_utils::Element>::new();
on_click_outside(
move || {
if !is_show_menu.get_untracked() {
return None;
}
let Some(trigger_el) = trigger_ref.get_untracked() else {
return None;
};
let Some(menu_el) = menu_ref.get_untracked() else {
return None;
};
Some(vec![menu_el.into(), trigger_el])
},
move || is_show_menu.set(false),
);
Either::Left(
trigger_children
.add_any_attr(node_ref(trigger_ref))
.add_any_attr(on(ev::click, move |_| {
is_show_menu.update(|show| {
*show = !*show;
});
})),
)
}
MenuTriggerType::Hover => Either::Right(
trigger_children
.add_any_attr(on(ev::mouseenter, on_mouse_enter))
.add_any_attr(on(ev::mouseleave, on_mouse_leave)),
),
};
let menu_injection = MenuInjection {
has_icon: RwSignal::new(false),
on_select: ArcOneCallback::new(move |value| {
is_show_menu.set(false);
on_select(value);
}),
};
view! {
<crate::_binder::Binder>
{trigger_children} <Follower slot show=is_show_menu placement=position auto_height=true>
<div
class=class_list![
"thaw-menu",
move || appearance.get().map(|a| format!("thaw-menu--{}", a.as_str())),
class
]
node_ref=menu_ref
on:mouseenter=on_mouse_enter
on:mouseleave=on_mouse_leave
>
<Provider value=menu_injection>{children()}</Provider>
</div>
</Follower>
</crate::_binder::Binder>
}
}
#[derive(Clone)]
pub(crate) struct MenuInjection {
has_icon: RwSignal<bool>,
on_select: ArcOneCallback<String>,
}
impl MenuInjection {
pub fn expect_context() -> Self {
expect_context()
}
}
#[derive(Default, PartialEq, Clone)]
pub enum MenuTriggerType {
Hover,
#[default]
Click,
}
impl Copy for MenuTriggerType {}
#[derive(Clone)]
pub enum MenuAppearance {
Brand,
Inverted,
}
impl MenuAppearance {
pub fn as_str(&self) -> &'static str {
match self {
MenuAppearance::Brand => "brand",
MenuAppearance::Inverted => "inverted",
}
}
}
#[derive(Default)]
pub enum MenuPosition {
Top,
#[default]
Bottom,
Left,
Right,
TopStart,
TopEnd,
LeftStart,
LeftEnd,
RightStart,
RightEnd,
BottomStart,
BottomEnd,
}
impl From<MenuPosition> for FollowerPlacement {
fn from(value: MenuPosition) -> Self {
match value {
MenuPosition::Top => Self::Top,
MenuPosition::Bottom => Self::Bottom,
MenuPosition::Left => Self::Left,
MenuPosition::Right => Self::Right,
MenuPosition::TopStart => Self::TopStart,
MenuPosition::TopEnd => Self::TopEnd,
MenuPosition::LeftStart => Self::LeftStart,
MenuPosition::LeftEnd => Self::LeftEnd,
MenuPosition::RightStart => Self::RightStart,
MenuPosition::RightEnd => Self::RightEnd,
MenuPosition::BottomStart => Self::BottomStart,
MenuPosition::BottomEnd => Self::BottomEnd,
}
}
}