fix(downloads): keep collapse rows mounted across viewport
This commit is contained in:
@@ -3,6 +3,7 @@ import { buildDownloadLogicalRows, type DownloadLogicalRow } from "./downloads-m
|
|||||||
import {
|
import {
|
||||||
activateDownloadDisclosureTransition,
|
activateDownloadDisclosureTransition,
|
||||||
DOWNLOAD_DISCLOSURE_DURATION_MS,
|
DOWNLOAD_DISCLOSURE_DURATION_MS,
|
||||||
|
getDownloadDisclosurePinnedIds,
|
||||||
mergeDownloadDisclosureRows,
|
mergeDownloadDisclosureRows,
|
||||||
prepareDownloadDisclosureTransition,
|
prepareDownloadDisclosureTransition,
|
||||||
scheduleDownloadDisclosureActivation,
|
scheduleDownloadDisclosureActivation,
|
||||||
@@ -98,6 +99,7 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D
|
|||||||
desiredRowsRef.current = desiredRows;
|
desiredRowsRef.current = desiredRows;
|
||||||
const previousRowsRef = useRef<readonly DownloadLogicalRow[]>(desiredRows);
|
const previousRowsRef = useRef<readonly DownloadLogicalRow[]>(desiredRows);
|
||||||
const transitionRowsRef = useRef<DownloadDisclosureRow[] | null>(null);
|
const transitionRowsRef = useRef<DownloadDisclosureRow[] | null>(null);
|
||||||
|
const transitionPinnedIdsRef = useRef<string[]>([]);
|
||||||
const [transitionRows, setTransitionRows] = useState<DownloadDisclosureRow[] | null>(null);
|
const [transitionRows, setTransitionRows] = useState<DownloadDisclosureRow[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -108,10 +110,16 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D
|
|||||||
const prepared = prepareDownloadDisclosureTransition(transitionRowsRef.current ?? previousRowsRef.current, desiredRowsRef.current);
|
const prepared = prepareDownloadDisclosureTransition(transitionRowsRef.current ?? previousRowsRef.current, desiredRowsRef.current);
|
||||||
if (!prepared.animated) {
|
if (!prepared.animated) {
|
||||||
transitionRowsRef.current = null;
|
transitionRowsRef.current = null;
|
||||||
|
transitionPinnedIdsRef.current = [];
|
||||||
previousRowsRef.current = desiredRowsRef.current;
|
previousRowsRef.current = desiredRowsRef.current;
|
||||||
setTransitionRows(null);
|
setTransitionRows(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
transitionPinnedIdsRef.current = getDownloadDisclosurePinnedIds(prepared.rows, {
|
||||||
|
scrollTop: viewport.scrollTop,
|
||||||
|
viewportHeight: viewport.viewportHeight,
|
||||||
|
overscan: DOWNLOAD_VIRTUAL_OVERSCAN_ROWS
|
||||||
|
});
|
||||||
transitionRowsRef.current = prepared.rows;
|
transitionRowsRef.current = prepared.rows;
|
||||||
setTransitionRows(prepared.rows);
|
setTransitionRows(prepared.rows);
|
||||||
let settleTimer = 0;
|
let settleTimer = 0;
|
||||||
@@ -122,6 +130,7 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D
|
|||||||
settleTimer = window.setTimeout(() => {
|
settleTimer = window.setTimeout(() => {
|
||||||
previousRowsRef.current = desiredRowsRef.current;
|
previousRowsRef.current = desiredRowsRef.current;
|
||||||
transitionRowsRef.current = null;
|
transitionRowsRef.current = null;
|
||||||
|
transitionPinnedIdsRef.current = [];
|
||||||
setTransitionRows(null);
|
setTransitionRows(null);
|
||||||
}, DOWNLOAD_DISCLOSURE_DURATION_MS);
|
}, DOWNLOAD_DISCLOSURE_DURATION_MS);
|
||||||
});
|
});
|
||||||
@@ -136,7 +145,7 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D
|
|||||||
scrollTop: viewport.scrollTop,
|
scrollTop: viewport.scrollTop,
|
||||||
viewportHeight: viewport.viewportHeight,
|
viewportHeight: viewport.viewportHeight,
|
||||||
overscan: DOWNLOAD_VIRTUAL_OVERSCAN_ROWS,
|
overscan: DOWNLOAD_VIRTUAL_OVERSCAN_ROWS,
|
||||||
pinnedIds: [model.editingPackageId]
|
pinnedIds: [model.editingPackageId, ...transitionPinnedIdsRef.current]
|
||||||
}), [model.editingPackageId, renderedRows, viewport.scrollTop, viewport.viewportHeight]);
|
}), [model.editingPackageId, renderedRows, viewport.scrollTop, viewport.viewportHeight]);
|
||||||
const spacerStyle = { "--downloads-virtual-total-height": `${virtualWindow.totalHeight}px` } as CSSProperties;
|
const spacerStyle = { "--downloads-virtual-total-height": `${virtualWindow.totalHeight}px` } as CSSProperties;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import type { DownloadLogicalRow } from "./downloads-model";
|
import type { DownloadLogicalRow } from "./downloads-model";
|
||||||
import { DOWNLOAD_FILE_ROW_HEIGHT } from "./download-virtualizer";
|
import {
|
||||||
|
DOWNLOAD_FILE_ROW_HEIGHT,
|
||||||
|
calculateDownloadVirtualWindow,
|
||||||
|
type DownloadVirtualWindowOptions
|
||||||
|
} from "./download-virtualizer";
|
||||||
|
|
||||||
export const DOWNLOAD_DISCLOSURE_DURATION_MS = 300;
|
export const DOWNLOAD_DISCLOSURE_DURATION_MS = 300;
|
||||||
export const DOWNLOAD_DISCLOSURE_MAX_ANIMATED_ITEMS = 64;
|
export const DOWNLOAD_DISCLOSURE_MAX_ANIMATED_ITEMS = 64;
|
||||||
@@ -149,3 +153,13 @@ export function activateDownloadDisclosureTransition(rows: readonly DownloadDisc
|
|||||||
: { ...row, height: 0, disclosureOpacity: 0 };
|
: { ...row, height: 0, disclosureOpacity: 0 };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDownloadDisclosurePinnedIds(
|
||||||
|
preparedRows: readonly DownloadDisclosureRow[],
|
||||||
|
options: DownloadVirtualWindowOptions
|
||||||
|
): string[] {
|
||||||
|
const windowOptions = { ...options, pinnedIds: [] };
|
||||||
|
const startRows = calculateDownloadVirtualWindow(preparedRows, windowOptions).rows;
|
||||||
|
const targetRows = calculateDownloadVirtualWindow(activateDownloadDisclosureTransition(preparedRows), windowOptions).rows;
|
||||||
|
return [...new Set([...startRows, ...targetRows].map((entry) => entry.id))];
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
DOWNLOAD_DISCLOSURE_MAX_ANIMATED_ITEMS,
|
DOWNLOAD_DISCLOSURE_MAX_ANIMATED_ITEMS,
|
||||||
activateDownloadDisclosureTransition,
|
activateDownloadDisclosureTransition,
|
||||||
|
getDownloadDisclosurePinnedIds,
|
||||||
mergeDownloadDisclosureRows,
|
mergeDownloadDisclosureRows,
|
||||||
prepareDownloadDisclosureTransition,
|
prepareDownloadDisclosureTransition,
|
||||||
scheduleDownloadDisclosureActivation,
|
scheduleDownloadDisclosureActivation,
|
||||||
@@ -302,6 +303,45 @@ describe("virtualisierte Paketanimation", () => {
|
|||||||
expect(stableDownloadDisclosureRows(collapsed).map((row) => row.id)).toEqual(["package-a", "package-b", "b-1"]);
|
expect(stableDownloadDisclosureRows(collapsed).map((row) => row.id)).toEqual(["package-a", "package-b", "b-1"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("mountet beim Zuklappen auch erst im Ziel-Viewport sichtbare Folgepakete vor der Bewegung", () => {
|
||||||
|
const childIds = Array.from({ length: 26 }, (_, index) => `first-${index}`);
|
||||||
|
const followingPackages = Array.from({ length: 36 }, (_, index) => pkg(`following-${index}`, `Folge ${index}`, [`following-item-${index}`]));
|
||||||
|
const firstPackage = pkg("first", "Erstes Paket", childIds);
|
||||||
|
const packageOrder = [firstPackage.id, ...followingPackages.map((entry) => entry.id)];
|
||||||
|
const packages = Object.fromEntries([firstPackage, ...followingPackages].map((entry) => [entry.id, entry]));
|
||||||
|
const queueItems = Object.fromEntries([
|
||||||
|
...childIds.map((id) => [id, item(id, firstPackage.id, "queued")] as const),
|
||||||
|
...followingPackages.map((entry, index) => [`following-item-${index}`, item(`following-item-${index}`, entry.id, "queued")] as const)
|
||||||
|
]);
|
||||||
|
const buildRows = (collapsed: boolean) => buildDownloadLogicalRows(buildDownloadsViewModel({
|
||||||
|
packageOrder,
|
||||||
|
packages,
|
||||||
|
items: queueItems,
|
||||||
|
displayMode: "packages",
|
||||||
|
filter: "all",
|
||||||
|
providerFilter: "all",
|
||||||
|
query: "",
|
||||||
|
collapsedPackageIds: collapsed ? packageOrder : followingPackages.map((entry) => entry.id),
|
||||||
|
selectedIds: [],
|
||||||
|
hideExtractedItems: false,
|
||||||
|
showAllPackages: true,
|
||||||
|
renderLimit: 100
|
||||||
|
}));
|
||||||
|
const prepared = prepareDownloadDisclosureTransition(stableDownloadDisclosureRows(buildRows(false)), buildRows(true));
|
||||||
|
const options = { scrollTop: 0, viewportHeight: 720, overscan: 8 };
|
||||||
|
const startWindow = calculateDownloadVirtualWindow(prepared.rows, options);
|
||||||
|
const targetWindow = calculateDownloadVirtualWindow(activateDownloadDisclosureTransition(prepared.rows), options);
|
||||||
|
const startIds = new Set(startWindow.rows.map((entry) => entry.id));
|
||||||
|
const targetOnlyIds = targetWindow.rows.map((entry) => entry.id).filter((id) => !startIds.has(id));
|
||||||
|
const pinnedIds = getDownloadDisclosurePinnedIds(prepared.rows, options);
|
||||||
|
const mountedStartWindow = calculateDownloadVirtualWindow(prepared.rows, { ...options, pinnedIds });
|
||||||
|
const mountedStartIds = new Set(mountedStartWindow.rows.map((entry) => entry.id));
|
||||||
|
|
||||||
|
expect(targetOnlyIds.length).toBeGreaterThan(10);
|
||||||
|
expect(targetOnlyIds.every((id) => mountedStartIds.has(id))).toBe(true);
|
||||||
|
expect(pinnedIds.length).toBeLessThan(80);
|
||||||
|
});
|
||||||
|
|
||||||
it("überspringt die Bewegung bei riesigen Einzelpaketen und hält das DOM-Fenster begrenzt", () => {
|
it("überspringt die Bewegung bei riesigen Einzelpaketen und hält das DOM-Fenster begrenzt", () => {
|
||||||
const count = DOWNLOAD_DISCLOSURE_MAX_ANIMATED_ITEMS + 10_000;
|
const count = DOWNLOAD_DISCLOSURE_MAX_ANIMATED_ITEMS + 10_000;
|
||||||
const hugePackage = pkg("huge-package", "Groß", Array.from({ length: count }, (_, index) => `huge-${index}`));
|
const hugePackage = pkg("huge-package", "Groß", Array.from({ length: count }, (_, index) => `huge-${index}`));
|
||||||
|
|||||||
Reference in New Issue
Block a user