fix(downloads): make package disclosure motion visible
Keep the explicitly requested 300 ms package transition active when Chromium reports reduced motion, and separate the prepared and active disclosure states by a painted frame. Add focused regression coverage and an opt-in motion mode for the visual harness while keeping normal captures deterministic.
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
|||||||
DOWNLOAD_DISCLOSURE_DURATION_MS,
|
DOWNLOAD_DISCLOSURE_DURATION_MS,
|
||||||
mergeDownloadDisclosureRows,
|
mergeDownloadDisclosureRows,
|
||||||
prepareDownloadDisclosureTransition,
|
prepareDownloadDisclosureTransition,
|
||||||
|
scheduleDownloadDisclosureActivation,
|
||||||
type DownloadDisclosureRow
|
type DownloadDisclosureRow
|
||||||
} from "./download-disclosure-transition";
|
} from "./download-disclosure-transition";
|
||||||
import type { DownloadsViewActions, DownloadsViewModel } from "./DownloadsView";
|
import type { DownloadsViewActions, DownloadsViewModel } from "./DownloadsView";
|
||||||
@@ -114,7 +115,7 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D
|
|||||||
transitionRowsRef.current = prepared.rows;
|
transitionRowsRef.current = prepared.rows;
|
||||||
setTransitionRows(prepared.rows);
|
setTransitionRows(prepared.rows);
|
||||||
let settleTimer = 0;
|
let settleTimer = 0;
|
||||||
const animationFrame = window.requestAnimationFrame(() => {
|
const cancelActivation = scheduleDownloadDisclosureActivation(window.requestAnimationFrame.bind(window), window.cancelAnimationFrame.bind(window), () => {
|
||||||
const active = activateDownloadDisclosureTransition(transitionRowsRef.current ?? prepared.rows);
|
const active = activateDownloadDisclosureTransition(transitionRowsRef.current ?? prepared.rows);
|
||||||
transitionRowsRef.current = active;
|
transitionRowsRef.current = active;
|
||||||
setTransitionRows(active);
|
setTransitionRows(active);
|
||||||
@@ -125,7 +126,7 @@ export function VirtualizedDownloadsBody({ actions, model, state }: { actions: D
|
|||||||
}, DOWNLOAD_DISCLOSURE_DURATION_MS);
|
}, DOWNLOAD_DISCLOSURE_DURATION_MS);
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
window.cancelAnimationFrame(animationFrame);
|
cancelActivation();
|
||||||
if (settleTimer) window.clearTimeout(settleTimer);
|
if (settleTimer) window.clearTimeout(settleTimer);
|
||||||
};
|
};
|
||||||
}, [model.disclosureRevision, model.displayMode]);
|
}, [model.disclosureRevision, model.displayMode]);
|
||||||
|
|||||||
@@ -4,6 +4,22 @@ import { DOWNLOAD_FILE_ROW_HEIGHT } 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;
|
||||||
|
|
||||||
|
export function scheduleDownloadDisclosureActivation(
|
||||||
|
requestFrame: (callback: FrameRequestCallback) => number,
|
||||||
|
cancelFrame: (frame: number) => void,
|
||||||
|
activate: () => void
|
||||||
|
): () => void {
|
||||||
|
let paintFrame = 0;
|
||||||
|
let activationFrame = 0;
|
||||||
|
paintFrame = requestFrame(() => {
|
||||||
|
activationFrame = requestFrame(activate);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelFrame(paintFrame);
|
||||||
|
if (activationFrame) cancelFrame(activationFrame);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type DownloadDisclosurePhase = "stable" | "entering" | "leaving";
|
export type DownloadDisclosurePhase = "stable" | "entering" | "leaving";
|
||||||
|
|
||||||
export type DownloadDisclosureRow = DownloadLogicalRow & {
|
export type DownloadDisclosureRow = DownloadLogicalRow & {
|
||||||
|
|||||||
@@ -583,6 +583,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.downloads-virtual-spacer {
|
||||||
|
transition-duration: 300ms !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloads-virtual-row {
|
||||||
|
transition-duration: 300ms, 300ms, 300ms !important;
|
||||||
|
}
|
||||||
|
|
||||||
.downloads-table.is-column-drag-active [data-download-column],
|
.downloads-table.is-column-drag-active [data-download-column],
|
||||||
.downloads-table.is-column-drag-settling [data-download-column] {
|
.downloads-table.is-column-drag-settling [data-download-column] {
|
||||||
transition-duration: 220ms !important;
|
transition-duration: 220ms !important;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
activateDownloadDisclosureTransition,
|
activateDownloadDisclosureTransition,
|
||||||
mergeDownloadDisclosureRows,
|
mergeDownloadDisclosureRows,
|
||||||
prepareDownloadDisclosureTransition,
|
prepareDownloadDisclosureTransition,
|
||||||
|
scheduleDownloadDisclosureActivation,
|
||||||
stableDownloadDisclosureRows
|
stableDownloadDisclosureRows
|
||||||
} from "../src/renderer/views/downloads/download-disclosure-transition";
|
} from "../src/renderer/views/downloads/download-disclosure-transition";
|
||||||
import {
|
import {
|
||||||
@@ -224,6 +225,32 @@ describe("virtualisierte Paketanimation", () => {
|
|||||||
renderLimit: 100
|
renderLimit: 100
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it("zeichnet den Startzustand in einem eigenen Frame vor der Aktivierung", () => {
|
||||||
|
const frames: FrameRequestCallback[] = [];
|
||||||
|
const cancelled: number[] = [];
|
||||||
|
const activated = vi.fn();
|
||||||
|
let frameId = 0;
|
||||||
|
const requestFrame = vi.fn((callback: FrameRequestCallback) => {
|
||||||
|
frames.push(callback);
|
||||||
|
frameId += 1;
|
||||||
|
return frameId;
|
||||||
|
});
|
||||||
|
const cancelFrame = vi.fn((frame: number) => cancelled.push(frame));
|
||||||
|
|
||||||
|
const cancel = scheduleDownloadDisclosureActivation(requestFrame, cancelFrame, activated);
|
||||||
|
|
||||||
|
expect(activated).not.toHaveBeenCalled();
|
||||||
|
expect(frames).toHaveLength(1);
|
||||||
|
frames.shift()?.(16);
|
||||||
|
expect(activated).not.toHaveBeenCalled();
|
||||||
|
expect(frames).toHaveLength(1);
|
||||||
|
frames.shift()?.(32);
|
||||||
|
expect(activated).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
cancel();
|
||||||
|
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 neue Unterzeilen von null auf ihre feste Höhe aus und verschiebt Folgepakete unter stabilen IDs", () => {
|
||||||
const collapsed = logicalRows([packageA.id]);
|
const collapsed = logicalRows([packageA.id]);
|
||||||
const expanded = logicalRows([]);
|
const expanded = logicalRows([]);
|
||||||
@@ -246,6 +273,13 @@ describe("virtualisierte Paketanimation", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("behält die ausdrücklich gewünschte Paketbewegung auch bei reduzierten Systemanimationen bei", () => {
|
||||||
|
const css = fs.readFileSync(path.join(process.cwd(), "src/renderer/views/downloads/downloads.css"), "utf8");
|
||||||
|
|
||||||
|
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.downloads-virtual-spacer\s*\{[^}]*transition-duration:\s*300ms !important;/s);
|
||||||
|
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.downloads-virtual-row\s*\{[^}]*transition-duration:\s*300ms, 300ms, 300ms !important;/s);
|
||||||
|
});
|
||||||
|
|
||||||
it("behält ausfahrende Unterzeilen bis zum Animationsende und reduziert ihre Höhe auf null", () => {
|
it("behält ausfahrende Unterzeilen bis zum Animationsende und reduziert ihre Höhe auf null", () => {
|
||||||
const expanded = logicalRows([]);
|
const expanded = logicalRows([]);
|
||||||
const collapsed = logicalRows([packageA.id]);
|
const collapsed = logicalRows([packageA.id]);
|
||||||
|
|||||||
@@ -4,10 +4,13 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Multi Debrid Downloader Visual Harness</title>
|
<title>Multi Debrid Downloader Visual Harness</title>
|
||||||
|
<script>
|
||||||
|
if (new URLSearchParams(location.search).has("motion")) document.documentElement.dataset.visualMotion = "true";
|
||||||
|
</script>
|
||||||
<style>
|
<style>
|
||||||
*,
|
html:not([data-visual-motion="true"]) *,
|
||||||
*::before,
|
html:not([data-visual-motion="true"]) *::before,
|
||||||
*::after {
|
html:not([data-visual-motion="true"]) *::after {
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
caret-color: transparent !important;
|
caret-color: transparent !important;
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user