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}