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