fix(downloads): animate package contents as one panel
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
mergeDownloadDisclosureRows,
|
mergeDownloadDisclosureRows,
|
||||||
prepareDownloadDisclosureTransition,
|
prepareDownloadDisclosureTransition,
|
||||||
scheduleDownloadDisclosureActivation,
|
scheduleDownloadDisclosureActivation,
|
||||||
|
stableDownloadDisclosureRows,
|
||||||
type DownloadDisclosureRow
|
type DownloadDisclosureRow
|
||||||
} from "./download-disclosure-transition";
|
} from "./download-disclosure-transition";
|
||||||
import type { DownloadsViewActions, DownloadsViewModel } from "./DownloadsView";
|
import type { DownloadsViewActions, DownloadsViewModel } from "./DownloadsView";
|
||||||
@@ -69,7 +70,16 @@ function disclosureOpacity(row: DownloadLogicalRow | DownloadDisclosureRow): num
|
|||||||
return "disclosureOpacity" in row && typeof row.disclosureOpacity === "number" ? row.disclosureOpacity : 1;
|
return "disclosureOpacity" in row && typeof row.disclosureOpacity === "number" ? row.disclosureOpacity : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderVirtualRow(row: DownloadLogicalRow, model: DownloadsViewModel, actions: DownloadsViewActions): ReactElement {
|
function renderVirtualRow(row: DownloadLogicalRow | DownloadDisclosureRow, model: DownloadsViewModel, actions: DownloadsViewActions): ReactElement {
|
||||||
|
if (row.type === "item-group") {
|
||||||
|
return (
|
||||||
|
<div className="downloads-disclosure-item-group">
|
||||||
|
{row.items.map((entry) => (
|
||||||
|
<ItemRow actions={actions} columnOrder={model.columnOrder} gridTemplate={model.gridTemplate} item={entry.item} key={entry.id} selected={model.selectedIds.has(entry.item.id)} sessionRunning={model.running} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (row.type === "item") {
|
if (row.type === "item") {
|
||||||
return (
|
return (
|
||||||
<ItemRow actions={actions} columnOrder={model.columnOrder} gridTemplate={model.gridTemplate} item={row.item} selected={model.selectedIds.has(row.item.id)} sessionRunning={model.running} />
|
<ItemRow actions={actions} columnOrder={model.columnOrder} gridTemplate={model.gridTemplate} item={row.item} selected={model.selectedIds.has(row.item.id)} sessionRunning={model.running} />
|
||||||
@@ -140,7 +150,7 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D
|
|||||||
};
|
};
|
||||||
}, [model.disclosureRevision, model.displayMode]);
|
}, [model.disclosureRevision, model.displayMode]);
|
||||||
|
|
||||||
const renderedRows = useMemo(() => transitionRows ? mergeDownloadDisclosureRows(transitionRows, desiredRows) : desiredRows, [desiredRows, transitionRows]);
|
const renderedRows = useMemo<DownloadDisclosureRow[]>(() => transitionRows ? mergeDownloadDisclosureRows(transitionRows, desiredRows) : stableDownloadDisclosureRows(desiredRows), [desiredRows, transitionRows]);
|
||||||
const virtualWindow = useMemo(() => calculateDownloadVirtualWindow(renderedRows, {
|
const virtualWindow = useMemo(() => calculateDownloadVirtualWindow(renderedRows, {
|
||||||
scrollTop: viewport.scrollTop,
|
scrollTop: viewport.scrollTop,
|
||||||
viewportHeight: viewport.viewportHeight,
|
viewportHeight: viewport.viewportHeight,
|
||||||
@@ -155,7 +165,7 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D
|
|||||||
{!state ? (
|
{!state ? (
|
||||||
<div className="downloads-virtual-spacer" style={spacerStyle}>
|
<div className="downloads-virtual-spacer" style={spacerStyle}>
|
||||||
{virtualWindow.rows.map((entry) => (
|
{virtualWindow.rows.map((entry) => (
|
||||||
<div className={`downloads-virtual-row${disclosureClassName(entry.source)}`} data-download-virtual-index={entry.index} key={`${entry.source.type}:${entry.id}`} style={rowStyle(entry.top, entry.height, disclosureOpacity(entry.source))}>
|
<div className={`downloads-virtual-row${entry.source.type === "item-group" ? " is-disclosure-group" : ""}${disclosureClassName(entry.source)}`} data-download-virtual-index={entry.index} key={`${entry.source.type}:${entry.id}`} style={rowStyle(entry.top, entry.height, disclosureOpacity(entry.source))}>
|
||||||
{renderVirtualRow(entry.source, model, actions)}
|
{renderVirtualRow(entry.source, model, actions)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { DownloadLogicalRow } from "./downloads-model";
|
|||||||
import {
|
import {
|
||||||
DOWNLOAD_FILE_ROW_HEIGHT,
|
DOWNLOAD_FILE_ROW_HEIGHT,
|
||||||
calculateDownloadVirtualWindow,
|
calculateDownloadVirtualWindow,
|
||||||
|
type DownloadVirtualRowInput,
|
||||||
type DownloadVirtualWindowOptions
|
type DownloadVirtualWindowOptions
|
||||||
} from "./download-virtualizer";
|
} from "./download-virtualizer";
|
||||||
|
|
||||||
@@ -26,28 +27,57 @@ export function scheduleDownloadDisclosureActivation(
|
|||||||
|
|
||||||
export type DownloadDisclosurePhase = "stable" | "entering" | "leaving";
|
export type DownloadDisclosurePhase = "stable" | "entering" | "leaving";
|
||||||
|
|
||||||
export type DownloadDisclosureRow = DownloadLogicalRow & {
|
type DownloadItemLogicalRow = Extract<DownloadLogicalRow, { type: "item" }>;
|
||||||
|
|
||||||
|
type DownloadDisclosureMeta = {
|
||||||
disclosureOpacity: number;
|
disclosureOpacity: number;
|
||||||
disclosurePhase: DownloadDisclosurePhase;
|
disclosurePhase: DownloadDisclosurePhase;
|
||||||
};
|
};
|
||||||
|
|
||||||
function stableRow(row: DownloadLogicalRow): DownloadDisclosureRow {
|
export type DownloadDisclosureLogicalRow = DownloadLogicalRow & DownloadDisclosureMeta;
|
||||||
return { ...row, disclosureOpacity: 1, disclosurePhase: "stable" };
|
|
||||||
}
|
export type DownloadDisclosureGroupRow = DownloadVirtualRowInput & DownloadDisclosureMeta & {
|
||||||
|
type: "item-group";
|
||||||
|
packageId: string;
|
||||||
|
items: DownloadItemLogicalRow[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DownloadDisclosureRow = DownloadDisclosureLogicalRow | DownloadDisclosureGroupRow;
|
||||||
|
|
||||||
type DownloadDisclosureSourceRow = DownloadLogicalRow | DownloadDisclosureRow;
|
type DownloadDisclosureSourceRow = DownloadLogicalRow | DownloadDisclosureRow;
|
||||||
|
|
||||||
function disclosureOpacity(row: DownloadDisclosureSourceRow): number {
|
function stableRow(row: DownloadLogicalRow): DownloadDisclosureLogicalRow {
|
||||||
return "disclosureOpacity" in row ? row.disclosureOpacity : 1;
|
return { ...row, disclosureOpacity: 1, disclosurePhase: "stable" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemGroup(
|
||||||
|
packageId: string,
|
||||||
|
items: readonly DownloadItemLogicalRow[],
|
||||||
|
height: number,
|
||||||
|
phase: Exclude<DownloadDisclosurePhase, "stable">
|
||||||
|
): DownloadDisclosureGroupRow {
|
||||||
|
return {
|
||||||
|
type: "item-group",
|
||||||
|
id: `${packageId}:items`,
|
||||||
|
packageId,
|
||||||
|
items: items.map((row) => ({ ...row, height: DOWNLOAD_FILE_ROW_HEIGHT })),
|
||||||
|
height,
|
||||||
|
disclosureOpacity: 1,
|
||||||
|
disclosurePhase: phase
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stableDownloadDisclosureRows(rows: readonly DownloadLogicalRow[]): DownloadDisclosureRow[] {
|
export function stableDownloadDisclosureRows(rows: readonly DownloadLogicalRow[]): DownloadDisclosureRow[] {
|
||||||
return rows.map(stableRow);
|
return rows.map(stableRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemRowsByPackage(rows: readonly DownloadDisclosureSourceRow[]): Map<string, DownloadDisclosureSourceRow[]> {
|
function itemRowsByPackage(rows: readonly DownloadDisclosureSourceRow[]): Map<string, DownloadItemLogicalRow[]> {
|
||||||
const grouped = new Map<string, DownloadDisclosureSourceRow[]>();
|
const grouped = new Map<string, DownloadItemLogicalRow[]>();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
if (row.type === "item-group") {
|
||||||
|
grouped.set(row.packageId, row.items);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (row.type !== "item") continue;
|
if (row.type !== "item") continue;
|
||||||
const entries = grouped.get(row.packageId) ?? [];
|
const entries = grouped.get(row.packageId) ?? [];
|
||||||
entries.push(row);
|
entries.push(row);
|
||||||
@@ -56,6 +86,10 @@ function itemRowsByPackage(rows: readonly DownloadDisclosureSourceRow[]): Map<st
|
|||||||
return grouped;
|
return grouped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function groupRowsByPackage(rows: readonly DownloadDisclosureSourceRow[]): Map<string, DownloadDisclosureGroupRow> {
|
||||||
|
return new Map(rows.filter((row): row is DownloadDisclosureGroupRow => row.type === "item-group").map((row) => [row.packageId, row]));
|
||||||
|
}
|
||||||
|
|
||||||
export function prepareDownloadDisclosureTransition(
|
export function prepareDownloadDisclosureTransition(
|
||||||
current: readonly DownloadDisclosureSourceRow[],
|
current: readonly DownloadDisclosureSourceRow[],
|
||||||
desired: readonly DownloadLogicalRow[]
|
desired: readonly DownloadLogicalRow[]
|
||||||
@@ -65,14 +99,9 @@ export function prepareDownloadDisclosureTransition(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentItems = itemRowsByPackage(current);
|
const currentItems = itemRowsByPackage(current);
|
||||||
|
const currentGroups = groupRowsByPackage(current);
|
||||||
const currentPackages = new Map(current.filter((row) => row.type === "package").map((row) => [row.packageId, row]));
|
const currentPackages = new Map(current.filter((row) => row.type === "package").map((row) => [row.packageId, row]));
|
||||||
const desiredItems = new Map<string, DownloadLogicalRow[]>();
|
const desiredItems = itemRowsByPackage(desired);
|
||||||
for (const row of desired) {
|
|
||||||
if (row.type !== "item") continue;
|
|
||||||
const entries = desiredItems.get(row.packageId) ?? [];
|
|
||||||
entries.push(row);
|
|
||||||
desiredItems.set(row.packageId, entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
let animatedItemCount = 0;
|
let animatedItemCount = 0;
|
||||||
for (const row of desired) {
|
for (const row of desired) {
|
||||||
@@ -93,6 +122,7 @@ export function prepareDownloadDisclosureTransition(
|
|||||||
const before = currentItems.get(row.packageId) ?? [];
|
const before = currentItems.get(row.packageId) ?? [];
|
||||||
const after = desiredItems.get(row.packageId) ?? [];
|
const after = desiredItems.get(row.packageId) ?? [];
|
||||||
const previousPackage = currentPackages.get(row.packageId);
|
const previousPackage = currentPackages.get(row.packageId);
|
||||||
|
const previousGroup = currentGroups.get(row.packageId);
|
||||||
const disclosureChanged = previousPackage?.type === "package" && previousPackage.packageRow.collapsed !== row.packageRow.collapsed;
|
const disclosureChanged = previousPackage?.type === "package" && previousPackage.packageRow.collapsed !== row.packageRow.collapsed;
|
||||||
if (!disclosureChanged) {
|
if (!disclosureChanged) {
|
||||||
rows.push(...after.map(stableRow));
|
rows.push(...after.map(stableRow));
|
||||||
@@ -100,21 +130,12 @@ export function prepareDownloadDisclosureTransition(
|
|||||||
}
|
}
|
||||||
if (row.packageRow.collapsed && before.length > 0) {
|
if (row.packageRow.collapsed && before.length > 0) {
|
||||||
animated = true;
|
animated = true;
|
||||||
rows.push(...before.map((entry) => ({ ...entry, disclosureOpacity: disclosureOpacity(entry), disclosurePhase: "leaving" as const })));
|
rows.push(itemGroup(row.packageId, before, previousGroup?.height ?? before.length * DOWNLOAD_FILE_ROW_HEIGHT, "leaving"));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const beforeById = new Map(before.map((entry) => [entry.id, entry]));
|
if (after.length > 0) {
|
||||||
for (const entry of after) {
|
animated = true;
|
||||||
const previous = beforeById.get(entry.id);
|
rows.push(itemGroup(row.packageId, after, previousGroup?.height ?? 0, "entering"));
|
||||||
if (!previous) {
|
|
||||||
animated = true;
|
|
||||||
rows.push({ ...entry, height: 0, disclosureOpacity: 0, disclosurePhase: "entering" });
|
|
||||||
} else if (("disclosurePhase" in previous && previous.disclosurePhase === "leaving") || previous.height === 0) {
|
|
||||||
animated = true;
|
|
||||||
rows.push({ ...entry, height: previous.height, disclosureOpacity: disclosureOpacity(previous), disclosurePhase: "entering" });
|
|
||||||
} else {
|
|
||||||
rows.push(stableRow(entry));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { animated, rows };
|
return { animated, rows };
|
||||||
@@ -124,33 +145,59 @@ export function mergeDownloadDisclosureRows(
|
|||||||
transition: readonly DownloadDisclosureRow[],
|
transition: readonly DownloadDisclosureRow[],
|
||||||
desired: readonly DownloadLogicalRow[]
|
desired: readonly DownloadLogicalRow[]
|
||||||
): DownloadDisclosureRow[] {
|
): DownloadDisclosureRow[] {
|
||||||
const transitionById = new Map(transition.map((row) => [`${row.type}:${row.id}`, row]));
|
const transitionPackages = new Map(transition.filter((row) => row.type === "package").map((row) => [row.packageId, row]));
|
||||||
const desiredIds = new Set(desired.map((row) => `${row.type}:${row.id}`));
|
const transitionGroups = groupRowsByPackage(transition);
|
||||||
const leavingByPackage = new Map<string, DownloadDisclosureRow[]>();
|
const desiredItems = itemRowsByPackage(desired);
|
||||||
for (const row of transition) {
|
const desiredPackages = new Map(desired.filter((row) => row.type === "package").map((row) => [row.packageId, row]));
|
||||||
if (row.type !== "item" || row.disclosurePhase !== "leaving" || desiredIds.has(`item:${row.id}`)) continue;
|
const freshGroupItems = new Map<string, DownloadItemLogicalRow[]>();
|
||||||
const entries = leavingByPackage.get(row.packageId) ?? [];
|
let groupedItemCount = 0;
|
||||||
entries.push(row);
|
for (const group of transitionGroups.values()) {
|
||||||
leavingByPackage.set(row.packageId, entries);
|
const packageRow = desiredPackages.get(group.packageId);
|
||||||
|
if (!packageRow || packageRow.type !== "package") continue;
|
||||||
|
const items = desiredItems.get(group.packageId) ?? packageRow.packageRow.items.map((item): DownloadItemLogicalRow => ({
|
||||||
|
type: "item",
|
||||||
|
id: item.id,
|
||||||
|
packageId: item.packageId,
|
||||||
|
item,
|
||||||
|
height: DOWNLOAD_FILE_ROW_HEIGHT
|
||||||
|
}));
|
||||||
|
groupedItemCount += items.length;
|
||||||
|
if (groupedItemCount > DOWNLOAD_DISCLOSURE_MAX_ANIMATED_ITEMS) {
|
||||||
|
return stableDownloadDisclosureRows(desired);
|
||||||
|
}
|
||||||
|
freshGroupItems.set(group.packageId, items);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows: DownloadDisclosureRow[] = [];
|
const rows: DownloadDisclosureRow[] = [];
|
||||||
|
|
||||||
for (const row of desired) {
|
for (const row of desired) {
|
||||||
const animated = transitionById.get(`${row.type}:${row.id}`);
|
if (row.type !== "package") continue;
|
||||||
rows.push(animated
|
const animatedPackage = transitionPackages.get(row.packageId);
|
||||||
? { ...row, height: animated.height, disclosureOpacity: animated.disclosureOpacity, disclosurePhase: animated.disclosurePhase }
|
rows.push(animatedPackage?.type === "package"
|
||||||
|
? { ...row, height: animatedPackage.height, disclosureOpacity: animatedPackage.disclosureOpacity, disclosurePhase: animatedPackage.disclosurePhase }
|
||||||
: stableRow(row));
|
: stableRow(row));
|
||||||
if (row.type === "package") rows.push(...(leavingByPackage.get(row.packageId) ?? []));
|
|
||||||
|
const group = transitionGroups.get(row.packageId);
|
||||||
|
if (!group) {
|
||||||
|
rows.push(...(desiredItems.get(row.packageId) ?? []).map(stableRow));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const freshItems = freshGroupItems.get(row.packageId) ?? [];
|
||||||
|
rows.push({
|
||||||
|
...group,
|
||||||
|
items: freshItems,
|
||||||
|
height: group.height > 0 ? freshItems.length * DOWNLOAD_FILE_ROW_HEIGHT : 0
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function activateDownloadDisclosureTransition(rows: readonly DownloadDisclosureRow[]): DownloadDisclosureRow[] {
|
export function activateDownloadDisclosureTransition(rows: readonly DownloadDisclosureRow[]): DownloadDisclosureRow[] {
|
||||||
return rows.map((row) => {
|
return rows.map((row) => {
|
||||||
if (row.type !== "item" || row.disclosurePhase === "stable") return row;
|
if (row.type !== "item-group") return row;
|
||||||
return row.disclosurePhase === "entering"
|
return row.disclosurePhase === "entering"
|
||||||
? { ...row, height: DOWNLOAD_FILE_ROW_HEIGHT, disclosureOpacity: 1 }
|
? { ...row, height: row.items.length * DOWNLOAD_FILE_ROW_HEIGHT }
|
||||||
: { ...row, height: 0, disclosureOpacity: 0 };
|
: { ...row, height: 0 };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -306,6 +306,10 @@
|
|||||||
height: var(--downloads-virtual-row-height);
|
height: var(--downloads-virtual-row-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.downloads-disclosure-item-group {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.downloads-package-card {
|
.downloads-package-card {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ describe("virtualisierte Paketanimation", () => {
|
|||||||
expect(cancelled).toContain(2);
|
expect(cancelled).toContain(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fährt neue Unterzeilen von null auf ihre feste Höhe aus und verschiebt Folgepakete unter stabilen IDs", () => {
|
it("fährt einen gemeinsamen Paketbereich aus und lässt seine Unterzeilen auf fester Höhe", () => {
|
||||||
const collapsed = logicalRows([packageA.id]);
|
const collapsed = logicalRows([packageA.id]);
|
||||||
const expanded = logicalRows([]);
|
const expanded = logicalRows([]);
|
||||||
const prepared = prepareDownloadDisclosureTransition(stableDownloadDisclosureRows(collapsed), expanded);
|
const prepared = prepareDownloadDisclosureTransition(stableDownloadDisclosureRows(collapsed), expanded);
|
||||||
@@ -261,15 +261,18 @@ describe("virtualisierte Paketanimation", () => {
|
|||||||
expect(prepared.animated).toBe(true);
|
expect(prepared.animated).toBe(true);
|
||||||
expect(prepared.rows.map((row) => [row.id, row.height, row.disclosurePhase, row.disclosureOpacity])).toEqual([
|
expect(prepared.rows.map((row) => [row.id, row.height, row.disclosurePhase, row.disclosureOpacity])).toEqual([
|
||||||
["package-a", 40, "stable", 1],
|
["package-a", 40, "stable", 1],
|
||||||
["a-1", 0, "entering", 0],
|
["package-a:items", 0, "entering", 1],
|
||||||
["a-2", 0, "entering", 0],
|
|
||||||
["package-b", 40, "stable", 1],
|
["package-b", 40, "stable", 1],
|
||||||
["b-1", 38, "stable", 1]
|
["b-1", 38, "stable", 1]
|
||||||
]);
|
]);
|
||||||
|
const preparedGroup = prepared.rows.find((row) => row.type === "item-group");
|
||||||
|
expect(preparedGroup?.type === "item-group" ? preparedGroup.items.map((row) => [row.id, row.height]) : []).toEqual([
|
||||||
|
["a-1", 38],
|
||||||
|
["a-2", 38]
|
||||||
|
]);
|
||||||
expect(activateDownloadDisclosureTransition(prepared.rows).map((row) => [row.id, row.height, row.disclosureOpacity])).toEqual([
|
expect(activateDownloadDisclosureTransition(prepared.rows).map((row) => [row.id, row.height, row.disclosureOpacity])).toEqual([
|
||||||
["package-a", 40, 1],
|
["package-a", 40, 1],
|
||||||
["a-1", 38, 1],
|
["package-a:items", 76, 1],
|
||||||
["a-2", 38, 1],
|
|
||||||
["package-b", 40, 1],
|
["package-b", 40, 1],
|
||||||
["b-1", 38, 1]
|
["b-1", 38, 1]
|
||||||
]);
|
]);
|
||||||
@@ -283,22 +286,22 @@ describe("virtualisierte Paketanimation", () => {
|
|||||||
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.downloads-virtual-row\s*\{[^}]*transition-duration:\s*1500ms, 1500ms, 1500ms !important;/s);
|
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.downloads-virtual-row\s*\{[^}]*transition-duration:\s*1500ms, 1500ms, 1500ms !important;/s);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("behält ausfahrende Unterzeilen bis zum Animationsende und reduziert ihre Höhe auf null", () => {
|
it("schneidet beim Einfahren den gemeinsamen Paketbereich ab statt Unterzeilen zusammenzuziehen", () => {
|
||||||
const expanded = logicalRows([]);
|
const expanded = logicalRows([]);
|
||||||
const collapsed = logicalRows([packageA.id]);
|
const collapsed = logicalRows([packageA.id]);
|
||||||
const prepared = prepareDownloadDisclosureTransition(stableDownloadDisclosureRows(expanded), collapsed);
|
const prepared = prepareDownloadDisclosureTransition(stableDownloadDisclosureRows(expanded), collapsed);
|
||||||
|
|
||||||
expect(prepared.rows.map((row) => [row.id, row.height, row.disclosurePhase, row.disclosureOpacity])).toEqual([
|
expect(prepared.rows.map((row) => [row.id, row.height, row.disclosurePhase, row.disclosureOpacity])).toEqual([
|
||||||
["package-a", 40, "stable", 1],
|
["package-a", 40, "stable", 1],
|
||||||
["a-1", 38, "leaving", 1],
|
["package-a:items", 76, "leaving", 1],
|
||||||
["a-2", 38, "leaving", 1],
|
|
||||||
["package-b", 40, "stable", 1],
|
["package-b", 40, "stable", 1],
|
||||||
["b-1", 38, "stable", 1]
|
["b-1", 38, "stable", 1]
|
||||||
]);
|
]);
|
||||||
|
const preparedGroup = prepared.rows.find((row) => row.type === "item-group");
|
||||||
|
expect(preparedGroup?.type === "item-group" ? preparedGroup.items.map((row) => row.height) : []).toEqual([38, 38]);
|
||||||
expect(activateDownloadDisclosureTransition(prepared.rows).map((row) => [row.id, row.height, row.disclosureOpacity])).toEqual([
|
expect(activateDownloadDisclosureTransition(prepared.rows).map((row) => [row.id, row.height, row.disclosureOpacity])).toEqual([
|
||||||
["package-a", 40, 1],
|
["package-a", 40, 1],
|
||||||
["a-1", 0, 0],
|
["package-a:items", 0, 1],
|
||||||
["a-2", 0, 0],
|
|
||||||
["package-b", 40, 1],
|
["package-b", 40, 1],
|
||||||
["b-1", 38, 1]
|
["b-1", 38, 1]
|
||||||
]);
|
]);
|
||||||
@@ -402,15 +405,59 @@ describe("virtualisierte Paketanimation", () => {
|
|||||||
const entering = activateDownloadDisclosureTransition(prepareDownloadDisclosureTransition(stableDownloadDisclosureRows(logicalRows([packageA.id])), logicalRows([])).rows);
|
const entering = activateDownloadDisclosureTransition(prepareDownloadDisclosureTransition(stableDownloadDisclosureRows(logicalRows([packageA.id])), logicalRows([])).rows);
|
||||||
const updated = logicalRows([]).map((row) => row.type === "item" && row.id === "a-1" ? { ...row, item: { ...row.item, downloadedBytes: 987_654_321 } } : row);
|
const updated = logicalRows([]).map((row) => row.type === "item" && row.id === "a-1" ? { ...row, item: { ...row.item, downloadedBytes: 987_654_321 } } : row);
|
||||||
const merged = mergeDownloadDisclosureRows(entering, updated);
|
const merged = mergeDownloadDisclosureRows(entering, updated);
|
||||||
const updatedItem = merged.find((row) => row.type === "item" && row.id === "a-1");
|
const enteringGroup = merged.find((row) => row.type === "item-group");
|
||||||
expect(updatedItem?.type === "item" ? updatedItem.item.downloadedBytes : 0).toBe(987_654_321);
|
const updatedItem = enteringGroup?.type === "item-group" ? enteringGroup.items.find((row) => row.id === "a-1") : null;
|
||||||
expect(updatedItem).toEqual(expect.objectContaining({ disclosurePhase: "entering", height: 38, disclosureOpacity: 1 }));
|
expect(updatedItem?.item.downloadedBytes).toBe(987_654_321);
|
||||||
|
expect(enteringGroup).toEqual(expect.objectContaining({ disclosurePhase: "entering", height: 76, disclosureOpacity: 1 }));
|
||||||
|
|
||||||
const collapsed = logicalRows([packageA.id]);
|
const collapsed = logicalRows([packageA.id]);
|
||||||
const leaving = activateDownloadDisclosureTransition(prepareDownloadDisclosureTransition(stableDownloadDisclosureRows(logicalRows([])), collapsed).rows);
|
const leaving = activateDownloadDisclosureTransition(prepareDownloadDisclosureTransition(stableDownloadDisclosureRows(logicalRows([])), collapsed).rows);
|
||||||
const reversed = prepareDownloadDisclosureTransition(leaving, logicalRows([]));
|
const reversed = prepareDownloadDisclosureTransition(leaving, logicalRows([]));
|
||||||
expect(reversed.animated).toBe(true);
|
expect(reversed.animated).toBe(true);
|
||||||
expect(reversed.rows.filter((row) => row.packageId === packageA.id && row.type === "item").every((row) => row.disclosurePhase === "entering")).toBe(true);
|
expect(reversed.rows.find((row) => row.packageId === packageA.id && row.type === "item-group")).toEqual(expect.objectContaining({ disclosurePhase: "entering", height: 0 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gleicht die Unterzeilenmenge einer laufenden Paketbewegung sofort mit dem aktuellen Modell ab", () => {
|
||||||
|
const entering = activateDownloadDisclosureTransition(prepareDownloadDisclosureTransition(stableDownloadDisclosureRows(logicalRows([packageA.id])), logicalRows([])).rows);
|
||||||
|
const current = logicalRows([]);
|
||||||
|
const replacement = item("a-3", packageA.id, "queued");
|
||||||
|
const updated = current.flatMap((row) => {
|
||||||
|
if (row.type === "item" && row.id === "a-2") {
|
||||||
|
return [{ ...row, id: replacement.id, item: replacement }];
|
||||||
|
}
|
||||||
|
return [row];
|
||||||
|
});
|
||||||
|
const merged = mergeDownloadDisclosureRows(entering, updated);
|
||||||
|
const group = merged.find((row) => row.type === "item-group");
|
||||||
|
|
||||||
|
expect(group?.type === "item-group" ? group.items.map((row) => row.id) : []).toEqual(["a-1", "a-3"]);
|
||||||
|
expect(group).toEqual(expect.objectContaining({ height: 76 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("bricht eine laufende Gruppenbewegung ab wenn ein Filterwechsel die Virtualisierungsgrenze überschreitet", () => {
|
||||||
|
const entering = activateDownloadDisclosureTransition(prepareDownloadDisclosureTransition(stableDownloadDisclosureRows(logicalRows([packageA.id])), logicalRows([])).rows);
|
||||||
|
const itemIds = Array.from({ length: DOWNLOAD_DISCLOSURE_MAX_ANIMATED_ITEMS + 100 }, (_, index) => `expanded-${index}`);
|
||||||
|
const expandedPackage = pkg(packageA.id, packageA.name, itemIds);
|
||||||
|
const expandedItems = Object.fromEntries(itemIds.map((id) => [id, item(id, packageA.id, "queued")]));
|
||||||
|
const expandedRows = buildDownloadLogicalRows(buildDownloadsViewModel({
|
||||||
|
packageOrder: [expandedPackage.id],
|
||||||
|
packages: { [expandedPackage.id]: expandedPackage },
|
||||||
|
items: expandedItems,
|
||||||
|
displayMode: "packages",
|
||||||
|
filter: "all",
|
||||||
|
providerFilter: "all",
|
||||||
|
query: "",
|
||||||
|
collapsedPackageIds: [],
|
||||||
|
selectedIds: [],
|
||||||
|
hideExtractedItems: false,
|
||||||
|
showAllPackages: true,
|
||||||
|
renderLimit: itemIds.length + 1
|
||||||
|
}));
|
||||||
|
const merged = mergeDownloadDisclosureRows(entering, expandedRows);
|
||||||
|
const window = calculateDownloadVirtualWindow(merged, { scrollTop: 0, viewportHeight: 720, overscan: 8 });
|
||||||
|
|
||||||
|
expect(merged.some((row) => row.type === "item-group")).toBe(false);
|
||||||
|
expect(window.rows.length).toBeLessThan(40);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user