StackedRouter: contain swipe to back state
This commit is contained in:
parent
76f7e08e78
commit
37b38be1d2
1 changed files with 132 additions and 116 deletions
|
@ -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()}
|
||||
>
|
||||
|
|
Loading…
Reference in a new issue