Skip to main content

dioxus_bootstrap_css/
scrollspy.rs

1use dioxus::prelude::*;
2
3/// Bootstrap Scrollspy — tracks scroll position and updates active signal.
4///
5/// Place this component in your layout. It watches the given target element
6/// (by CSS selector) and updates the `active` signal with the `id` of the
7/// currently visible section.
8///
9/// ```rust
10/// let active_section = use_signal(|| String::new());
11/// rsx! {
12///     Scrollspy { target: "main", active: active_section, offset: 80 }
13///     nav {
14///         a { class: if *active_section.read() == "intro" { "nav-link active" } else { "nav-link" },
15///             href: "#intro", "Intro" }
16///         a { class: if *active_section.read() == "features" { "nav-link active" } else { "nav-link" },
17///             href: "#features", "Features" }
18///     }
19/// }
20/// ```
21#[derive(Clone, PartialEq, Props)]
22pub struct ScrollspyProps {
23    /// CSS selector for the scrollable container (e.g., "main", "#content", "body").
24    #[props(default = "body".to_string())]
25    pub target: String,
26    /// Signal that receives the `id` of the currently active section.
27    pub active: Signal<String>,
28    /// Offset in pixels from the top (useful for fixed/sticky navbars).
29    #[props(default = 0)]
30    pub offset: i32,
31}
32
33#[component]
34pub fn Scrollspy(props: ScrollspyProps) -> Element {
35    let mut active_signal = props.active;
36    let target = props.target.clone();
37    let offset = props.offset;
38
39    // Set up IntersectionObserver via eval to track which [id] section is visible
40    use_effect(move || {
41        let target = target.clone();
42        document::eval(&format!(
43            r#"
44            (function() {{
45                var container = document.querySelector('{target}');
46                if (!container || container === document.body) container = document;
47                var sections = document.querySelectorAll('[id]');
48                if (sections.length === 0) return;
49
50                function update() {{
51                    var scrollTop = (container === document)
52                        ? window.scrollY || document.documentElement.scrollTop
53                        : container.scrollTop;
54                    var offset = {offset};
55                    var active = '';
56                    sections.forEach(function(section) {{
57                        var rect = section.getBoundingClientRect();
58                        if (rect.top <= offset + 10) {{
59                            active = section.id;
60                        }}
61                    }});
62                    if (active && window.__dioxus_scrollspy_active !== active) {{
63                        window.__dioxus_scrollspy_active = active;
64                        // Dispatch a custom event that Dioxus can listen to
65                        window.dispatchEvent(new CustomEvent('scrollspy', {{ detail: active }}));
66                    }}
67                }}
68
69                var scrollTarget = (container === document) ? window : container;
70                scrollTarget.addEventListener('scroll', update, {{ passive: true }});
71                update();
72            }})();
73            "#
74        ));
75    });
76
77    // Listen for scrollspy events via polling with eval
78    // Note: This uses a simple approach — the JS side updates a global,
79    // and we read it periodically via use_effect
80    use_effect(move || {
81        let eval_handle = document::eval(
82            r#"
83            (function() {
84                return new Promise(function(resolve) {
85                    var current = window.__dioxus_scrollspy_active || '';
86                    // Set up listener for changes
87                    window.addEventListener('scrollspy', function handler(e) {
88                        resolve(e.detail);
89                        window.removeEventListener('scrollspy', handler);
90                    });
91                    // If already set, resolve immediately
92                    if (current) resolve(current);
93                });
94            })()
95            "#,
96        );
97
98        spawn(async move {
99            if let Ok(value) = eval_handle.await {
100                if let Some(id) = value.as_str() {
101                    active_signal.set(id.to_string());
102                }
103            }
104        });
105    });
106
107    rsx! {}
108}