floating_ui_dom/
auto_update.rs

1use std::{cell::RefCell, rc::Rc};
2
3use floating_ui_utils::{
4    ClientRectObject,
5    dom::{OverflowAncestor, get_document_element, get_overflow_ancestors, get_window},
6};
7use web_sys::{
8    AddEventListenerOptions, Element, EventTarget, IntersectionObserver, IntersectionObserverEntry,
9    IntersectionObserverInit, ResizeObserver, ResizeObserverEntry,
10    wasm_bindgen::{JsCast, JsValue, closure::Closure},
11    window,
12};
13
14use crate::{
15    types::{ElementOrVirtual, OwnedElementOrVirtual},
16    utils::{get_bounding_client_rect::get_bounding_client_rect, rects_are_equal::rects_are_equal},
17};
18
19fn request_animation_frame(callback: &Closure<dyn FnMut()>) -> i32 {
20    window()
21        .expect("Window should exist.")
22        .request_animation_frame(callback.as_ref().unchecked_ref())
23        .expect("Request animation frame should be successful.")
24}
25
26fn cancel_animation_frame(handle: i32) {
27    window()
28        .expect("Window should exist.")
29        .cancel_animation_frame(handle)
30        .expect("Cancel animation frame should be successful.")
31}
32
33fn observe_move(element: Element, on_move: Rc<dyn Fn()>) -> Box<dyn Fn()> {
34    let io: Rc<RefCell<Option<IntersectionObserver>>> = Rc::new(RefCell::new(None));
35    let timeout_id: Rc<RefCell<Option<i32>>> = Rc::new(RefCell::new(None));
36
37    let window = get_window(Some(&element));
38    let root = get_document_element(Some((&element).into()));
39
40    type ObserveClosure = Closure<dyn Fn(Vec<IntersectionObserverEntry>)>;
41    let observe_closure: Rc<RefCell<Option<ObserveClosure>>> = Rc::new(RefCell::new(None));
42
43    let cleanup_io = io.clone();
44    let cleanup_timeout_id = timeout_id.clone();
45    let cleanup_window = window.clone();
46    let cleanup_observe_closure = observe_closure.clone();
47    let cleanup = move || {
48        if let Some(timeout_id) = cleanup_timeout_id.take() {
49            cleanup_window.clear_timeout_with_handle(timeout_id);
50        }
51
52        if let Some(io) = cleanup_io.take() {
53            io.disconnect();
54        }
55
56        _ = cleanup_observe_closure.take();
57    };
58    let cleanup_rc = Rc::new(cleanup);
59    type RefreshFn = Box<dyn Fn(bool, f64)>;
60    let refresh_closure: Rc<RefCell<Option<RefreshFn>>> = Rc::new(RefCell::new(None));
61    let refresh_closure_clone = refresh_closure.clone();
62
63    let refresh_cleanup = cleanup_rc.clone();
64    *refresh_closure_clone.borrow_mut() = Some(Box::new(move |skip: bool, threshold: f64| {
65        refresh_cleanup();
66
67        let element_rect_for_root_margin = element.get_bounding_client_rect();
68
69        if !skip {
70            on_move();
71        }
72
73        if element_rect_for_root_margin.width() == 0.0
74            || element_rect_for_root_margin.height() == 0.0
75        {
76            return;
77        }
78
79        let inset_top = element_rect_for_root_margin.top().floor();
80        let inset_right = (root.client_width() as f64
81            - (element_rect_for_root_margin.left() + element_rect_for_root_margin.width()))
82        .floor();
83        let inset_bottom = (root.client_height() as f64
84            - (element_rect_for_root_margin.top() + element_rect_for_root_margin.height()))
85        .floor();
86        let inset_left = element_rect_for_root_margin.left().floor();
87        let root_margin = format!(
88            "{}px {}px {}px {}px",
89            -inset_top, -inset_right, -inset_bottom, -inset_left
90        );
91
92        let is_first_update: Rc<RefCell<bool>> = Rc::new(RefCell::new(true));
93
94        let timeout_refresh = refresh_closure.clone();
95        let timeout_closure: Rc<Closure<dyn Fn()>> = Rc::new(Closure::new(move || {
96            timeout_refresh
97                .borrow()
98                .as_ref()
99                .expect("Refresh closure should exist.")(false, 1e-7)
100        }));
101
102        let observe_timeout_id = timeout_id.clone();
103        let observe_window = window.clone();
104        let observe_refresh = refresh_closure.clone();
105        let local_observe_closure = Closure::new({
106            let element = element.clone();
107
108            move |entries: Vec<IntersectionObserverEntry>| {
109                let ratio = entries[0].intersection_ratio();
110
111                if ratio != threshold {
112                    if !*is_first_update.borrow() {
113                        observe_refresh
114                            .borrow()
115                            .as_ref()
116                            .expect("Refresh closure should exist.")(
117                            false, 1.0
118                        );
119                        return;
120                    }
121
122                    if ratio == 0.0 {
123                        // If the reference is clipped, the ratio is 0. Throttle the refresh to prevent an infinite loop of updates.
124                        observe_timeout_id.replace(Some(
125                            observe_window
126                                .set_timeout_with_callback_and_timeout_and_arguments_0(
127                                    (*timeout_closure).as_ref().unchecked_ref(),
128                                    1000,
129                                )
130                                .expect("Set timeout should be successful."),
131                        ));
132                    } else {
133                        observe_refresh
134                            .borrow()
135                            .as_ref()
136                            .expect("Refresh closure should exist.")(
137                            false, ratio
138                        );
139                    }
140                }
141
142                if ratio == 1.0
143                    && !rects_are_equal(
144                        &element_rect_for_root_margin.clone().into(),
145                        &element.get_bounding_client_rect().into(),
146                    )
147                {
148                    // It's possible that even though the ratio is reported as 1, the
149                    // element is not actually fully within the IntersectionObserver's root
150                    // area anymore. This can happen under performance constraints. This may
151                    // be a bug in the browser's IntersectionObserver implementation. To
152                    // work around this, we compare the element's bounding rect now with
153                    // what it was at the time we created the IntersectionObserver. If they
154                    // are not equal then the element moved, so we refresh.
155                    observe_refresh
156                        .borrow()
157                        .as_ref()
158                        .expect("Refresh closure should exist.")(false, 1.0);
159                }
160
161                is_first_update.replace(false);
162            }
163        });
164
165        let options = IntersectionObserverInit::new();
166        options.set_root_margin(&root_margin);
167        options.set_threshold(&JsValue::from_f64(threshold.clamp(0.0, 1.0)));
168
169        let local_io = IntersectionObserver::new_with_options(
170            local_observe_closure.as_ref().unchecked_ref(),
171            &options,
172        )
173        .expect("Intersection observer should be created.");
174
175        observe_closure.replace(Some(local_observe_closure));
176
177        local_io.observe(&element);
178        io.replace(Some(local_io));
179    }));
180
181    refresh_closure_clone
182        .borrow()
183        .as_ref()
184        .expect("Refresh closure should exist.")(true, 1.0);
185
186    Box::new(move || {
187        cleanup_rc();
188    })
189}
190
191/// Options for [`auto_update`].
192#[derive(Clone, Debug, Default, PartialEq)]
193pub struct AutoUpdateOptions {
194    /// Whether to update the position when an overflow ancestor is scrolled.
195    ///
196    /// Defaults to `true`.
197    pub ancestor_scroll: Option<bool>,
198
199    /// Whether to update the position when an overflow ancestor is resized. This uses the native `resize` event.
200    ///
201    /// Defaults to `true`.
202    pub ancestor_resize: Option<bool>,
203
204    /// Whether to update the position when either the reference or floating elements resized. This uses a `ResizeObserver`.
205    ///
206    /// Defaults to `true`.
207    pub element_resize: Option<bool>,
208
209    /// Whether to update the position when the reference relocated on the screen due to layout shift.
210    ///
211    /// Defaults to `true`.
212    pub layout_shift: Option<bool>,
213
214    /// Whether to update on every animation frame if necessary.
215    /// Only use if you need to update the position in response to an animation using transforms.
216    ///
217    /// Defaults to `false`.
218    pub animation_frame: Option<bool>,
219}
220
221impl AutoUpdateOptions {
222    /// Set `ancestor_scroll` option.
223    pub fn ancestor_scroll(mut self, value: bool) -> Self {
224        self.ancestor_scroll = Some(value);
225        self
226    }
227
228    /// Set `ancestor_resize` option.
229    pub fn ancestor_resize(mut self, value: bool) -> Self {
230        self.ancestor_resize = Some(value);
231        self
232    }
233
234    /// Set `element_resize` option.
235    pub fn element_resize(mut self, value: bool) -> Self {
236        self.element_resize = Some(value);
237        self
238    }
239
240    /// Set `layout_shift` option.
241    pub fn layout_shift(mut self, value: bool) -> Self {
242        self.layout_shift = Some(value);
243        self
244    }
245
246    /// Set `animation_frame` option.
247    pub fn animation_frame(mut self, value: bool) -> Self {
248        self.animation_frame = Some(value);
249        self
250    }
251}
252
253/// Automatically updates the position of the floating element when necessary.
254/// Should only be called when the floating element is mounted on the DOM or visible on the screen.
255pub fn auto_update(
256    reference: ElementOrVirtual,
257    floating: &Element,
258    update: Rc<dyn Fn()>,
259    options: AutoUpdateOptions,
260) -> Box<dyn Fn()> {
261    let ancestor_scoll = options.ancestor_scroll.unwrap_or(true);
262    let ancestor_resize = options.ancestor_resize.unwrap_or(true);
263    let element_resize = options.element_resize.unwrap_or(true);
264    let layout_shift = options.layout_shift.unwrap_or(true);
265    let animation_frame = options.animation_frame.unwrap_or(false);
266
267    let reference_element = reference.clone().resolve();
268
269    let owned_reference = match reference.clone() {
270        ElementOrVirtual::Element(e) => OwnedElementOrVirtual::Element(e.clone()),
271        ElementOrVirtual::VirtualElement(ve) => OwnedElementOrVirtual::VirtualElement(ve.clone()),
272    };
273
274    let ancestors = if ancestor_scoll || ancestor_resize {
275        let mut ancestors = vec![];
276
277        if let Some(reference) = reference_element.as_ref() {
278            ancestors = get_overflow_ancestors(reference, ancestors, true);
279        }
280
281        ancestors.append(&mut get_overflow_ancestors(floating, vec![], true));
282
283        ancestors
284    } else {
285        vec![]
286    };
287
288    let update_closure: Closure<dyn Fn()> = Closure::new({
289        let update = update.clone();
290
291        move || {
292            update();
293        }
294    });
295
296    for ancestor in &ancestors {
297        let event_target: &EventTarget = match ancestor {
298            OverflowAncestor::Element(element) => element,
299            OverflowAncestor::Window(window) => window,
300        };
301
302        if ancestor_scoll {
303            let options = AddEventListenerOptions::new();
304            options.set_passive(true);
305
306            event_target
307                .add_event_listener_with_callback_and_add_event_listener_options(
308                    "scroll",
309                    update_closure.as_ref().unchecked_ref(),
310                    &options,
311                )
312                .expect("Scroll event listener should be added.");
313        }
314
315        if ancestor_resize {
316            event_target
317                .add_event_listener_with_callback("resize", update_closure.as_ref().unchecked_ref())
318                .expect("Resize event listener should be added.");
319        }
320    }
321
322    let cleanup_observe_move = reference_element.as_ref().and_then(|reference_element| {
323        layout_shift.then(|| observe_move(reference_element.clone(), update.clone()))
324    });
325
326    let reobserve_frame: Rc<RefCell<Option<i32>>> = Rc::new(RefCell::new(None));
327    let resize_observer: Rc<RefCell<Option<ResizeObserver>>> = Rc::new(RefCell::new(None));
328
329    if element_resize {
330        let reobserve_floating = floating.clone();
331        let reobserve_closure: Rc<Closure<dyn FnMut()>> = Rc::new(Closure::new({
332            let resize_observer = resize_observer.clone();
333
334            move || {
335                resize_observer
336                    .borrow()
337                    .as_ref()
338                    .expect("Resize observer should exist.")
339                    .observe(&reobserve_floating);
340            }
341        }));
342
343        let resize_reference_element = reference_element.clone();
344        let resize_closure: Closure<dyn Fn(Vec<ResizeObserverEntry>)> = Closure::new({
345            let reobserve_frame = reobserve_frame.clone();
346            let update = update.clone();
347
348            move |entries: Vec<ResizeObserverEntry>| {
349                if let Some(first_entry) = entries.first()
350                    && resize_reference_element
351                        .as_ref()
352                        .is_some_and(|reference_element| first_entry.target() == *reference_element)
353                {
354                    if let Some(reobserve_frame) = reobserve_frame.take() {
355                        cancel_animation_frame(reobserve_frame);
356                    }
357
358                    reobserve_frame
359                        .replace(Some(request_animation_frame(reobserve_closure.as_ref())));
360                }
361
362                update();
363            }
364        });
365
366        resize_observer.replace(Some(
367            ResizeObserver::new(resize_closure.into_js_value().unchecked_ref())
368                .expect("Resize observer should be created."),
369        ));
370
371        if let Some(reference) = reference_element.as_ref()
372            && !animation_frame
373        {
374            resize_observer
375                .borrow()
376                .as_ref()
377                .expect("Resize observer should exist.")
378                .observe(reference);
379        }
380
381        resize_observer
382            .borrow()
383            .as_ref()
384            .expect("Resize observer should exist.")
385            .observe(floating);
386    }
387
388    let frame_id: Rc<RefCell<Option<i32>>> = Rc::new(RefCell::new(None));
389    let prev_ref_rect: Rc<RefCell<Option<ClientRectObject>>> =
390        Rc::new(RefCell::new(animation_frame.then(|| {
391            get_bounding_client_rect(reference, false, false, None)
392        })));
393
394    let frame_loop_frame_id = frame_id.clone();
395    let frame_loop_closure = Rc::new(RefCell::new(None));
396    let frame_loop_closure_clone = frame_loop_closure.clone();
397
398    *frame_loop_closure_clone.borrow_mut() = Some(Closure::new({
399        let owned_reference = owned_reference.clone();
400        let update = update.clone();
401        let prev_ref_rect = prev_ref_rect.clone();
402        let frame_loop_frame_id = frame_loop_frame_id.clone();
403
404        move || {
405            let next_ref_rect =
406                get_bounding_client_rect((&owned_reference).into(), false, false, None);
407
408            if let Some(prev_ref_rect) = prev_ref_rect.borrow().as_ref()
409                && !rects_are_equal(prev_ref_rect, &next_ref_rect)
410            {
411                update();
412            }
413
414            prev_ref_rect.replace(Some(next_ref_rect));
415            frame_loop_frame_id.replace(Some(request_animation_frame(
416                frame_loop_closure
417                    .borrow()
418                    .as_ref()
419                    .expect("Frame loop closure should exist."),
420            )));
421        }
422    }));
423
424    if animation_frame {
425        // Frame loop closure can't be called here, so the code below is copied.
426
427        let next_ref_rect = get_bounding_client_rect((&owned_reference).into(), false, false, None);
428
429        if let Some(prev_ref_rect) = prev_ref_rect.borrow().as_ref()
430            && (next_ref_rect.x != prev_ref_rect.x
431                || next_ref_rect.y != prev_ref_rect.y
432                || next_ref_rect.width != prev_ref_rect.width
433                || next_ref_rect.height != prev_ref_rect.height)
434        {
435            update();
436        }
437
438        prev_ref_rect.replace(Some(next_ref_rect));
439        frame_loop_frame_id.replace(Some(request_animation_frame(
440            frame_loop_closure_clone
441                .borrow()
442                .as_ref()
443                .expect("Frame loop closure should exist."),
444        )));
445    }
446
447    update();
448
449    Box::new(move || {
450        for ancestor in &ancestors {
451            let event_target: &EventTarget = match ancestor {
452                OverflowAncestor::Element(element) => element,
453                OverflowAncestor::Window(window) => window,
454            };
455
456            if ancestor_scoll {
457                event_target
458                    .remove_event_listener_with_callback(
459                        "scroll",
460                        update_closure.as_ref().unchecked_ref(),
461                    )
462                    .expect("Scroll event listener should be removed.");
463            }
464
465            if ancestor_resize {
466                event_target
467                    .remove_event_listener_with_callback(
468                        "resize",
469                        update_closure.as_ref().unchecked_ref(),
470                    )
471                    .expect("Resize event listener should be removed.");
472            }
473        }
474
475        if let Some(cleanup_observe_move) = &cleanup_observe_move {
476            cleanup_observe_move();
477        }
478
479        if let Some(reobserve_frame) = reobserve_frame.take() {
480            cancel_animation_frame(reobserve_frame);
481        }
482
483        if let Some(resize_observer) = resize_observer.take() {
484            resize_observer.disconnect();
485        }
486
487        if let Some(frame_id) = frame_id.take() {
488            cancel_animation_frame(frame_id);
489        }
490    })
491}