StackedRouter: hero animation support

This commit is contained in:
thislight 2024-11-17 17:37:42 +08:00
parent 63fe4acc98
commit 1641f3e75b
No known key found for this signature in database
GPG key ID: FCFE5192241CCD4E
2 changed files with 54 additions and 18 deletions

View file

@ -24,12 +24,8 @@ dialog.StackedPage {
background: var(--tutu-color-surface);
box-shadow: var(--tutu-shadow-e16);
@media (min-width: 560px) {
& {
left: 50%;
transform: translateX(-50%);
}
}
margin-left: auto;
margin-right: auto;
@media (max-width: 560px) {
& {
@ -50,4 +46,12 @@ dialog.StackedPage {
&::backdrop {
background: none;
}
&.animating {
overflow: hidden;
* {
overflow: hidden;
}
}
}

View file

@ -6,6 +6,7 @@ import {
createRenderEffect,
createUniqueId,
Index,
onMount,
Show,
untrack,
useContext,
@ -15,9 +16,7 @@ import { createStore, unwrap } from "solid-js/store";
import "./StackedRouter.css";
import { animateSlideInFromRight, animateSlideOutToRight } from "./anim";
import { ANIM_CURVE_DECELERATION, ANIM_CURVE_STD } from "../material/theme";
import {
makeEventListener,
} from "@solid-primitives/event-listener";
import { makeEventListener } from "@solid-primitives/event-listener";
export type StackedRouterProps = Omit<RouterProps, "url">;
@ -25,6 +24,9 @@ export type StackFrame = {
path: string;
rootId: string;
state: unknown;
animateOpen?: (element: HTMLElement) => Animation;
animateClose?: (element: HTMLElement) => Animation;
};
export type NewFrameOptions<T> = (T extends undefined
@ -33,6 +35,8 @@ export type NewFrameOptions<T> = (T extends undefined
}
: { state: T }) & {
replace?: boolean;
animateOpen?: StackFrame["animateOpen"];
animateClose?: StackFrame["animateClose"];
};
export type FramePusher<T, K extends keyof T = keyof T> = T[K] extends
@ -117,9 +121,11 @@ function animateClose(element: HTMLElement) {
function animateOpen(element: HTMLElement) {
if (window.innerWidth <= 560) {
animateSlideInFromRight(element, { easing: ANIM_CURVE_DECELERATION });
return animateSlideInFromRight(element, {
easing: ANIM_CURVE_DECELERATION,
});
} else {
element.animate(
return element.animate(
{
opacity: [0.5, 1],
},
@ -128,6 +134,17 @@ function animateOpen(element: HTMLElement) {
}
}
function serializableStack(stack: readonly StackFrame[]) {
const frames = unwrap(stack);
return frames.map((fr) => {
return {
path: fr.path,
rootId: fr.rootId,
state: fr.state,
};
});
}
/**
* The router that stacks the pages.
*/
@ -140,13 +157,15 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
path,
state: opts?.state,
rootId: createUniqueId(),
animateOpen: opts?.animateOpen,
animateClose: opts?.animateClose,
};
mutStack(opts?.replace ? stack.length - 1 : stack.length, frame);
if (opts?.replace) {
window.history.replaceState(unwrap(stack), "", path);
window.history.replaceState(serializableStack(stack), "", path);
} else {
window.history.pushState(unwrap(stack), "", path);
window.history.pushState(serializableStack(stack), "", path);
}
return frame;
});
@ -165,10 +184,17 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
}
if (stack.length > 1) {
const lastFrame = stack[stack.length - 1];
const element = document.getElementById(lastFrame.rootId)!;
const element = document.getElementById(
lastFrame.rootId,
)! as HTMLDialogElement;
const createAnimation = lastFrame.animateClose ?? animateClose;
requestAnimationFrame(() => {
const animation = animateClose(element);
animation.addEventListener("finish", () => onlyPopFrame(depth));
element.classList.add("animating")
const animation = createAnimation(element);
animation.addEventListener("finish", () => {
element.classList.remove("animating")
onlyPopFrame(depth);
});
});
} else {
onlyPopFrame(depth);
@ -195,10 +221,16 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
});
const onBeforeDialogMount = (element: HTMLDialogElement) => {
createEffect(() => {
onMount(() => {
const lastFr = untrack(() => stack[stack.length - 1]);
const createAnimation = lastFr.animateOpen ?? animateOpen;
requestAnimationFrame(() => {
element.showModal();
animateOpen(element);
element.classList.add("animating");
const animation = createAnimation(element);
animation.addEventListener("finish", () =>
element.classList.remove("animating"),
);
});
});
};