/** * A roving tab stop over a virtualized list of rows. * * Three lists needed the same thing at once — the queue panel and both * playlist detail views — because a context menu opened with Shift+F10 * needs a focused row to open *from*, and none of the three had one: * their rows were plain `
`s with no `tabindex` and no `role`. * * `track-list` deliberately does not use this. Its equivalent predates * it, carries selection semantics (shift-extend, ctrl-toggle) that the * other three do not have, and is pinned by its own tests; converting it * would be a rewrite of the one list that already worked. * * Two things here are not optional: * * - **The virtualizer is told the index changed.** Rows come from the * `virtualize` directive, which re-renders on the virtualizer's *own* * properties — a host re-render does not move a `tabindex`. * - **Focus is taken after the update.** The row for an index that was * off-screen does not exist until the virtualizer has scrolled to it. */ import type { LitVirtualizer } from '@lit-labs/virtualizer'; export interface RovingRowsHost { requestUpdate(): void; updateComplete: Promise; } /** The keys this handles, and what they mean given a row count. */ export function nextRovingIndex( key: string, current: number, count: number, ): number | null { switch (key) { case 'ArrowDown': return Math.min(current + 1, count - 1); case 'ArrowUp': return Math.max(current - 1, 0); case 'Home': return 0; case 'End': return count - 1; default: return null; } } /** * Move the tab stop to `index` and put focus on it. * * `rowSelector` receives the index and must return a selector matching * that row inside the virtualizer's light DOM. */ export async function focusRovingRow( host: RovingRowsHost, virtualizer: LitVirtualizer | undefined, index: number, rowSelector: (index: number) => string, ): Promise { host.requestUpdate(); virtualizer?.requestUpdate(); virtualizer?.scrollToIndex(index, 'nearest'); await host.updateComplete; virtualizer ?.querySelector(rowSelector(index)) ?.focus(); }