Skip to content

Commit 03e9e9c

Browse files
authored
Fix/component click outside (#334)
* fix: Clicking the outer part of the Popover component does not close * fix: Clicking the outer part of the Menu component does not close
1 parent 152cfc8 commit 03e9e9c

File tree

3 files changed

+111
-86
lines changed

3 files changed

+111
-86
lines changed

thaw/src/menu/mod.rs

+41-30
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub use menu_item::*;
55
use crate::ConfigInjection;
66
use leptos::{
77
context::Provider,
8+
either::Either,
89
ev::{self, on},
910
html::Div,
1011
leptos_dom::helpers::TimeoutHandle,
@@ -13,10 +14,7 @@ use leptos::{
1314
};
1415
use std::time::Duration;
1516
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement};
16-
use thaw_utils::{
17-
add_event_listener, call_on_click_outside, class_list, mount_style, ArcOneCallback,
18-
BoxCallback, BoxOneCallback,
19-
};
17+
use thaw_utils::{class_list, mount_style, on_click_outside, ArcOneCallback, BoxOneCallback};
2018

2119
#[slot]
2220
pub struct MenuTrigger<T> {
@@ -47,7 +45,6 @@ where
4745
let config_provider = ConfigInjection::expect_context();
4846

4947
let menu_ref = NodeRef::<Div>::new();
50-
let target_ref = NodeRef::<thaw_utils::Element>::new();
5148
let is_show_menu = RwSignal::new(false);
5249
let show_menu_handle = StoredValue::new(None::<TimeoutHandle>);
5350

@@ -80,27 +77,47 @@ where
8077
});
8178
};
8279

83-
if trigger_type != MenuTriggerType::Hover {
84-
call_on_click_outside(menu_ref, BoxCallback::new(move || is_show_menu.set(false)));
85-
}
86-
87-
Effect::new(move |_| {
88-
let Some(target_el) = target_ref.get() else {
89-
return;
90-
};
91-
let handler = add_event_listener(target_el, ev::click, move |event| {
92-
if trigger_type != MenuTriggerType::Click {
93-
return;
94-
}
95-
event.stop_propagation();
96-
is_show_menu.update(|show| *show = !*show);
97-
});
98-
on_cleanup(move || handler.remove());
99-
});
100-
10180
let MenuTrigger {
10281
children: trigger_children,
10382
} = menu_trigger;
83+
let trigger_children = trigger_children.into_inner()()
84+
.into_inner()
85+
.add_any_attr(tachys_class(("thaw-menu-trigger", true)));
86+
87+
let trigger_children = match trigger_type {
88+
MenuTriggerType::Click => {
89+
let trigger_ref = NodeRef::<thaw_utils::Element>::new();
90+
on_click_outside(
91+
move || {
92+
if !is_show_menu.get_untracked() {
93+
return None;
94+
}
95+
let Some(trigger_el) = trigger_ref.get_untracked() else {
96+
return None;
97+
};
98+
let Some(menu_el) = menu_ref.get_untracked() else {
99+
return None;
100+
};
101+
Some(vec![menu_el.into(), trigger_el])
102+
},
103+
move || is_show_menu.set(false),
104+
);
105+
Either::Left(
106+
trigger_children
107+
.add_any_attr(node_ref(trigger_ref))
108+
.add_any_attr(on(ev::click, move |_| {
109+
is_show_menu.update(|show| {
110+
*show = !*show;
111+
});
112+
})),
113+
)
114+
}
115+
MenuTriggerType::Hover => Either::Right(
116+
trigger_children
117+
.add_any_attr(on(ev::mouseenter, on_mouse_enter))
118+
.add_any_attr(on(ev::mouseleave, on_mouse_leave)),
119+
),
120+
};
104121

105122
let menu_injection = MenuInjection {
106123
has_icon: RwSignal::new(false),
@@ -112,13 +129,7 @@ where
112129

113130
view! {
114131
<Binder>
115-
{trigger_children
116-
.into_inner()()
117-
.into_inner()
118-
.add_any_attr(tachys_class(("thaw-menu-trigger", true)))
119-
.add_any_attr(node_ref(target_ref))
120-
.add_any_attr(on(ev::mouseenter, on_mouse_enter))
121-
.add_any_attr(on(ev::mouseleave, on_mouse_leave))}
132+
{trigger_children}
122133
<Follower slot show=is_show_menu placement=position>
123134
<Provider value=menu_injection>
124135
<CSSTransition

thaw/src/popover/mod.rs

+44-56
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub use types::*;
44

55
use crate::ConfigInjection;
66
use leptos::{
7+
either::Either,
78
ev::{self, on},
89
html,
910
leptos_dom::helpers::TimeoutHandle,
@@ -12,7 +13,7 @@ use leptos::{
1213
};
1314
use std::time::Duration;
1415
use thaw_components::{Binder, CSSTransition, Follower};
15-
use thaw_utils::{add_event_listener, class_list, mount_style, BoxCallback};
16+
use thaw_utils::{class_list, mount_style, on_click_outside, BoxCallback};
1617

1718
#[component]
1819
pub fn Popover<T>(
@@ -41,7 +42,6 @@ where
4142
let config_provider = ConfigInjection::expect_context();
4243

4344
let popover_ref = NodeRef::<html::Div>::new();
44-
let target_ref = NodeRef::<thaw_utils::Element>::new();
4545
let is_show_popover = RwSignal::new(false);
4646
let show_popover_handle = StoredValue::new(None::<TimeoutHandle>);
4747

@@ -93,67 +93,55 @@ where
9393
.ok();
9494
});
9595
};
96-
#[cfg(any(feature = "csr", feature = "hydrate"))]
97-
{
98-
let handle = window_event_listener(ev::click, move |ev| {
99-
use leptos::wasm_bindgen::__rt::IntoJsResult;
100-
if trigger_type != PopoverTriggerType::Click {
101-
return;
102-
}
103-
if !is_show_popover.get_untracked() {
104-
return;
105-
}
106-
let el = ev.target();
107-
let mut el: Option<web_sys::Element> =
108-
el.into_js_result().map_or(None, |el| Some(el.into()));
109-
let body = document().body().unwrap();
110-
while let Some(current_el) = el {
111-
if current_el == *body {
112-
break;
113-
};
114-
let Some(popover_el) = popover_ref.get_untracked() else {
115-
break;
116-
};
117-
if current_el == **popover_el {
118-
return;
119-
}
120-
el = current_el.parent_element();
121-
}
122-
is_show_popover.set(false);
123-
});
124-
on_cleanup(move || handle.remove());
125-
}
126-
127-
Effect::new(move |_| {
128-
let Some(target_el) = target_ref.get() else {
129-
return;
130-
};
131-
let handler = add_event_listener(target_el, ev::click, move |event| {
132-
if trigger_type != PopoverTriggerType::Click {
133-
return;
134-
}
135-
event.stop_propagation();
136-
is_show_popover.update(|show| *show = !*show);
137-
});
138-
on_cleanup(move || handler.remove());
139-
});
14096

14197
let PopoverTrigger {
14298
children: trigger_children,
14399
} = popover_trigger;
100+
let trigger_children = trigger_children.into_inner()()
101+
.into_inner()
102+
.add_any_attr(tachys_class(("thaw-popover-trigger", true)))
103+
.add_any_attr(tachys_class(("thaw-popover-trigger--open", move || {
104+
is_show_popover.get()
105+
})));
106+
107+
let trigger_children = match trigger_type {
108+
PopoverTriggerType::Click => {
109+
let trigger_ref = NodeRef::<thaw_utils::Element>::new();
110+
on_click_outside(
111+
move || {
112+
if !is_show_popover.get_untracked() {
113+
return None;
114+
}
115+
let Some(trigger_el) = trigger_ref.get_untracked() else {
116+
return None;
117+
};
118+
let Some(popover_el) = popover_ref.get_untracked() else {
119+
return None;
120+
};
121+
Some(vec![popover_el.into(), trigger_el])
122+
},
123+
move || is_show_popover.set(false),
124+
);
125+
Either::Left(
126+
trigger_children
127+
.add_any_attr(node_ref(trigger_ref))
128+
.add_any_attr(on(ev::click, move |_| {
129+
is_show_popover.update(|show| {
130+
*show = !*show;
131+
});
132+
})),
133+
)
134+
}
135+
PopoverTriggerType::Hover => Either::Right(
136+
trigger_children
137+
.add_any_attr(on(ev::mouseenter, on_mouse_enter))
138+
.add_any_attr(on(ev::mouseleave, on_mouse_leave)),
139+
),
140+
};
144141

145142
view! {
146143
<Binder>
147-
{trigger_children
148-
.into_inner()()
149-
.into_inner()
150-
.add_any_attr(tachys_class(("thaw-popover-trigger", true)))
151-
.add_any_attr(
152-
tachys_class(("thaw-popover-trigger--open", move || is_show_popover.get())),
153-
)
154-
.add_any_attr(node_ref(target_ref))
155-
.add_any_attr(on(ev::mouseenter, on_mouse_enter))
156-
.add_any_attr(on(ev::mouseleave, on_mouse_leave))}
144+
{trigger_children}
157145
<Follower slot show=is_show_popover placement=position>
158146
<CSSTransition
159147
name="popover-transition"

thaw_utils/src/on_click_outside.rs

+26
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,29 @@ pub fn call_on_click_outside_with_list(refs: Vec<NodeRef<Div>>, on_click: BoxCal
4646
let _ = on_click;
4747
}
4848
}
49+
50+
pub fn on_click_outside<EF, CF>(els: EF, on_click: CF)
51+
where
52+
EF: Fn() -> Option<Vec<web_sys::Element>> + 'static,
53+
CF: Fn() + 'static,
54+
{
55+
#[cfg(any(feature = "csr", feature = "hydrate"))]
56+
{
57+
let handle = window_event_listener(::leptos::ev::click, move |ev| {
58+
let Some(els) = els() else {
59+
return;
60+
};
61+
let composed_path = ev.composed_path();
62+
if els.iter().any(|el| composed_path.includes(&el, 0)) {
63+
return;
64+
}
65+
on_click();
66+
});
67+
on_cleanup(move || handle.remove());
68+
}
69+
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
70+
{
71+
let _ = els;
72+
let _ = on_click;
73+
}
74+
}

0 commit comments

Comments
 (0)