diff --git a/src/platform/StackedRouter.tsx b/src/platform/StackedRouter.tsx index eab70ea..991c436 100644 --- a/src/platform/StackedRouter.tsx +++ b/src/platform/StackedRouter.tsx @@ -79,6 +79,11 @@ export type Navigator> = { const NavigatorContext = /* @__PURE__ */ createContext(); +/** + * Get the possible navigator of the {@link StackedRouter}. + * + * @see useNavigator for the navigator usage. + */ export function useMaybeNavigator() { return useContext(NavigatorContext); } @@ -91,6 +96,8 @@ export function useMaybeNavigator() { * path and its state. If you need push guide, you may want to * define your own function (like `useAppNavigator`) and cast the * navigator to the type you need. + * + * @see {@link useMaybeNavigator} if you are not sure you are under a {@link StackedRouter}. */ export function useNavigator() { const navigator = useMaybeNavigator(); @@ -110,10 +117,20 @@ export type CurrentFrame = { const CurrentFrameContext = /* @__PURE__ */ createContext>>(); +/** + * Return the current, if possible. + * + * @see {@link useCurrentFrame} asserts the frame exists + */ export function useMaybeCurrentFrame() { return useContext(CurrentFrameContext); } +/** + * Return the current frame, assert the frame exists. + * + * @see {@link useMaybeCurrentFrame} if you are not sure you are under a {@link StackedRouter}. + */ export function useCurrentFrame() { const frame = useMaybeCurrentFrame(); @@ -130,8 +147,11 @@ export function useCurrentFrame() { * A suspended frame is the one not on the top. "Suspended" * is the description of a certain situtation, not in the life cycle * of a frame. + * + * If this is not called under a {@link StackedRouter}, it always + * returns `false`. */ -export function useMaybeIsFrameSuspended() { +export function useIsFrameSuspended() { const { frames } = useMaybeNavigator() || {}; if (typeof frames === "undefined") { @@ -203,10 +223,31 @@ function serializableStack(stack: readonly StackFrame[]) { }); } +function isNotInIOSSwipeToBackArea(x: number) { + return ( + (x > 22 && x < window.innerWidth - 22) || + (x < -22 && x > window.innerWidth + 22) + ); +} + +function onEntryTouchStart(event: TouchEvent) { + if (event.touches.length !== 1) { + return; + } + + const [fig0] = event.touches; + + if (isNotInIOSSwipeToBackArea(fig0.clientX)) { + return; + } + + event.preventDefault(); +} + /** * The router that stacks the pages. * - * **Routes** The router accepts the {@link RouteProps} excluding the "url" field. + * **Routes** The router accepts the {@link RouterProps} excluding the "url" field. * You can seamlessly use the `` from `@solidjs/router`. * * Be advised that this component is not a drop-in replacement of that router. @@ -240,7 +281,7 @@ function serializableStack(stack: readonly StackFrame[]) { * Navigation animations (even the custom ones) will be played during * swipe to back, please keep in mind when designing animations. * - * The iOS default gesture is blocked on those pages. + * The iOS default gesture is blocked on all pages. */ const StackedRouter: Component = (oprops) => { const [stack, mutStack] = createStore([] as StackFrame[], { name: "stack" }); @@ -388,11 +429,7 @@ const StackedRouter: Component = (oprops) => { origFigX = fig0.clientX; origFigY = fig0.clientY; - const isNotInSwipeToBackArea = - (fig0.clientX > 22 && fig0.clientX < window.innerWidth - 22) || - (fig0.clientX < -22 && fig0.clientX > window.innerWidth + 22); - - if (isNotInSwipeToBackArea) { + if (isNotInIOSSwipeToBackArea(fig0.clientX)) { return; } // Prevent the default swipe to back/forward on iOS @@ -400,6 +437,21 @@ const StackedRouter: Component = (oprops) => { 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 }, ) => { @@ -429,13 +481,13 @@ const StackedRouter: Component = (oprops) => { event.preventDefault(); event.stopPropagation(); - const pc = ofsX / origWidth / window.devicePixelRatio; + nextAnimationProgress = ofsX / origWidth / window.devicePixelRatio; - const { activeDuration, delay } = - reenterableAnimation.effect!.getComputedTiming(); + if (!animationProgressUpdateRequested) { + animationProgressUpdateRequested = true; - const totalTime = (delay || 0) + Number(activeDuration); - reenterableAnimation.currentTime = totalTime * pc; + requestAnimationFrame(updateAnimationProgress) + } }; const onDialogTouchEnd = (event: TouchEvent) => { @@ -492,6 +544,7 @@ const StackedRouter: Component = (oprops) => { class="StackedPage" id={frame().rootId} role="presentation" + on:touchstart={onEntryTouchStart} > diff --git a/src/timelines/PullDownToRefresh.tsx b/src/timelines/PullDownToRefresh.tsx index 7cbacdb..5a61b46 100644 --- a/src/timelines/PullDownToRefresh.tsx +++ b/src/timelines/PullDownToRefresh.tsx @@ -10,7 +10,7 @@ import { Refresh as RefreshIcon } from "@suid/icons-material"; import { CircularProgress } from "@suid/material"; import { makeEventListener } from "@solid-primitives/event-listener"; import { createVisibilityObserver } from "@solid-primitives/intersection-observer"; -import { useMaybeIsFrameSuspended } from "../platform/StackedRouter"; +import { useIsFrameSuspended } from "../platform/StackedRouter"; const PullDownToRefresh: Component<{ loading?: boolean; @@ -34,7 +34,7 @@ const PullDownToRefresh: Component<{ }); const rootVisible = obvx(() => rootElement); - const isFrameSuspended = useMaybeIsFrameSuspended() + const isFrameSuspended = useIsFrameSuspended() createEffect(() => { if (!rootVisible()) setPullDown(0);