fix(ui): in-page browse virtual lists and cover load priority (#783)

* fix(ui): in-page browse virtual lists and cover load priority

Align virtualizer scrollMargin with the in-page scroll viewport so
artist/album rows do not vanish on deep scroll after #731. Resolve
CachedImage IntersectionObserver root to the nearest scrolling pane so
network fetch slots favor visible covers; throttle Artists infinite scroll
to one page per visibleCount update.

* chore(release): CHANGELOG and credits for PR #783
This commit is contained in:
cucadmuh
2026-05-18 21:30:36 +03:00
committed by GitHub
parent 70c2fdfbf9
commit f6fe1484a9
10 changed files with 112 additions and 20 deletions
@@ -0,0 +1,44 @@
// @vitest-environment jsdom
import { describe, expect, it, beforeEach } from 'vitest';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../../constants/appScroll';
import { resolveIntersectionScrollRoot } from './resolveIntersectionScrollRoot';
describe('resolveIntersectionScrollRoot', () => {
beforeEach(() => {
document.body.innerHTML = '';
});
it('prefers the nearest scrolling ancestor', () => {
const outer = document.createElement('div');
const scroller = document.createElement('div');
const img = document.createElement('img');
Object.defineProperty(scroller, 'scrollHeight', { value: 2000, configurable: true });
Object.defineProperty(scroller, 'clientHeight', { value: 400, configurable: true });
scroller.style.overflowY = 'auto';
outer.appendChild(scroller);
scroller.appendChild(img);
document.body.appendChild(outer);
expect(resolveIntersectionScrollRoot(img)).toBe(scroller);
});
it('falls back to mainstage in-page viewport class', () => {
const inpage = document.createElement('div');
inpage.className = 'mainstage-inpage-scroll__viewport';
const img = document.createElement('img');
inpage.appendChild(img);
document.body.appendChild(inpage);
expect(resolveIntersectionScrollRoot(img)).toBe(inpage);
});
it('falls back to app main scroll viewport id', () => {
const main = document.createElement('div');
main.id = APP_MAIN_SCROLL_VIEWPORT_ID;
const img = document.createElement('img');
main.appendChild(img);
document.body.appendChild(main);
expect(resolveIntersectionScrollRoot(img)).toBe(main);
});
});
@@ -0,0 +1,25 @@
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../../constants/appScroll';
/**
* Scroll container for `IntersectionObserver` priority scoring on cover art.
* Prefer the nearest scrolling ancestor (in-page browse, queue, route viewport);
* fall back to known viewport class hooks, then the main route scroll element.
*/
export function resolveIntersectionScrollRoot(node: HTMLElement): Element | null {
let parent = node.parentElement;
while (parent) {
const { overflowY } = window.getComputedStyle(parent);
if (
(overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay')
&& parent.scrollHeight > parent.clientHeight + 2
) {
return parent;
}
parent = parent.parentElement;
}
return (
(node.closest('.mainstage-inpage-scroll__viewport') as HTMLElement | null)
?? (node.closest('.app-shell-route-scroll__viewport') as HTMLElement | null)
?? (document.getElementById(APP_MAIN_SCROLL_VIEWPORT_ID) as HTMLElement | null)
);
}