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…
	
	Add table
		Add a link
		
	
		Reference in a new issue