logicaffeine_web/ui/fragment.rs
1//! Pages whose route models a `#fragment` open at the element it names.
2//!
3//! The route keeps the fragment through boot normalization (`#:section` in its `#[route]`), but
4//! the browser's own jump to it cannot be relied on: the prerendered copy it jumped in is swapped
5//! for the live app, a link inside the app changes the route without loading a page, and a page
6//! whose body is fetched renders the element only once the fetch lands. So the page scrolls
7//! itself, once the element exists, honouring `prefers-reduced-motion`.
8
9/// How long to wait for the element to appear: a fetched page body (the legal pages) can take a
10/// few seconds on a slow connection.
11#[cfg(target_arch = "wasm32")]
12const WAIT_FRAMES: u32 = 600;
13
14/// Bring the element with `id` to the top of the page, under the sticky nav (the target's
15/// `scroll-margin-top` sets the gap), waiting up to ten seconds for it to be rendered.
16pub fn scroll_to_section(id: String) {
17 #[cfg(target_arch = "wasm32")]
18 wasm_bindgen_futures::spawn_local(async move {
19 for _ in 0..WAIT_FRAMES {
20 gloo_timers::future::TimeoutFuture::new(16).await;
21 let Some(window) = web_sys::window() else { return };
22 let Some(element) = window.document().and_then(|d| d.get_element_by_id(&id)) else { continue };
23 let reduced = window
24 .match_media("(prefers-reduced-motion: reduce)")
25 .ok()
26 .flatten()
27 .is_some_and(|m| m.matches());
28 let options = web_sys::ScrollIntoViewOptions::new();
29 options.set_behavior(if reduced { web_sys::ScrollBehavior::Instant } else { web_sys::ScrollBehavior::Smooth });
30 options.set_block(web_sys::ScrollLogicalPosition::Start);
31 element.scroll_into_view_with_scroll_into_view_options(&options);
32 return;
33 }
34 });
35 #[cfg(not(target_arch = "wasm32"))]
36 let _ = id;
37}