From 37b38be1d2c44c871fdacc75c512db8d2f836136 Mon Sep 17 00:00:00 2001 From: thislight Date: Thu, 21 Nov 2024 22:53:31 +0800 Subject: [PATCH] StackedRouter: contain swipe to back state --- src/platform/StackedRouter.tsx | 248 ++++++++++++++++++--------------- 1 file changed, 132 insertions(+), 116 deletions(-) diff --git a/src/platform/StackedRouter.tsx b/src/platform/StackedRouter.tsx index 991c436..0686c9b 100644 --- a/src/platform/StackedRouter.tsx +++ b/src/platform/StackedRouter.tsx @@ -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[], + 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 = (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 = (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 ( = (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()} >