adui_dioxus/components/
drawer.rs

1use crate::components::overlay::{OverlayKey, OverlayKind, use_overlay};
2use dioxus::prelude::*;
3
4/// Placement for Drawer panel.
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum DrawerPlacement {
7    Left,
8    Right,
9    Top,
10    Bottom,
11}
12
13/// Basic Drawer props.
14#[derive(Props, Clone, PartialEq)]
15pub struct DrawerProps {
16    pub open: bool,
17    #[props(optional)]
18    pub title: Option<String>,
19    #[props(optional)]
20    pub on_close: Option<EventHandler<()>>,
21    #[props(default = true)]
22    pub mask_closable: bool,
23    #[props(default = true)]
24    pub closable: bool,
25    /// Destroy contents when closed.
26    #[props(default)]
27    pub destroy_on_close: bool,
28    /// Drawer side.
29    #[props(default = DrawerPlacement::Right)]
30    pub placement: DrawerPlacement,
31    /// Logical size (width for left/right, height for top/bottom).
32    #[props(optional)]
33    pub size: Option<f32>,
34    #[props(optional)]
35    pub class: Option<String>,
36    #[props(optional)]
37    pub style: Option<String>,
38    pub children: Element,
39}
40
41/// Simple Ant Design flavored Drawer.
42#[component]
43pub fn Drawer(props: DrawerProps) -> Element {
44    let DrawerProps {
45        open,
46        title,
47        on_close,
48        mask_closable,
49        closable,
50        destroy_on_close,
51        placement,
52        size,
53        class,
54        style,
55        children,
56    } = props;
57
58    let overlay = use_overlay();
59    let drawer_key: Signal<Option<OverlayKey>> = use_signal(|| None);
60    let z_index: Signal<i32> = use_signal(|| 1000);
61
62    {
63        let overlay = overlay.clone();
64        let mut key_signal = drawer_key;
65        let mut z_signal = z_index;
66        use_effect(move || {
67            if let Some(handle) = overlay.clone() {
68                let current_key = {
69                    let guard = key_signal.read();
70                    *guard
71                };
72                if open {
73                    if current_key.is_none() {
74                        let (key, meta) = handle.open(OverlayKind::Drawer, true);
75                        z_signal.set(meta.z_index);
76                        key_signal.set(Some(key));
77                    }
78                } else if let Some(key) = current_key {
79                    handle.close(key);
80                    key_signal.set(None);
81                }
82            }
83        });
84    }
85
86    if !open && destroy_on_close {
87        return rsx! {};
88    }
89
90    let current_z = *z_index.read();
91    let logical_size = size.unwrap_or(378.0);
92    let class_attr = class.unwrap_or_else(|| "adui-drawer".to_string());
93    let style_attr = style.unwrap_or_default();
94
95    let close = move || {
96        if let Some(cb) = on_close {
97            cb.call(());
98        }
99    };
100
101    // Compute positioning styles based on placement.
102    let (panel_style, wrapper_align_style) = match placement {
103        DrawerPlacement::Left => (
104            format!("left: 0; top: 0; bottom: 0; width: {logical_size}px;",),
105            "justify-content: flex-start; align-items: stretch;".to_string(),
106        ),
107        DrawerPlacement::Right => (
108            format!("right: 0; top: 0; bottom: 0; width: {logical_size}px;",),
109            "justify-content: flex-end; align-items: stretch;".to_string(),
110        ),
111        DrawerPlacement::Top => (
112            format!("top: 0; left: 0; right: 0; height: {logical_size}px;",),
113            "justify-content: flex-start; align-items: stretch;".to_string(),
114        ),
115        DrawerPlacement::Bottom => (
116            format!("bottom: 0; left: 0; right: 0; height: {logical_size}px;",),
117            "justify-content: flex-end; align-items: stretch;".to_string(),
118        ),
119    };
120
121    rsx! {
122        if open {
123            // Mask layer
124            div {
125                class: "adui-drawer-mask",
126                style: "position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: {current_z};",
127                onclick: move |_| {
128                    if mask_closable {
129                        close();
130                    }
131                }
132            }
133            // Drawer panel
134            div {
135                class: "{class_attr}",
136                style: "position: fixed; inset: 0; display: flex; {wrapper_align_style} z-index: {current_z + 1}; {style_attr}",
137                div {
138                    class: "adui-drawer-panel",
139                    style: "position: absolute; {panel_style} background: var(--adui-color-bg-container); border-radius: 0; box-shadow: var(--adui-shadow-secondary); border: 1px solid var(--adui-color-border); display: flex; flex-direction: column;",
140                    // Header
141                    if title.is_some() || closable {
142                        div {
143                            class: "adui-drawer-header",
144                            style: "display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--adui-color-border);",
145                            if let Some(text) = title {
146                                div { class: "adui-drawer-title", "{text}" }
147                            }
148                            if closable {
149                                button {
150                                    class: "adui-drawer-close",
151                                    r#type: "button",
152                                    style: "border: none; background: none; cursor: pointer; font-size: 16px;",
153                                    onclick: move |_| close(),
154                                    "×"
155                                }
156                            }
157                        }
158                    }
159                    // Body
160                    div {
161                        class: "adui-drawer-body",
162                        style: "padding: 16px; flex: 1; overflow: auto;",
163                        {children}
164                    }
165                }
166            }
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn drawer_placement_all_variants() {
177        assert_eq!(DrawerPlacement::Left, DrawerPlacement::Left);
178        assert_eq!(DrawerPlacement::Right, DrawerPlacement::Right);
179        assert_eq!(DrawerPlacement::Top, DrawerPlacement::Top);
180        assert_eq!(DrawerPlacement::Bottom, DrawerPlacement::Bottom);
181        assert_ne!(DrawerPlacement::Left, DrawerPlacement::Right);
182        assert_ne!(DrawerPlacement::Left, DrawerPlacement::Top);
183        assert_ne!(DrawerPlacement::Left, DrawerPlacement::Bottom);
184        assert_ne!(DrawerPlacement::Right, DrawerPlacement::Top);
185        assert_ne!(DrawerPlacement::Right, DrawerPlacement::Bottom);
186        assert_ne!(DrawerPlacement::Top, DrawerPlacement::Bottom);
187    }
188
189    #[test]
190    fn drawer_placement_clone() {
191        let original = DrawerPlacement::Left;
192        let cloned = original;
193        assert_eq!(original, cloned);
194    }
195
196    #[test]
197    fn drawer_placement_debug() {
198        let placement = DrawerPlacement::Right;
199        let debug_str = format!("{:?}", placement);
200        assert!(debug_str.contains("Right"));
201    }
202}