biji_ui/utils/
prevent_scroll.rs

1use std::time::Duration;
2
3use leptos::{
4    leptos_dom::{self, helpers::TimeoutHandle},
5    prelude::*,
6};
7
8/// A utility that prevents scrolling on the document body when a condition is true.
9///
10/// This is useful for modals, dialogs, and other overlay components that should
11/// prevent background scrolling. It also compensates for scrollbar width to prevent
12/// layout shift.
13///
14/// # Example
15///
16/// ```rust
17/// let is_open = RwSignal::new(false);
18///
19/// let _effect = use_prevent_scroll(
20///     move || is_open.get(),
21///     Duration::from_millis(200)
22/// );
23/// ```
24pub fn use_prevent_scroll<F>(should_prevent: F, hide_delay: Duration) -> RenderEffect<()>
25where
26    F: Fn() -> bool + 'static,
27{
28    let hide_handle: StoredValue<Option<TimeoutHandle>> = StoredValue::new(None);
29
30    RenderEffect::new(move |_| {
31        if should_prevent() {
32            // Clear any pending hide timeout
33            if let Some(h) = hide_handle.get_value() {
34                h.clear();
35            }
36
37            // Apply prevent scroll styles
38            if let Some(doc) = document().body() {
39                let client_width = f64::from(doc.client_width());
40                let inner_width = window()
41                    .inner_width()
42                    .unwrap()
43                    .as_f64()
44                    .unwrap_or(client_width);
45                let scrollbar_width = inner_width - client_width;
46
47                let _ = doc.style().set_property("overflow", "hidden");
48                let _ = doc
49                    .style()
50                    .set_property("--scrollbar-width", &format!("{}px", scrollbar_width));
51                let _ = doc
52                    .style()
53                    .set_property("padding-right", &format!("calc({}px)", scrollbar_width));
54            }
55        } else {
56            // Schedule removal of prevent scroll styles
57            let h = leptos_dom::helpers::set_timeout_with_handle(
58                move || {
59                    if let Some(doc) = document().body() {
60                        let _ = doc.style().remove_property("overflow");
61                        let _ = doc.style().remove_property("--scrollbar-width");
62                        let _ = doc.style().remove_property("padding-right");
63                    }
64                },
65                hide_delay,
66            )
67            .expect("set timeout in use_prevent_scroll");
68            hide_handle.set_value(Some(h));
69        }
70    })
71}