Handle interrupted navigation transitions
This commit is contained in:
@@ -2,8 +2,7 @@
|
|||||||
// browser's View Transitions API so route changes cross-fade cleanly and
|
// browser's View Transitions API so route changes cross-fade cleanly and
|
||||||
// shared-element transitions (Fleet card → Server detail hero) morph.
|
// shared-element transitions (Fleet card → Server detail hero) morph.
|
||||||
//
|
//
|
||||||
// Chromium ships the API today; Firefox and Safari fall through to a plain
|
// Browsers without the API use ordinary navigation.
|
||||||
// navigation — same behavior as before, just without the transition.
|
|
||||||
|
|
||||||
// Structural type — accepts whatever SvelteKit passes to onNavigate without
|
// Structural type — accepts whatever SvelteKit passes to onNavigate without
|
||||||
// coupling to a specific @sveltejs/kit version's type name.
|
// coupling to a specific @sveltejs/kit version's type name.
|
||||||
@@ -13,15 +12,20 @@ export function enableViewTransitions(navigation: NavigationLike): Promise<void>
|
|||||||
// Feature-detect without a custom Document extension — the DOM lib type
|
// Feature-detect without a custom Document extension — the DOM lib type
|
||||||
// varies between TS versions, and any narrowing has to survive both.
|
// varies between TS versions, and any narrowing has to survive both.
|
||||||
const start = (document as unknown as {
|
const start = (document as unknown as {
|
||||||
startViewTransition?: (cb: () => void | Promise<void>) => unknown;
|
startViewTransition?: (cb: () => void | Promise<void>) => { ready: Promise<void>; finished: Promise<void> };
|
||||||
}).startViewTransition;
|
}).startViewTransition;
|
||||||
|
|
||||||
if (typeof start !== 'function') return;
|
if (typeof start !== 'function') return;
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
start.call(document, async () => {
|
try {
|
||||||
resolve();
|
const transition = start.call(document, async () => {
|
||||||
await navigation.complete;
|
resolve();
|
||||||
});
|
// Another navigation can supersede this one during its animation.
|
||||||
|
await navigation.complete.catch(() => {});
|
||||||
|
});
|
||||||
|
void transition.ready.catch(() => {});
|
||||||
|
void transition.finished.catch(() => {}).finally(resolve);
|
||||||
|
} catch { resolve(); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { afterEach, expect, it, vi } from 'vitest';
|
||||||
|
import { enableViewTransitions } from '../../src/lib/ui/motion/viewTransition.js';
|
||||||
|
|
||||||
|
afterEach(() => vi.unstubAllGlobals());
|
||||||
|
|
||||||
|
it('allows navigation to continue when a transition is skipped or superseded', async () => {
|
||||||
|
let finish: () => void = () => {};
|
||||||
|
const finished = new Promise<void>(resolve => { finish = resolve; });
|
||||||
|
const complete = Promise.reject(new Error('navigation aborted'));
|
||||||
|
vi.stubGlobal('document', { startViewTransition: (update: () => Promise<void>) => {
|
||||||
|
void update().then(finish);
|
||||||
|
return { ready: Promise.reject(new DOMException('Skipped transition', 'AbortError')), finished };
|
||||||
|
} });
|
||||||
|
await enableViewTransitions({ complete });
|
||||||
|
await finished;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('continues without animation when the browser cannot start a transition', async () => {
|
||||||
|
vi.stubGlobal('document', { startViewTransition: () => { throw new Error('Unavailable'); } });
|
||||||
|
await enableViewTransitions({ complete: Promise.resolve() });
|
||||||
|
vi.stubGlobal('document', {});
|
||||||
|
expect(enableViewTransitions({ complete: Promise.resolve() })).toBeUndefined();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user