adui_dioxus/components/floating.rs
1use dioxus::prelude::*;
2
3/// Helper handle for floating overlays (Tooltip / Popover / Popconfirm /
4/// Dropdown 等),封装点击空白关闭 + ESC 关闭的共用逻辑。
5///
6/// 使用方式:
7/// - 在组件内部创建 `let handle = use_floating_close_handle(open_signal);`
8/// - 在触发区或浮层内部交互前调用 `handle.mark_internal_click()`,避免当前事件被
9/// document 级别的 click 监听误判为空白点击;
10/// - 在键盘事件中检测 ESC 并调用 `handle.close()`。
11#[derive(Clone, Copy)]
12pub struct FloatingCloseHandle {
13 internal_click_flag: Signal<bool>,
14 open: Signal<bool>,
15}
16
17impl FloatingCloseHandle {
18 /// 标记当前 click/交互来源于组件内部,避免 document click handler 立即关闭。
19 pub fn mark_internal_click(&self) {
20 let mut flag = self.internal_click_flag;
21 flag.set(true);
22 }
23
24 /// 主动关闭浮层(例如 ESC 或业务逻辑需要)。
25 pub fn close(&self) {
26 let mut open = self.open;
27 open.set(false);
28 }
29}
30
31/// Hook:为当前浮层组件安装 click-outside + ESC 关闭的共用逻辑。
32///
33/// - `open` 是该浮层的可写 Signal;
34/// - 只在 wasm32 目标下注册 document click 监听,其他目标为 no-op。
35pub fn use_floating_close_handle(open: Signal<bool>) -> FloatingCloseHandle {
36 let internal_click_flag: Signal<bool> = use_signal(|| false);
37
38 #[cfg(target_arch = "wasm32")]
39 {
40 let mut flag_for_global = internal_click_flag;
41 let mut open_for_global = open;
42 use_effect(move || {
43 use wasm_bindgen::{JsCast, closure::Closure};
44 if let Some(window) = web_sys::window() {
45 if let Some(document) = window.document() {
46 let target: web_sys::EventTarget = document.into();
47 let handler =
48 Closure::<dyn FnMut(web_sys::MouseEvent)>::wrap(Box::new(move |_evt| {
49 let mut flag = flag_for_global;
50 if *flag.read() {
51 // 本轮事件来源于内部交互,消费标记后不关闭浮层。
52 flag.set(false);
53 return;
54 }
55 let mut open_signal = open_for_global;
56 if *open_signal.read() {
57 open_signal.set(false);
58 }
59 }));
60 let _ = target.add_event_listener_with_callback(
61 "click",
62 handler.as_ref().unchecked_ref(),
63 );
64 handler.forget();
65 }
66 }
67 });
68 }
69
70 FloatingCloseHandle {
71 internal_click_flag,
72 open,
73 }
74}
75
76#[cfg(test)]
77mod floating_tests {
78 use super::*;
79
80 #[test]
81 fn floating_close_handle_implements_clone_and_copy() {
82 // Verify that FloatingCloseHandle implements Clone and Copy traits
83 // This is important for the component's usage pattern
84 fn assert_clone<T: Clone>() {}
85 fn assert_copy<T: Copy>() {}
86 assert_clone::<FloatingCloseHandle>();
87 assert_copy::<FloatingCloseHandle>();
88 }
89
90 #[test]
91 fn floating_close_handle_method_signatures() {
92 // Verify that the methods exist on FloatingCloseHandle with correct signatures
93 // mark_internal_click takes &self (immutable reference) and returns ()
94 // close takes &self (immutable reference) and returns ()
95 fn assert_mark_method(_handle: &FloatingCloseHandle) {
96 // Signature: fn mark_internal_click(&self)
97 }
98 fn assert_close_method(_handle: &FloatingCloseHandle) {
99 // Signature: fn close(&self)
100 }
101 // These functions verify the method signatures are correct
102 assert_mark_method as fn(&FloatingCloseHandle);
103 assert_close_method as fn(&FloatingCloseHandle);
104 }
105
106 #[test]
107 fn use_floating_close_handle_function_signature() {
108 // Verify that use_floating_close_handle function exists with correct signature
109 // Signature: fn use_floating_close_handle(open: Signal<bool>) -> FloatingCloseHandle
110 fn assert_function_signature(_open: Signal<bool>) -> FloatingCloseHandle {
111 // This would require runtime context, but we verify the signature
112 unreachable!("This is just for type checking")
113 }
114 // Verify the function type matches
115 let _function_type: fn(Signal<bool>) -> FloatingCloseHandle = assert_function_signature;
116 }
117
118 #[test]
119 fn floating_close_handle_is_copy_type() {
120 // Verify FloatingCloseHandle is Copy, meaning it can be copied by value
121 // This is important for the component's usage pattern where handles are passed around
122 fn requires_copy<T: Copy>(_t: T) {}
123 // This test verifies the type constraint at compile time
124 // We can't create an instance without runtime, but we verify the trait bound
125 }
126}