Stabilize collector scrolling for large package sets

Reserve each offscreen collector package with its exact expanded or collapsed block height so content-visibility cannot collapse flex items to zero and rebuild the scroll geometry while moving through the list. Add a regression test for the rendered intrinsic block-size contract.
This commit is contained in:
Sucukdeluxe
2026-08-26 14:15:43 +02:00
parent da4c2fb069
commit aea0839b08
3 changed files with 20 additions and 3 deletions
+10 -2
View File
@@ -1,4 +1,4 @@
import { memo, useEffect, useState, type ChangeEvent, type ReactElement } from "react"; import { memo, useEffect, useState, type ChangeEvent, type CSSProperties, type ReactElement } from "react";
import { formatDateTime, formatHosterLabel, humanSize } from "../../download-format"; import { formatDateTime, formatHosterLabel, humanSize } from "../../download-format";
import { DataTable, DataTableBody, DataTableEmpty, DataTableHeader } from "../../ui/DataTable"; import { DataTable, DataTableBody, DataTableEmpty, DataTableHeader } from "../../ui/DataTable";
import { Dialog } from "../../ui/Dialog"; import { Dialog } from "../../ui/Dialog";
@@ -81,6 +81,10 @@ function linkAvailability(availability: "online" | "offline" | "unknown"): strin
return "Ungeprüft"; return "Ungeprüft";
} }
export function collectorPackageIntrinsicBlockSize(row: CollectorWorkspacePackageRow): number {
return 46 + (row.collapsed ? 0 : row.links.length * 40);
}
export function toggleAllCollectorPackageIds( export function toggleAllCollectorPackageIds(
packageIds: readonly string[], packageIds: readonly string[],
collapsedPackageIds: ReadonlySet<string> collapsedPackageIds: ReadonlySet<string>
@@ -166,6 +170,10 @@ function CollectorPackageGroup({ row, model, actions, selected }: {
const partiallySelected = row.selectedCount > 0 && !allSelected; const partiallySelected = row.selectedCount > 0 && !allSelected;
const animateItems = model.animationsEnabled && row.allLinks.length <= 64; const animateItems = model.animationsEnabled && row.allLinks.length <= 64;
const [renderItems, setRenderItems] = useState(!row.collapsed); const [renderItems, setRenderItems] = useState(!row.collapsed);
const packageStyle: CSSProperties = {
containIntrinsicBlockSize: `auto ${collectorPackageIntrinsicBlockSize(row)}px`,
flex: "0 0 auto"
};
useEffect(() => { useEffect(() => {
if (!row.collapsed) { if (!row.collapsed) {
setRenderItems(true); setRenderItems(true);
@@ -179,7 +187,7 @@ function CollectorPackageGroup({ row, model, actions, selected }: {
return () => window.clearTimeout(timer); return () => window.clearTimeout(timer);
}, [animateItems, row.collapsed]); }, [animateItems, row.collapsed]);
return ( return (
<div className={`collector-package-group${row.collapsed ? " is-collapsed" : ""}${model.animationsEnabled ? " is-motion-enabled" : ""}`} role="rowgroup"> <div className={`collector-package-group${row.collapsed ? " is-collapsed" : ""}${model.animationsEnabled ? " is-motion-enabled" : ""}`} role="rowgroup" style={packageStyle}>
<div className={`collector-package-row${row.selectedCount > 0 ? " is-selected" : ""}`} role="row"> <div className={`collector-package-row${row.selectedCount > 0 ? " is-selected" : ""}`} role="row">
<span className="collector-column-select" role="cell"> <span className="collector-column-select" role="cell">
<input <input
@@ -216,7 +216,6 @@
} }
.collector-package-group { .collector-package-group {
contain-intrinsic-size: 46px 654px;
content-visibility: auto; content-visibility: auto;
} }
+10
View File
@@ -18,6 +18,7 @@ import {
CollectorSidebar, CollectorSidebar,
CollectorToolbar, CollectorToolbar,
CollectorView, CollectorView,
collectorPackageIntrinsicBlockSize,
toggleAllCollectorPackageIds, toggleAllCollectorPackageIds,
type CollectorViewActions type CollectorViewActions
} from "../src/renderer/views/collector/CollectorView"; } from "../src/renderer/views/collector/CollectorView";
@@ -286,6 +287,15 @@ describe("collector workspace model", () => {
}); });
describe("CollectorView", () => { describe("CollectorView", () => {
it("reserves the exact package height while content visibility skips offscreen rows", () => {
const expanded = buildCollectorWorkspaceViewModel([packages[0]], "all", "", false, [], [], "", true).packages[0];
const collapsed = { ...expanded, collapsed: true };
expect(collectorPackageIntrinsicBlockSize(expanded)).toBe(126);
expect(collectorPackageIntrinsicBlockSize(collapsed)).toBe(46);
expect(renderToStaticMarkup(<CollectorContent actions={createActions()} model={buildCollectorWorkspaceViewModel([packages[0]], "all", "", false, [], [], "", true)} />)).toContain("contain-intrinsic-block-size:auto 126px");
});
it("renders expandable package and file rows with preview columns", () => { it("renders expandable package and file rows with preview columns", () => {
const html = renderToStaticMarkup(<CollectorView actions={createActions()} model={buildCollectorWorkspaceViewModel(packages, "all", "", false, [], [], "", true)} />); const html = renderToStaticMarkup(<CollectorView actions={createActions()} model={buildCollectorWorkspaceViewModel(packages, "all", "", false, [], [], "", true)} />);