diff --git a/cbd-web/Cargo.toml b/cbd-web/Cargo.toml index 4d914d0..9c18394 100644 --- a/cbd-web/Cargo.toml +++ b/cbd-web/Cargo.toml @@ -25,6 +25,8 @@ web-sys = { workspace = true, features = [ "KeyboardEvent", "Location", "Performance", + "ScrollIntoViewOptions", + "ScrollLogicalPosition", "Storage", "Window", ] } diff --git a/cbd-web/src/app.rs b/cbd-web/src/app.rs index 2db02bc..6c736cb 100644 --- a/cbd-web/src/app.rs +++ b/cbd-web/src/app.rs @@ -65,6 +65,27 @@ fn apply_theme(theme: &str) { } } +/// Scrolls the selected row of a pane back into view after a keyboard move, +/// so the cursor never drifts out of the scroll box (the click handlers +/// need no help — the pointer is already on-screen). `pane_selector` picks +/// the pane (`.pane.library` / `.pane.queue`); we then find its `.selected` +/// row. Deferred to the next animation frame so the freshly-rendered row is +/// in the DOM. `block: nearest` scrolls the minimum amount — an already +/// visible row does not move. +fn scroll_selected_into_view(pane_selector: &'static str) { + request_animation_frame(move || { + let selector = format!("{pane_selector} .list li.selected"); + if let Some(el) = web_sys::window() + .and_then(|w| w.document()) + .and_then(|d| d.query_selector(&selector).ok().flatten()) + { + let opts = web_sys::ScrollIntoViewOptions::new(); + opts.set_block(web_sys::ScrollLogicalPosition::Nearest); + el.scroll_into_view_with_scroll_into_view_options(&opts); + } + }); +} + // ---- the store --------------------------------------------------------- /// Every signal the components share. `Copy` so closures capture it @@ -733,6 +754,13 @@ fn LibraryView(store: Store) -> impl IntoView { let focused = move || store.focus.get() == Focus::Library; let library = store.library; + // Keep the keyboard cursor visible: whenever the library cursor (or its + // listing) changes, scroll the selected row back into the scroll box. + Effect::new(move |_| { + let _ = library.with(|p| p.selected); + scroll_selected_into_view(".pane.library"); + }); + let toolbar = move || { let pane = library.get(); let selection = pane.selected_item(); @@ -888,6 +916,14 @@ fn LibraryView(store: Store) -> impl IntoView { #[component] fn QueueView(store: Store) -> impl IntoView { let focused = move || store.focus.get() == Focus::Queue; + + // Keep the keyboard cursor visible. The cursor is its own signal, so this + // fires on moves but not on every stream-driven queue refresh. + Effect::new(move |_| { + let _ = store.queue_cursor.get(); + scroll_selected_into_view(".pane.queue"); + }); + view! {