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