1use std::{cell::RefCell, rc::Rc};
2
3use floating_ui_dom::{
4 ComputePositionConfig, MiddlewareData, OwnedElementOrVirtual, Placement, Strategy,
5 VirtualElement, compute_position,
6};
7use web_sys::wasm_bindgen::JsCast;
8use yew::{NodeRef, hook, use_callback, use_effect_with, use_memo, use_mut_ref, use_state_eq};
9
10use crate::{
11 types::{
12 FloatingStyles, ShallowRc, UseFloatingOptions, UseFloatingReturn,
13 WhileElementsMountedCleanupFn,
14 },
15 utils::{get_dpr::get_dpr, round_by_dpr::round_by_dpr},
16};
17
18#[derive(Clone, PartialEq)]
19pub enum VirtualElementOrNodeRef {
20 VirtualElement(Box<dyn VirtualElement<web_sys::Element>>),
21 NodeRef(NodeRef),
22}
23
24impl VirtualElementOrNodeRef {
25 pub fn get(&self) -> Option<OwnedElementOrVirtual> {
26 match self {
27 VirtualElementOrNodeRef::VirtualElement(virtual_element) => {
28 Some(virtual_element.clone().into())
29 }
30 VirtualElementOrNodeRef::NodeRef(node_ref) => node_ref.get().map(|node| {
31 OwnedElementOrVirtual::Element(
32 node.dyn_into::<web_sys::Element>()
33 .expect("Reference element should be an Element."),
34 )
35 }),
36 }
37 }
38}
39
40impl From<Box<dyn VirtualElement<web_sys::Element>>> for VirtualElementOrNodeRef {
41 fn from(value: Box<dyn VirtualElement<web_sys::Element>>) -> Self {
42 VirtualElementOrNodeRef::VirtualElement(value)
43 }
44}
45
46impl From<NodeRef> for VirtualElementOrNodeRef {
47 fn from(value: NodeRef) -> Self {
48 VirtualElementOrNodeRef::NodeRef(value)
49 }
50}
51
52#[hook]
54pub fn use_floating(
55 reference: VirtualElementOrNodeRef,
56 floating: NodeRef,
57 options: UseFloatingOptions,
58) -> UseFloatingReturn {
59 let while_elements_mounted_option = options.while_elements_mounted.map(ShallowRc::from);
60 let open_option = use_memo(options.open, |open| open.unwrap_or(true));
61 let middleware_option = use_memo(options.middleware, |middleware| {
62 middleware.clone().unwrap_or_default()
63 });
64 let placement_option = use_memo(options.placement, |placement| {
65 placement.unwrap_or(Placement::Bottom)
66 });
67 let strategy_option = use_memo(options.strategy, |strategy| {
68 strategy.unwrap_or(Strategy::Absolute)
69 });
70 let transform_option = use_memo(options.transform, |transform| transform.unwrap_or(true));
71
72 let x = use_state_eq(|| 0.0);
73 let y = use_state_eq(|| 0.0);
74 let strategy = use_state_eq(|| *strategy_option);
75 let placement = use_state_eq(|| *placement_option);
76 let middleware_data = use_state_eq(MiddlewareData::default);
77 let is_positioned = use_state_eq(|| false);
78 let floating_styles = use_memo(
79 (
80 floating.clone(),
81 transform_option,
82 x.clone(),
83 y.clone(),
84 strategy.clone(),
85 ),
86 |(floating, transform_option, x, y, strategy)| {
87 let initial_styles = FloatingStyles {
88 position: **strategy,
89 top: "0".to_owned(),
90 left: "0".to_owned(),
91 transform: None,
92 will_change: None,
93 };
94
95 match floating.get() {
96 Some(floating_element) => {
97 let x_val = round_by_dpr(&floating_element, **x);
98 let y_val = round_by_dpr(&floating_element, **y);
99
100 if **transform_option {
101 FloatingStyles {
102 transform: Some(format!("translate({x_val}px, {y_val}px)")),
103 will_change: (get_dpr(&floating_element) >= 1.5)
104 .then_some("transform".to_owned()),
105 ..initial_styles
106 }
107 } else {
108 FloatingStyles {
109 left: format!("{x_val}px"),
110 top: format!("{y_val}px"),
111 ..initial_styles
112 }
113 }
114 }
115 _ => initial_styles,
116 }
117 },
118 );
119
120 let update = use_callback(
121 (
122 reference.clone(),
123 floating.clone(),
124 placement_option.clone(),
125 strategy_option.clone(),
126 middleware_option.clone(),
127 x.clone(),
128 y.clone(),
129 strategy.clone(),
130 placement.clone(),
131 middleware_data.clone(),
132 is_positioned.clone(),
133 ),
134 {
135 let open_option = open_option.clone();
136
137 move |_,
138 (
139 reference,
140 floating,
141 placement_option,
142 strategy_option,
143 middleware_option,
144 x,
145 y,
146 strategy,
147 placement,
148 middleware_data,
149 is_positioned,
150 )| {
151 if let Some(reference_element) = reference.get() {
152 if let Some(floating_element) = floating.get() {
153 let config = ComputePositionConfig {
154 placement: Some(**placement_option),
155 strategy: Some(**strategy_option),
156 middleware: Some((**middleware_option).clone()),
157 };
158
159 let open = *open_option;
160
161 let position = compute_position(
162 (&reference_element).into(),
163 floating_element
164 .dyn_ref()
165 .expect("Floating element should be an Element."),
166 config,
167 );
168 x.set(position.x);
169 y.set(position.y);
170 strategy.set(position.strategy);
171 placement.set(position.placement);
172 middleware_data.set(position.middleware_data);
173 is_positioned.set(open);
178 }
179 }
180 }
181 },
182 );
183
184 let while_elements_mounted_cleanup: Rc<
185 RefCell<Option<ShallowRc<WhileElementsMountedCleanupFn>>>,
186 > = use_mut_ref(|| None);
187
188 let cleanup = use_callback(
189 while_elements_mounted_cleanup.clone(),
190 |_, while_elements_mounted_cleanup| {
191 if let Some(while_elements_mounted_cleanup) = while_elements_mounted_cleanup.take() {
192 while_elements_mounted_cleanup();
193 }
194 },
195 );
196
197 let attach = use_callback(
198 (
199 reference.clone(),
200 floating.clone(),
201 while_elements_mounted_option,
202 while_elements_mounted_cleanup,
203 ),
204 {
205 let update = update.clone();
206 let cleanup = cleanup.clone();
207
208 move |_: (),
209 (
210 reference,
211 floating,
212 while_elements_mounted_option,
213 while_elements_mounted_cleanup,
214 )| {
215 cleanup.emit(());
216
217 if let Some(while_elements_mounted) = while_elements_mounted_option {
218 if let Some(reference_element) = reference.get() {
219 if let Some(floating_element) = floating.get() {
220 while_elements_mounted_cleanup.replace(Some(ShallowRc::from(
221 (**while_elements_mounted)(
222 (&reference_element).into(),
223 floating_element
224 .dyn_ref()
225 .expect("Floating element should be an Element."),
226 Rc::new({
227 let update = update.clone();
228
229 move || {
230 update.emit(());
231 }
232 }),
233 ),
234 )));
235 }
236 }
237 } else {
238 update.emit(());
239 }
240 }
241 },
242 );
243
244 let reset = use_callback(
245 (open_option.clone(), is_positioned.clone()),
246 |_, (open_option, is_positioned)| {
247 if **open_option {
248 is_positioned.set(false);
249 }
250 },
251 );
252
253 use_effect_with(
254 (
255 open_option.clone(),
256 placement_option,
257 strategy_option,
258 middleware_option,
259 update.clone(),
260 ),
261 |(_, _, _, _, update)| {
262 update.emit(());
263 },
264 );
265
266 use_effect_with((reference, floating, attach), |(_, _, attach)| {
267 attach.emit(());
268 });
269
270 use_effect_with((open_option, reset), |(_, reset)| {
271 reset.emit(());
272 });
273
274 use_effect_with((), move |_| {
275 move || {
276 cleanup.emit(());
277 }
278 });
279
280 UseFloatingReturn {
281 x,
282 y,
283 placement,
284 strategy,
285 middleware_data,
286 is_positioned,
287 floating_styles,
288 update,
289 }
290}