orbital_base_components/overlay/positioning/
mod.rs1mod resolve_external_anchor;
2mod resolve_offset;
3mod styles;
4mod types;
5mod use_anchor_position;
6
7use leptos::tachys::view::any_view::IntoAny;
8use leptos::{html, prelude::*, tachys::html::node_ref::node_ref};
9use orbital_motion::{
10 resolve_presence_motion, MotionElementCallback, OrbitalPresence, PresenceMotion,
11};
12use orbital_style::inject_style;
13use orbital_theme::ThemeInjection;
14use send_wrapper::SendWrapper;
15use std::cell::RefCell;
16use std::rc::Rc;
17
18use crate::overlay::panel_surface::OverlayArrowInjection;
19use crate::overlay::OverlayDismiss;
20use crate::Handler;
21
22pub use resolve_external_anchor::resolve_external_anchor;
23pub use styles::positioning_panel_styles;
24pub use types::{
25 AnchorArrow, AnchorPosition, AnchorWidth, AnchoredPanel, RepositionInjection,
26 UseAnchorPositionOptions,
27};
28pub use use_anchor_position::use_anchor_position;
29
30#[derive(Clone, Copy)]
32pub struct OverlayPlacementInjection {
33 pub placement_label: Signal<String>,
34}
35
36#[component]
38pub fn AnchoredPositioner<T, FT>(
39 #[prop(optional, into)] on_css_transition_after_leave: Option<Handler>,
40 #[prop(default = None)] mount: Option<NodeRef<html::Div>>,
41 #[prop(optional)] panel_ref: Option<NodeRef<html::Div>>,
42 #[prop(default = false)] dismiss_on_anchor_hidden: bool,
43 panel: AnchoredPanel<FT>,
44 children: TypedChildren<T>,
45) -> impl IntoView
46where
47 T: AddAnyAttr + IntoView + Send + 'static,
48 FT: AddAnyAttr + IntoView + Send + 'static,
49{
50 inject_style("orbital-positioning-panel", positioning_panel_styles());
51
52 let theme = expect_context::<ThemeInjection>();
53 let theme_id = StoredValue::new(theme.id());
54
55 let AnchoredPanel {
56 show: panel_show,
57 width: panel_width,
58 placement: panel_placement,
59 children: panel_children,
60 auto_height,
61 arrow,
62 motion: panel_motion,
63 } = panel;
64
65 let arrow_injection = arrow.as_ref().map(|anchor| OverlayArrowInjection {
66 node_ref: anchor.node_ref,
67 });
68 let overlay_dismiss = use_context::<OverlayDismiss>();
70
71 let AnchorPosition {
72 target_ref,
73 content_ref,
74 panel_ref,
75 placement,
76 sync_position,
77 ensure_listener,
78 remove_listener,
79 } = use_anchor_position(
80 panel_width,
81 Signal::from(panel_placement),
82 auto_height,
83 arrow,
84 None,
85 panel_ref,
86 UseAnchorPositionOptions {
87 mount_ref: mount.clone(),
88 dismiss_on_anchor_hidden,
89 on_dismiss: overlay_dismiss.map(|d| d.close),
90 },
91 );
92
93 let placement_label = Signal::derive(move || placement.get().as_str().to_string());
94 provide_context(OverlayPlacementInjection { placement_label });
95
96 let reposition_injection = RepositionInjection::new({
97 let sync_position = sync_position.clone();
98 move || sync_position()
99 });
100
101 let sync_on_enter = {
102 let sync_position = sync_position.clone();
103 MotionElementCallback::new(move |_: web_sys::HtmlElement| sync_position())
104 };
105 let _on_before_enter = sync_on_enter;
106 let _on_after_enter = sync_on_enter;
107
108 #[cfg(not(feature = "ssr"))]
109 {
110 let sync_position = sync_position.clone();
111 Effect::new(move |_| {
112 if target_ref.try_get().flatten().is_none() || content_ref.try_get().flatten().is_none()
113 {
114 return;
115 }
116 if panel_show.get() {
117 sync_position();
118 remove_listener();
119 ensure_listener();
120 } else {
121 remove_listener();
122 }
123 });
124 }
125 #[cfg(feature = "ssr")]
126 let _ = (&ensure_listener, &remove_listener);
127
128 let on_after_leave = MotionElementCallback::new(move |_: web_sys::HtmlElement| {
129 if let Some(on_css_transition_after_leave) = &on_css_transition_after_leave {
130 on_css_transition_after_leave.run(());
131 }
132 });
133
134 let motion = resolve_presence_motion(panel_motion, PresenceMotion::fade());
135
136 let render_panel_children = panel_children.into_inner();
137 let panel_builder = SendWrapper::new(Rc::new(RefCell::new(Some(render_panel_children))));
138 let on_before_enter = sync_on_enter;
139 let on_after_enter = sync_on_enter;
140
141 let trigger_view = children.into_inner()()
142 .into_inner()
143 .add_any_attr(node_ref(target_ref));
144
145 let portal_panel = {
148 let panel_builder = panel_builder.clone();
149 let reposition_injection = reposition_injection.clone();
150 let on_before_enter = on_before_enter;
151 let on_after_enter = on_after_enter;
152 let on_after_leave = on_after_leave;
153 let overlay_dismiss = overlay_dismiss;
154 move || {
155 if let Some(dismiss) = overlay_dismiss {
156 provide_context(dismiss);
157 }
158 if let Some(inj) = arrow_injection {
159 provide_context(inj);
160 }
161 provide_context(reposition_injection.clone());
162
163 let Some(build_children) = panel_builder.borrow_mut().take() else {
164 return ().into_any();
165 };
166 let inner = build_children().into_inner();
167
168 let panel_body = view! {
169 <div class="orbital-positioning-content">
170 {inner}
171 </div>
172 }
173 .add_any_attr(node_ref(content_ref));
174
175 let presence = view! {
176 <OrbitalPresence
177 show=panel_show
178 motion=motion
179 on_before_enter=on_before_enter
180 on_after_enter=on_after_enter
181 on_after_leave=on_after_leave
182 >
183 {panel_body}
184 </OrbitalPresence>
185 };
186
187 let wrapped = view! {
188 <div
189 class="orbital-theme-provider orbital-positioning-panel"
190 node_ref=panel_ref
191 data-orbital-placement=move || placement.get().as_str()
192 data-orbital-theme-id=move || theme_id.get_value()
193 >
194 {presence}
195 </div>
196 };
197
198 wrapped.into_any()
199 }
200 };
201
202 if let Some(mount_el) = mount.as_ref().and_then(|r| r.try_get().flatten()) {
203 view! {
204 {trigger_view}
205 <super::portal::Portal immediate=panel_show mount=mount_el.into()>
206 {portal_panel}
207 </super::portal::Portal>
208 }
209 } else if mount.is_some() {
210 view! {
211 {trigger_view}
212 <super::portal::Portal immediate=panel_show mount_ref=mount>
213 {portal_panel}
214 </super::portal::Portal>
215 }
216 } else {
217 view! {
218 {trigger_view}
219 <super::portal::Portal immediate=panel_show>
220 {portal_panel}
221 </super::portal::Portal>
222 }
223 }
224}