Skip to main content

dioxus_bootstrap_css/
popover.rs

1use dioxus::prelude::*;
2
3/// Popover placement.
4#[derive(Clone, Copy, Debug, Default, PartialEq)]
5pub enum PopoverPlacement {
6    #[default]
7    Top,
8    Bottom,
9    Start,
10    End,
11}
12
13/// Bootstrap Popover component — CSS-positioned, no JavaScript.
14///
15/// Shows a popover with title and body on click. Click outside to close.
16///
17/// # Bootstrap HTML → Dioxus
18///
19/// ```html
20/// <!-- Bootstrap HTML (requires JavaScript + Popper.js) -->
21/// <button data-bs-toggle="popover" title="Title" data-bs-content="Content">Click</button>
22/// ```
23///
24/// ```rust,no_run
25/// // Dioxus equivalent — no JavaScript needed
26/// rsx! {
27///     Popover {
28///         title: "Popover Title",
29///         body: rsx! { p { "Rich content here." } },
30///         placement: PopoverPlacement::Top,
31///         Button { color: Color::Info, "Click for details" }
32///     }
33/// }
34/// ```
35///
36/// # Props
37///
38/// - `title` — popover header text
39/// - `body` — popover body content (Element)
40/// - `placement` — `PopoverPlacement::Top`, `Bottom`, `Start`, `End`
41#[derive(Clone, PartialEq, Props)]
42pub struct PopoverProps {
43    /// Popover title (header).
44    #[props(default)]
45    pub title: String,
46    /// Popover body content.
47    pub body: Element,
48    /// Popover placement relative to the trigger element.
49    #[props(default)]
50    pub placement: PopoverPlacement,
51    /// Additional CSS classes for the popover.
52    #[props(default)]
53    pub class: String,
54    /// Child element (the trigger).
55    pub children: Element,
56}
57
58#[component]
59pub fn Popover(props: PopoverProps) -> Element {
60    let open = use_signal(|| false);
61    let is_open = *open.read();
62    let mut open_signal = open;
63
64    let placement_class = match props.placement {
65        PopoverPlacement::Top => "bs-popover-top",
66        PopoverPlacement::Bottom => "bs-popover-bottom",
67        PopoverPlacement::Start => "bs-popover-start",
68        PopoverPlacement::End => "bs-popover-end",
69    };
70
71    let popover_class = if props.class.is_empty() {
72        format!("popover fade {placement_class} show")
73    } else {
74        format!("popover fade {placement_class} show {}", props.class)
75    };
76
77    let position_style = match props.placement {
78        PopoverPlacement::Top => {
79            "position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); margin-bottom: 0.5rem;"
80        }
81        PopoverPlacement::Bottom => {
82            "position: absolute; top: 100%; left: 50%; transform: translateX(-50%); margin-top: 0.5rem;"
83        }
84        PopoverPlacement::Start => {
85            "position: absolute; right: 100%; top: 50%; transform: translateY(-50%); margin-right: 0.5rem;"
86        }
87        PopoverPlacement::End => {
88            "position: absolute; left: 100%; top: 50%; transform: translateY(-50%); margin-left: 0.5rem;"
89        }
90    };
91
92    let arrow_placement = match props.placement {
93        PopoverPlacement::Top => {
94            "bottom: calc(-0.5rem - 1px); left: 50%; transform: translateX(-50%);"
95        }
96        PopoverPlacement::Bottom => {
97            "top: calc(-0.5rem - 1px); left: 50%; transform: translateX(-50%);"
98        }
99        PopoverPlacement::Start => {
100            "right: calc(-0.5rem - 1px); top: 50%; transform: translateY(-50%);"
101        }
102        PopoverPlacement::End => {
103            "left: calc(-0.5rem - 1px); top: 50%; transform: translateY(-50%);"
104        }
105    };
106
107    rsx! {
108        // Overlay to close on outside click
109        if is_open {
110            div {
111                style: "position: fixed; inset: 0; z-index: 1069;",
112                onclick: move |_| open_signal.set(false),
113            }
114        }
115        div {
116            style: if is_open { "position: relative; display: inline-block; z-index: 1070;" } else { "position: relative; display: inline-block;" },
117            onclick: move |evt| {
118                evt.stop_propagation();
119                open_signal.set(!is_open);
120            },
121            {props.children}
122            if is_open {
123                div {
124                    class: "{popover_class}",
125                    role: "tooltip",
126                    style: "{position_style} z-index: 1070; min-width: 200px;",
127                    onclick: move |evt| evt.stop_propagation(),
128                    div { class: "popover-arrow", style: "position: absolute; {arrow_placement}" }
129                    if !props.title.is_empty() {
130                        h3 { class: "popover-header", "{props.title}" }
131                    }
132                    div { class: "popover-body", {props.body} }
133                }
134            }
135        }
136    }
137}