1mod menu_item;
2
3pub use menu_item::*;
4
5use leptos::{
6 context::Provider,
7 either::Either,
8 ev::{self, on},
9 html::Div,
10 leptos_dom::helpers::TimeoutHandle,
11 prelude::*,
12 tachys::html::{class::class as tachys_class, node_ref::node_ref},
13};
14use std::time::Duration;
15use thaw_components::{Follower, FollowerPlacement};
16use thaw_utils::{class_list, mount_style, on_click_outside, ArcOneCallback, BoxOneCallback};
17
18#[slot]
19pub struct MenuTrigger<T> {
20 children: TypedChildren<T>,
21}
22
23#[component]
24pub fn Menu<T>(
25 #[prop(optional, into)] class: MaybeProp<String>,
26 menu_trigger: MenuTrigger<T>,
28 #[prop(optional)]
30 trigger_type: MenuTriggerType,
31 #[prop(optional)]
33 position: MenuPosition,
34 #[prop(into)]
36 on_select: BoxOneCallback<String>,
37 #[prop(optional, into)] appearance: MaybeProp<MenuAppearance>,
38 children: Children,
39) -> impl IntoView
40where
41 T: AddAnyAttr + IntoView + Send + 'static,
42{
43 mount_style("menu", include_str!("./menu.css"));
44
45 let menu_ref = NodeRef::<Div>::new();
46 let is_show_menu = RwSignal::new(false);
47 let show_menu_handle = StoredValue::new(None::<TimeoutHandle>);
48
49 let on_mouse_enter = move |_| {
50 if trigger_type != MenuTriggerType::Hover {
51 return;
52 }
53 show_menu_handle.update_value(|handle| {
54 if let Some(handle) = handle.take() {
55 handle.clear();
56 }
57 });
58 is_show_menu.set(true);
59 };
60 let on_mouse_leave = move |_| {
61 if trigger_type != MenuTriggerType::Hover {
62 return;
63 }
64 show_menu_handle.update_value(|handle| {
65 if let Some(handle) = handle.take() {
66 handle.clear();
67 }
68 *handle = set_timeout_with_handle(
69 move || {
70 is_show_menu.set(false);
71 },
72 Duration::from_millis(100),
73 )
74 .ok();
75 });
76 };
77
78 let MenuTrigger {
79 children: trigger_children,
80 } = menu_trigger;
81 let trigger_children = trigger_children.into_inner()()
82 .into_inner()
83 .add_any_attr(tachys_class(("thaw-menu-trigger", true)));
84
85 let trigger_children = match trigger_type {
86 MenuTriggerType::Click => {
87 let trigger_ref = NodeRef::<thaw_utils::Element>::new();
88 on_click_outside(
89 move || {
90 if !is_show_menu.get_untracked() {
91 return None;
92 }
93 let Some(trigger_el) = trigger_ref.get_untracked() else {
94 return None;
95 };
96 let Some(menu_el) = menu_ref.get_untracked() else {
97 return None;
98 };
99 Some(vec![menu_el.into(), trigger_el])
100 },
101 move || is_show_menu.set(false),
102 );
103 Either::Left(
104 trigger_children
105 .add_any_attr(node_ref(trigger_ref))
106 .add_any_attr(on(ev::click, move |_| {
107 is_show_menu.update(|show| {
108 *show = !*show;
109 });
110 })),
111 )
112 }
113 MenuTriggerType::Hover => Either::Right(
114 trigger_children
115 .add_any_attr(on(ev::mouseenter, on_mouse_enter))
116 .add_any_attr(on(ev::mouseleave, on_mouse_leave)),
117 ),
118 };
119
120 let menu_injection = MenuInjection {
121 has_icon: RwSignal::new(false),
122 on_select: ArcOneCallback::new(move |value| {
123 is_show_menu.set(false);
124 on_select(value);
125 }),
126 };
127
128 view! {
129 <crate::_binder::Binder>
130 {trigger_children} <Follower slot show=is_show_menu placement=position auto_height=true>
131 <div
132 class=class_list![
133 "thaw-menu",
134 move || appearance.get().map(|a| format!("thaw-menu--{}", a.as_str())),
135 class
136 ]
137 node_ref=menu_ref
138 on:mouseenter=on_mouse_enter
139 on:mouseleave=on_mouse_leave
140 >
141 <Provider value=menu_injection>{children()}</Provider>
142 </div>
143 </Follower>
144 </crate::_binder::Binder>
145 }
146}
147
148#[derive(Clone)]
149pub(crate) struct MenuInjection {
150 has_icon: RwSignal<bool>,
151 on_select: ArcOneCallback<String>,
152}
153
154impl MenuInjection {
155 pub fn expect_context() -> Self {
156 expect_context()
157 }
158}
159
160#[derive(Default, PartialEq, Clone)]
161pub enum MenuTriggerType {
162 Hover,
163 #[default]
164 Click,
165}
166
167impl Copy for MenuTriggerType {}
168
169#[derive(Clone)]
170pub enum MenuAppearance {
171 Brand,
172 Inverted,
173}
174
175impl MenuAppearance {
176 pub fn as_str(&self) -> &'static str {
177 match self {
178 MenuAppearance::Brand => "brand",
179 MenuAppearance::Inverted => "inverted",
180 }
181 }
182}
183
184#[derive(Default)]
185pub enum MenuPosition {
186 Top,
187 #[default]
188 Bottom,
189 Left,
190 Right,
191 TopStart,
192 TopEnd,
193 LeftStart,
194 LeftEnd,
195 RightStart,
196 RightEnd,
197 BottomStart,
198 BottomEnd,
199}
200
201impl From<MenuPosition> for FollowerPlacement {
202 fn from(value: MenuPosition) -> Self {
203 match value {
204 MenuPosition::Top => Self::Top,
205 MenuPosition::Bottom => Self::Bottom,
206 MenuPosition::Left => Self::Left,
207 MenuPosition::Right => Self::Right,
208 MenuPosition::TopStart => Self::TopStart,
209 MenuPosition::TopEnd => Self::TopEnd,
210 MenuPosition::LeftStart => Self::LeftStart,
211 MenuPosition::LeftEnd => Self::LeftEnd,
212 MenuPosition::RightStart => Self::RightStart,
213 MenuPosition::RightEnd => Self::RightEnd,
214 MenuPosition::BottomStart => Self::BottomStart,
215 MenuPosition::BottomEnd => Self::BottomEnd,
216 }
217 }
218}