StackedRouter: contain swipe to back state

This commit is contained in:
thislight 2024-11-21 22:53:31 +08:00
parent 76f7e08e78
commit 37b38be1d2
No known key found for this signature in database
GPG key ID: FCFE5192241CCD4E

View file

@ -244,6 +244,136 @@ function onEntryTouchStart(event: TouchEvent) {
event.preventDefault();
}
/**
* This function contains the state for swipe to back.
*
* @returns the props for dialogs to feature swipe to back.
*/
function createManagedSwipeToBack(
stack: readonly Readonly<StackFrame>[],
onlyPopFrame: (depth: number) => void,
) {
let reenterableAnimation: Animation | undefined;
let origWidth = 0,
origFigX = 0,
origFigY = 0;
const resetAnimation = () => {
reenterableAnimation = undefined;
};
const onDialogTouchStart = (
event: TouchEvent & { currentTarget: HTMLDialogElement },
) => {
if (event.touches.length !== 1) {
return;
}
event.stopPropagation();
const [fig0] = event.touches;
const { width } = event.currentTarget.getBoundingClientRect();
origWidth = width;
origFigX = fig0.clientX;
origFigY = fig0.clientY;
if (isNotInIOSSwipeToBackArea(fig0.clientX)) {
return;
}
// Prevent the default swipe to back/forward on iOS
event.preventDefault();
};
let animationProgressUpdateReleased = true;
let nextAnimationProgress = 0;
const updateAnimationProgress = () => {
try {
if (!reenterableAnimation) return;
const { activeDuration, delay } =
reenterableAnimation.effect!.getComputedTiming();
const totalTime = (delay || 0) + Number(activeDuration);
reenterableAnimation.currentTime = totalTime * nextAnimationProgress;
} finally {
animationProgressUpdateReleased = true;
}
};
const onDialogTouchMove = (
event: TouchEvent & { currentTarget: HTMLDialogElement },
) => {
if (event.touches.length !== 1) {
if (reenterableAnimation) {
reenterableAnimation.reverse();
reenterableAnimation.play();
}
}
const [fig0] = event.touches;
const ofsX = fig0.clientX - origFigX;
if (!reenterableAnimation) {
if (!(ofsX > 22) || !(Math.abs(fig0.clientY - origFigY) < 44)) {
return;
}
const lastFr = stack[stack.length - 1];
const createAnimation = lastFr.animateClose ?? animateClose;
reenterableAnimation = createAnimation(event.currentTarget);
reenterableAnimation.pause();
reenterableAnimation.addEventListener("finish", resetAnimation);
reenterableAnimation.addEventListener("cancel", resetAnimation);
}
event.preventDefault();
event.stopPropagation();
nextAnimationProgress = ofsX / origWidth / window.devicePixelRatio;
if (animationProgressUpdateReleased) {
animationProgressUpdateReleased = false;
requestAnimationFrame(updateAnimationProgress);
}
};
const onDialogTouchEnd = (event: TouchEvent) => {
if (!reenterableAnimation) return;
event.preventDefault();
event.stopPropagation();
const { activeDuration, delay } =
reenterableAnimation.effect!.getComputedTiming();
const totalTime = (delay || 0) + Number(activeDuration);
if (Number(reenterableAnimation.currentTime) / totalTime > 0.1) {
reenterableAnimation.addEventListener("finish", () => {
onlyPopFrame(1);
});
reenterableAnimation.play();
} else {
reenterableAnimation.cancel();
}
};
const onDialogTouchCancel = (event: TouchEvent) => {
if (!reenterableAnimation) return;
event.preventDefault();
event.stopPropagation();
reenterableAnimation.cancel();
};
return {
"on:touchstart": onDialogTouchStart,
"on:touchmove": onDialogTouchMove,
"on:touchend": onDialogTouchEnd,
"on:touchcancel": onDialogTouchCancel,
};
}
/**
* The router that stacks the pages.
*
@ -389,7 +519,6 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
const oinsetRight = computedStyle
.getPropertyValue("--safe-area-inset-right")
.split("px", 1)[0];
console.debug("insets-inline", oinsetLeft, oinsetRight);
const left = Number(oinsetLeft),
right = Number(oinsetRight.slice(0, oinsetRight.length - 2));
const totalWidth = SUBPAGE_MAX_WIDTH + left + right;
@ -406,117 +535,7 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
};
});
let reenterableAnimation: Animation | undefined;
let origWidth = 0,
origFigX = 0,
origFigY = 0;
const resetAnimation = () => {
reenterableAnimation = undefined;
};
const onDialogTouchStart = (
event: TouchEvent & { currentTarget: HTMLDialogElement },
) => {
if (event.touches.length !== 1) {
return;
}
event.stopPropagation();
const [fig0] = event.touches;
const { width } = event.currentTarget.getBoundingClientRect();
origWidth = width;
origFigX = fig0.clientX;
origFigY = fig0.clientY;
if (isNotInIOSSwipeToBackArea(fig0.clientX)) {
return;
}
// Prevent the default swipe to back/forward on iOS
event.preventDefault();
};
let animationProgressUpdateRequested = false
let nextAnimationProgress = 0
const updateAnimationProgress = () => {
if (!reenterableAnimation) return;
const { activeDuration, delay } =
reenterableAnimation.effect!.getComputedTiming();
const totalTime = (delay || 0) + Number(activeDuration);
reenterableAnimation.currentTime = totalTime * nextAnimationProgress;
animationProgressUpdateRequested = false
}
const onDialogTouchMove = (
event: TouchEvent & { currentTarget: HTMLDialogElement },
) => {
if (event.touches.length !== 1) {
if (reenterableAnimation) {
reenterableAnimation.reverse();
reenterableAnimation.play();
}
}
const [fig0] = event.touches;
const ofsX = fig0.clientX - origFigX;
if (!reenterableAnimation) {
if (!(ofsX > 22) || !(Math.abs(fig0.clientY - origFigY) < 44)) {
return;
}
const lastFr = stack[stack.length - 1];
const createAnimation = lastFr.animateClose ?? animateClose;
reenterableAnimation = createAnimation(event.currentTarget);
reenterableAnimation.pause();
reenterableAnimation.addEventListener("finish", resetAnimation);
reenterableAnimation.addEventListener("cancel", resetAnimation);
}
event.preventDefault();
event.stopPropagation();
nextAnimationProgress = ofsX / origWidth / window.devicePixelRatio;
if (!animationProgressUpdateRequested) {
animationProgressUpdateRequested = true;
requestAnimationFrame(updateAnimationProgress)
}
};
const onDialogTouchEnd = (event: TouchEvent) => {
if (!reenterableAnimation) return;
event.preventDefault();
event.stopPropagation();
const { activeDuration, delay } =
reenterableAnimation.effect!.getComputedTiming();
const totalTime = (delay || 0) + Number(activeDuration);
if (Number(reenterableAnimation.currentTime) / totalTime > 0.1) {
reenterableAnimation.addEventListener("finish", () => {
onlyPopFrame(1);
});
reenterableAnimation.play();
} else {
reenterableAnimation.cancel();
}
};
const onDialogTouchCancel = (event: TouchEvent) => {
if (!reenterableAnimation) return;
event.preventDefault();
event.stopPropagation();
reenterableAnimation.cancel();
};
const swipeToBackProps = createManagedSwipeToBack(stack, onlyPopFrame);
return (
<NavigatorContext.Provider
@ -555,10 +574,7 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
class="StackedPage"
onCancel={[popFrame, 1]}
onClick={[onDialogClick, popFrame]}
on:touchstart={onDialogTouchStart}
on:touchmove={onDialogTouchMove}
on:touchend={onDialogTouchEnd}
on:touchcancel={onDialogTouchCancel}
{...swipeToBackProps}
id={frame().rootId}
style={subInsets()}
>