floating_ui_yew/
use_floating.rs

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/// Computes the `x` and `y` coordinates that will place the floating element next to a reference element.
53#[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                        // The floating element's position may be recomputed while it's closed
174                        // but still mounted (such as when transitioning out). To ensure
175                        // `is_positioned` will be `false` initially on the next open,
176                        // avoid setting it to `true` when `open === false` (must be specified).
177                        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}