fix(downloads): make column reorder frames atomic

Commit the target grid immediately, derive track definitions from the same column order as the cells, and animate only cell contents with FLIP. Disable grid-track interpolation to avoid Chromium reduced-motion mix frames, honor the application animation setting, and retain serialized persistence and import authority.
This commit is contained in:
Sucukdeluxe
2026-08-20 09:16:06 +02:00
parent 32fc1e7321
commit 0dfb57525a
7 changed files with 205 additions and 67 deletions
@@ -36,10 +36,21 @@ function HosterLabels({ labels }: { labels: HosterLabel[] }): ReactElement {
);
}
function downloadGridTemplate(gridTemplate: string): string {
export function downloadGridTemplate(gridTemplate: string): string {
return `${DOWNLOAD_SELECTION_COLUMN_WIDTH} ${gridTemplate} ${DOWNLOAD_ACTION_COLUMN_WIDTH}`;
}
export function downloadGridTemplateForOrder(columnOrder: readonly string[]): string {
return downloadGridTemplate(columnOrder.map((column) => downloadColumnDefinitions[column]?.width ?? "100px").join(" "));
}
export function applyDownloadGridTemplate(root: HTMLElement, columnOrder: readonly string[]): void {
const value = downloadGridTemplateForOrder(columnOrder);
root.querySelectorAll<HTMLElement>(".downloads-table-header, .downloads-package-row, .downloads-item-row").forEach((row) => {
row.style.gridTemplateColumns = value;
});
}
function isPackageRowDisclosureExcluded(target: EventTarget | null): boolean {
const closest = (target as { closest?: (selector: string) => Element | null } | null)?.closest;
return typeof closest === "function" && closest.call(target, PACKAGE_ROW_DISCLOSURE_EXCLUSION_SELECTOR) !== null;
@@ -260,7 +271,7 @@ export function ItemRowContent({ item, selected, sessionRunning = true, columnOr
className={`downloads-item-row${selected ? " is-selected" : ""}`}
data-download-row-id={item.id}
role="row"
style={{ gridTemplateColumns: downloadGridTemplate(gridTemplate) }}
style={{ gridTemplateColumns: downloadGridTemplateForOrder(columnOrder) }}
onClick={(event) => {
event.stopPropagation();
actions.onToggleSelection(item.id, event.ctrlKey || event.metaKey, event.shiftKey);
@@ -460,7 +471,7 @@ export function PackageCardContent({ row, selectedIds, editing, editingName, pac
className="downloads-package-row"
data-download-row-id={entry.id}
role="row"
style={{ gridTemplateColumns: downloadGridTemplate(gridTemplate) }}
style={{ gridTemplateColumns: downloadGridTemplateForOrder(columnOrder) }}
onClick={(event) => {
if (event.detail > 1) return;
if (!event.ctrlKey && !event.metaKey && !event.shiftKey && selectedIds.size === 1 && selectedIds.has(entry.id)) return;
@@ -533,7 +544,7 @@ export function DownloadsTableHeader({ actions, columnOrder, gridTemplate, sortC
const allSelected = visibleIds.length > 0 && selectedCount === visibleIds.length;
const mixedSelection = selectedCount > 0 && selectedCount < visibleIds.length;
return (
<div className="downloads-table-header" role="row" style={{ gridTemplateColumns: downloadGridTemplate(gridTemplate) }}>
<div className="downloads-table-header" role="row" style={{ gridTemplateColumns: downloadGridTemplateForOrder(columnOrder) }}>
<span className="downloads-selection-cell" role="columnheader"><input aria-checked={mixedSelection ? "mixed" : allSelected} aria-label="Alle sichtbaren Downloads auswählen" checked={allSelected} onChange={(event) => actions.onSetVisibleSelection(visibleIds, event.target.checked)} ref={(input) => { if (input) input.indeterminate = mixedSelection; }} type="checkbox" /></span>
{columnOrder.map((column, index) => {
const definition = downloadColumnDefinitions[column];