feat(albums): draw the album dropdown that was already being computed
Build & publish Arch package / arch-package (push) Successful in 2m1s
CI / check (push) Successful in 2m27s
Search index maintenance / maintain-index (push) Successful in 6s
CI / e2e (push) Successful in 4m49s

Enter on an album card fetched the album's tracks over the IPC and ran
the whole split state machine (splitMode true, splitIndex measured
against the real container), then render() drew the single grid because
it never consulted splitMode; connectedCallback referenced
renderSplitGrid only to satisfy noUnusedLocals. perf.p2 files this as
dead code — it is the only route from the albums grid to track-details,
since a plain click navigates to the catalog page instead.

Two things it needed that the audit does not mention. The grid could
not scroll: .grid-scroll-container is the markup artists-view and
genres-view use, and cover-grid had the class with no rule for it, so
186984px of albums sat in a 772px box at 5000 albums, unreachable by
wheel, keyboard or scrollbar — and that is the element scroll-manager
saves and restores, so its scrollTop was permanently 0. And the shared
context menu was labelled 'Album actions' unconditionally, which nothing
could observe while a track menu was unreachable.

Both halves of the split grid carry the listbox semantics the single
grid gained in the ARIA pass.
This commit is contained in:
2026-08-12 15:10:05 -04:00
parent 287b6445fa
commit 71324b561a
5 changed files with 487 additions and 10 deletions
@@ -13,6 +13,27 @@ const gridStyles = css`
contain: layout style;
}
/*
* The scroller. artists-view and genres-view carry the same
* markup with this rule; cover-grid had the class and no rule for
* it, so nothing in the albums view scrolled — the container grew to
* its full content height inside an overflow: hidden host and
* everything past the first screenful was unreachable by wheel,
* keyboard or scrollbar. Invisible on the eight-album fixture and
* fatal on a real library: measured at 5 000 albums, 186 984 px of
* content in a 772 px box.
*
* It is also what the dropdown's scroll manager was written
* against — it saves and restores this element's scrollTop, which
* was permanently 0.
*/
.grid-scroll-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
contain: paint;
}
/* ========================================
* Album card
* ======================================== */
@@ -430,10 +430,6 @@ export class CoverGrid
override connectedCallback() {
super.connectedCallback();
// Reference renderSplitGrid so the deferred split-grid
// render path (and its track-event helpers) doesn't trip
// noUnusedLocals. Never invoked at runtime.
void this.renderSplitGrid;
this.restoreSortPreferences();
this.loadAlbums();
@@ -1783,7 +1779,18 @@ export class CoverGrid
`;
}
const gridContent = this.renderSingleGrid();
// The split path draws the dropdown between two grids. Until
// this was wired up, `render()` ignored `splitMode` entirely:
// pressing Enter on an album card fetched its tracks over the
// IPC, ran the whole split state machine (`splitMode: true`,
// `splitIndex: 6`, measured against the real container) and
// then drew the single grid regardless, so the only route from
// the albums grid to a track was the plain click that
// navigates away to the catalog page.
const gridContent =
this.splitMode && this.expandedTracks.length > 0
? this.renderSplitGrid()
: this.renderSingleGrid();
return html`
${this.renderPageHeader()}
@@ -1821,10 +1828,12 @@ export class CoverGrid
}
/**
* Dual virtualizer — dropdown sandwiched between
* "before" and "after" grids. Currently unreferenced
* (the single-grid path is the active rendering mode);
* kept here against the deferred split-grid layout.
* Dual virtualizer — dropdown sandwiched between the "before" and
* "after" halves of the grid.
*
* Both halves carry the same listbox semantics as the single grid:
* they are one control to the user, and a selection that spans the
* dropdown must be announced the same way on either side of it.
*/
private renderSplitGrid() {
const sm = this.scrollMgr;
@@ -1836,6 +1845,9 @@ export class CoverGrid
return html`
<lit-virtualizer
id="grid-before"
role="listbox"
aria-label="Albums"
aria-multiselectable="true"
.items=${this.getBeforeEntries()}
.renderItem=${this.renderGridEntry}
.keyFunction=${(entry: GridEntry) => entry.album.ID}
@@ -1865,6 +1877,9 @@ export class CoverGrid
? html`
<lit-virtualizer
id="grid-after"
role="listbox"
aria-label="Albums, continued"
aria-multiselectable="true"
.items=${afterEntries}
.renderItem=${this.renderGridEntry}
.keyFunction=${(entry: GridEntry) => entry.album.ID}
@@ -1904,7 +1919,13 @@ export class CoverGrid
>
${ctxMenu.contextMenuOpen
? html`
<div class="context-menu-panel" role="menu" aria-label="Album actions">
<div
class="context-menu-panel"
role="menu"
aria-label=${this.contextMenuTarget.kind === 'track'
? 'Track actions'
: 'Album actions'}
>
<wa-dropdown-item
@click=${() =>
this.onContextMenuAction(