Web client: scroll the keyboard cursor back into view
Moving the library or queue cursor with the keyboard (j/k, page keys, first/last) updated the selection but never scrolled the list, so the selected row could slide out of the scroll box and disappear. Each pane now runs an effect that re-scrolls its `.selected` row into view whenever the cursor moves. The queue cursor is its own signal, so this fires on moves but not on every stream-driven queue refresh; the library cursor lives in the library pane signal. The scroll is deferred to the next animation frame (the freshly-rendered row must be in the DOM) and uses `block: nearest`, so an already-visible row does not jump. Click/drag selection needs no help -- the pointer is already on-screen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
42c5e9dbc2
commit
b6f1275d6b
|
|
@ -25,6 +25,8 @@ web-sys = { workspace = true, features = [
|
|||
"KeyboardEvent",
|
||||
"Location",
|
||||
"Performance",
|
||||
"ScrollIntoViewOptions",
|
||||
"ScrollLogicalPosition",
|
||||
"Storage",
|
||||
"Window",
|
||||
] }
|
||||
|
|
|
|||
|
|
@ -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! {
|
||||
<section
|
||||
class="pane queue"
|
||||
|
|
|
|||
Loading…
Reference in New Issue