dioxus_bootstrap_css/offcanvas.rs
1use dioxus::prelude::*;
2
3/// Bootstrap Offcanvas (slide-in sidebar) — signal-driven, no JavaScript.
4///
5/// # Bootstrap HTML → Dioxus
6///
7/// ```html
8/// <!-- Bootstrap HTML (requires JavaScript) -->
9/// <button data-bs-toggle="offcanvas" data-bs-target="#sidebar">Open</button>
10/// <div class="offcanvas offcanvas-start" id="sidebar">
11/// <div class="offcanvas-header"><h5>Menu</h5><button class="btn-close" data-bs-dismiss="offcanvas"></button></div>
12/// <div class="offcanvas-body">Content</div>
13/// </div>
14/// ```
15///
16/// ```rust,no_run
17/// // Dioxus equivalent
18/// let show = use_signal(|| false);
19/// rsx! {
20/// Button { onclick: move |_| show.set(true), "Open Sidebar" }
21/// Offcanvas { show: show, title: "Menu", placement: OffcanvasPlacement::Start,
22/// Nav { vertical: true, pills: true,
23/// NavItem { NavLink { active: true, "Home" } }
24/// NavItem { NavLink { "Settings" } }
25/// }
26/// }
27/// }
28/// ```
29///
30/// # Props
31///
32/// - `show` — `Signal<bool>` controlling visibility
33/// - `title` — header title
34/// - `placement` — `OffcanvasPlacement::Start`, `End`, `Top`, `Bottom`
35/// - `backdrop` — show backdrop overlay (default: true)
36/// - `backdrop_close` — close on backdrop click (default: true)
37/// - `responsive` — responsive variant breakpoint (e.g., "lg")
38#[derive(Clone, PartialEq, Props)]
39pub struct OffcanvasProps {
40 /// Signal controlling visibility.
41 pub show: Signal<bool>,
42 /// Title shown in the offcanvas header.
43 #[props(default)]
44 pub title: String,
45 /// Placement (which side it slides in from).
46 #[props(default)]
47 pub placement: OffcanvasPlacement,
48 /// Close when clicking the backdrop.
49 #[props(default = true)]
50 pub backdrop_close: bool,
51 /// Show backdrop overlay.
52 #[props(default = true)]
53 pub backdrop: bool,
54 /// Show close button.
55 #[props(default = true)]
56 pub show_close: bool,
57 /// Responsive variant — offcanvas only below this breakpoint.
58 /// E.g., "lg" makes it offcanvas below lg, regular content above.
59 #[props(default)]
60 pub responsive: String,
61 /// Additional CSS classes.
62 #[props(default)]
63 pub class: String,
64 /// Child elements (offcanvas body content).
65 pub children: Element,
66}
67
68/// Offcanvas slide-in direction.
69#[derive(Clone, Copy, Debug, Default, PartialEq)]
70pub enum OffcanvasPlacement {
71 #[default]
72 Start,
73 End,
74 Top,
75 Bottom,
76}
77
78impl std::fmt::Display for OffcanvasPlacement {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match self {
81 OffcanvasPlacement::Start => write!(f, "offcanvas-start"),
82 OffcanvasPlacement::End => write!(f, "offcanvas-end"),
83 OffcanvasPlacement::Top => write!(f, "offcanvas-top"),
84 OffcanvasPlacement::Bottom => write!(f, "offcanvas-bottom"),
85 }
86 }
87}
88
89#[component]
90pub fn Offcanvas(props: OffcanvasProps) -> Element {
91 let is_shown = *props.show.read();
92 let mut show_signal = props.show;
93
94 if !is_shown {
95 return rsx! {};
96 }
97
98 let placement = props.placement;
99 let show = " show";
100
101 let offcanvas_base = if props.responsive.is_empty() {
102 "offcanvas".to_string()
103 } else {
104 format!("offcanvas-{}", props.responsive)
105 };
106
107 let full_class = if props.class.is_empty() {
108 format!("{offcanvas_base} {placement}{show}")
109 } else {
110 format!("{offcanvas_base} {placement}{show} {}", props.class)
111 };
112
113 let backdrop_close = props.backdrop_close;
114
115 rsx! {
116 // Backdrop
117 if props.backdrop {
118 div {
119 class: "offcanvas-backdrop fade show",
120 onclick: move |_| {
121 if backdrop_close {
122 show_signal.set(false);
123 }
124 },
125 }
126 }
127 // Offcanvas panel
128 div {
129 class: "{full_class}",
130 style: "visibility: visible;",
131 tabindex: "-1",
132 "aria-modal": "true",
133 role: "dialog",
134 if !props.title.is_empty() || props.show_close {
135 div { class: "offcanvas-header",
136 if !props.title.is_empty() {
137 h5 { class: "offcanvas-title", "{props.title}" }
138 }
139 if props.show_close {
140 button {
141 class: "btn-close",
142 r#type: "button",
143 "aria-label": "Close",
144 onclick: move |_| show_signal.set(false),
145 }
146 }
147 }
148 }
149 div { class: "offcanvas-body",
150 {props.children}
151 }
152 }
153 }
154}