// ── PageUp/PageDown over a list the keyboard otherwise walks row by row (owner request) ────────── // The jump-to palette (461 rows, ~11 in view) and every popover list — the sidebar's channel filter // over hundreds of channels, the Plex pickers, the filter and sort menus — had only the arrows. One // definition of "a page" here, so the palette and the dropdowns cannot drift apart. /** * A page when there is no layout to measure (jsdom, or a list not painted yet). */ export const PAGE_FALLBACK = 10; /** The nearest ancestor that scrolls vertically. */ export function scrollParent(node: Element): HTMLElement | null { for (let el = node.parentElement; el; el = el.parentElement) { const oy = getComputedStyle(el).overflowY; if (oy === "auto" || oy === "scroll") return el; } return null; } /** How far one PageUp/PageDown moves: the rows that fit wholly inside `scroller` right now, minus one, * so the row you paged from stays in sight at the new page's edge (a native listbox does the same). * Measured rather than fixed: a list's height follows the viewport, and group headings eat into it. */ export function pageStep(scroller: Element | null, rows: Iterable): number { const box = scroller?.getBoundingClientRect(); if (!box || box.height === 0) return PAGE_FALLBACK; let fits = 0; for (const row of rows) { const r = row.getBoundingClientRect(); if (r.top >= box.top - 0.5 && r.bottom <= box.bottom + 0.5) fits++; } return Math.max(1, fits - 1); } /** Where a page move of `delta` from row `from` lands among `count` rows. It stops at both ends — no * wrap, unlike the arrows: one step past the end is a small, obvious move, but a page-sized jump that * lands back at the top reads as having lost your place. A row `usable` refuses (a disabled menu row) * is never the target: the search goes on past the landing row first, then back toward the start, * never beyond it. `null` = nowhere to go. */ export function pageTarget( from: number, delta: number, count: number, usable: (index: number) => boolean = () => true, ): number | null { if (count <= 0) return null; const to = Math.min(count - 1, Math.max(0, from + delta)); const dir = delta < 0 ? -1 : 1; for (let i = to; i >= 0 && i < count; i += dir) if (usable(i)) return i; for (let i = to - dir; (i - from) * dir > 0; i -= dir) if (usable(i)) return i; return null; }