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