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",
|
"KeyboardEvent",
|
||||||
"Location",
|
"Location",
|
||||||
"Performance",
|
"Performance",
|
||||||
|
"ScrollIntoViewOptions",
|
||||||
|
"ScrollLogicalPosition",
|
||||||
"Storage",
|
"Storage",
|
||||||
"Window",
|
"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 ---------------------------------------------------------
|
// ---- the store ---------------------------------------------------------
|
||||||
|
|
||||||
/// Every signal the components share. `Copy` so closures capture it
|
/// 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 focused = move || store.focus.get() == Focus::Library;
|
||||||
let library = store.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 toolbar = move || {
|
||||||
let pane = library.get();
|
let pane = library.get();
|
||||||
let selection = pane.selected_item();
|
let selection = pane.selected_item();
|
||||||
|
|
@ -888,6 +916,14 @@ fn LibraryView(store: Store) -> impl IntoView {
|
||||||
#[component]
|
#[component]
|
||||||
fn QueueView(store: Store) -> impl IntoView {
|
fn QueueView(store: Store) -> impl IntoView {
|
||||||
let focused = move || store.focus.get() == Focus::Queue;
|
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! {
|
view! {
|
||||||
<section
|
<section
|
||||||
class="pane queue"
|
class="pane queue"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue