From 939fff460e1cf39ec293719653b7cebb7d0cbbde Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Mon, 4 Dec 2023 14:38:16 +0100 Subject: [PATCH] Transition: Fixes for controlled swipe --- src/components/ui/Transition.tsx | 51 +++++++++++++++++++------------- src/util/animation.ts | 22 +++++++++----- src/util/arePropsShallowEqual.ts | 5 ++-- src/util/swipeController.ts | 17 +++++++---- 4 files changed, 57 insertions(+), 38 deletions(-) diff --git a/src/components/ui/Transition.tsx b/src/components/ui/Transition.tsx index b37a30c1a..d1e1a24af 100644 --- a/src/components/ui/Transition.tsx +++ b/src/components/ui/Transition.tsx @@ -97,11 +97,12 @@ function Transition({ const rendersRef = useRef>({}); const prevActiveKey = usePrevious(activeKey); const forceUpdate = useForceUpdate(); + const isAnimatingRef = useRef(false); const isSwipeJustCancelledRef = useRef(false); - const activeKeyChanged = prevActiveKey !== undefined && activeKey !== prevActiveKey; + const hasActiveKeyChanged = prevActiveKey !== undefined && activeKey !== prevActiveKey; - if (!renderCount && activeKeyChanged) { + if (!renderCount && hasActiveKeyChanged) { rendersRef.current = { [prevActiveKey]: rendersRef.current[prevActiveKey] }; } @@ -149,25 +150,26 @@ function Transition({ } }); - if (!activeKeyChanged) { - if (childElements.length === 1 || (nextKey !== undefined && childElements.length === 2)) { - const firstChild = childNodes[activeIndex] as HTMLElement; - - addExtraClass(firstChild, CLASSES.active); - - if (isSlideOptimized) { - setExtraStyles(firstChild, { - transition: 'none', - transform: 'translate3d(0, 0, 0)', - }); - } - - if (childElements.length === 2) { - const nextChild = childElements[0] === firstChild ? childElements[1] : childElements[0]; - addExtraClass(nextChild, CLASSES.inactive); - } + if (!hasActiveKeyChanged) { + if (isAnimatingRef.current) { + return; } + childElements.forEach((childElement) => { + if (childElement === childNodes[activeIndex]) { + addExtraClass(childElement, CLASSES.active); + + if (isSlideOptimized) { + setExtraStyles(childElement, { + transition: 'none', + transform: 'translate3d(0, 0, 0)', + }); + } + } else { + addExtraClass(childElement, CLASSES.inactive); + } + }); + return; } @@ -185,6 +187,7 @@ function Transition({ cleanup, activeKey, currentKeyRef, + isAnimatingRef, container, childNodes[activeIndex], childNodes[prevActiveIndex], @@ -224,6 +227,7 @@ function Transition({ } }); + isAnimatingRef.current = true; const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent(); onStart?.(); @@ -260,6 +264,7 @@ function Transition({ onStop?.(); dispatchHeavyAnimationStop(); + isAnimatingRef.current = false; cleanup(); }); @@ -281,6 +286,7 @@ function Transition({ isSwipeJustCancelledRef.current = true; onStop?.(); dispatchHeavyAnimationStop(); + isAnimatingRef.current = false; }, ); } else { @@ -293,7 +299,7 @@ function Transition({ activeKey, nextKey, prevActiveKey, - activeKeyChanged, + hasActiveKeyChanged, isBackwards, name, onStart, @@ -373,6 +379,7 @@ function performSlideOptimized( cleanup: NoneToVoidFunction, activeKey: number, currentKeyRef: { current: number | undefined }, + isAnimatingRef: { current: boolean | undefined }, container: HTMLElement, toSlide: ChildNode, fromSlide?: ChildNode, @@ -409,8 +416,8 @@ function performSlideOptimized( isBackwards = !isBackwards; } + isAnimatingRef.current = true; const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent(); - onStart?.(); toggleExtraClass(container, `Transition-${name}`, !isBackwards); @@ -476,6 +483,8 @@ function performSlideOptimized( onStop?.(); dispatchHeavyAnimationStop(); + isAnimatingRef.current = false; + cleanup(); }); }); diff --git a/src/util/animation.ts b/src/util/animation.ts index a7fd31bbb..8ce897cfb 100644 --- a/src/util/animation.ts +++ b/src/util/animation.ts @@ -56,7 +56,7 @@ type AnimateNumberProps = { duration: number; onUpdate: (value: T) => void; timing?: TimingFn; - onEnd?: () => void; + onEnd?: (isCanceled?: boolean) => void; }; export const timingFunctions = { @@ -87,13 +87,15 @@ export function animateNumber({ to, }: AnimateNumberProps) { const t0 = Date.now(); - let canceled = false; + + let isCanceled = false; animateInstantly(() => { - if (canceled) return false; + if (isCanceled) return false; + const t1 = Date.now(); - let t = (t1 - t0) / duration; - if (t > 1) t = 1; + const t = Math.min((t1 - t0) / duration, 1); + const progress = timing(t); if (typeof from === 'number' && typeof to === 'number') { onUpdate((from + ((to - from) * progress)) as T); @@ -101,13 +103,17 @@ export function animateNumber({ const result = from.map((f, i) => f + ((to[i] - f) * progress)); onUpdate(result as T); } - if (t === 1 && onEnd) onEnd(); + + if (t === 1) { + onEnd?.(); + } + return t < 1; }, requestMeasure); return () => { - canceled = true; - if (onEnd) onEnd(); + isCanceled = true; + onEnd?.(true); }; } diff --git a/src/util/arePropsShallowEqual.ts b/src/util/arePropsShallowEqual.ts index 834d05dd6..a6ba94531 100644 --- a/src/util/arePropsShallowEqual.ts +++ b/src/util/arePropsShallowEqual.ts @@ -38,11 +38,10 @@ export function logUnequalProps(currentProps: AnyLiteral, newProps: AnyLiteral, // eslint-disable-next-line no-console console.log(msg); - - for (const prop of currentKeys) { + currentKeys.forEach((prop) => { if (currentProps[prop] !== newProps[prop]) { // eslint-disable-next-line no-console console.log(debugKey, prop, ':', currentProps[prop], '=>', newProps[prop]); } - } + }); } diff --git a/src/util/swipeController.ts b/src/util/swipeController.ts index 5d38832e3..8c1dfe994 100644 --- a/src/util/swipeController.ts +++ b/src/util/swipeController.ts @@ -14,6 +14,7 @@ let isSwipeActive = false; let swipeOffsets: MoveOffsets | undefined; let onDrag: ((offsets: MoveOffsets) => void) | undefined; let onRelease: ((onCancel: NoneToVoidFunction) => void) | undefined; +let cancelCurrentReleaseAnimation: NoneToVoidFunction | undefined; export function captureControlledSwipe( element: HTMLElement, options: { @@ -70,6 +71,8 @@ export function allowSwipeControlForTransition( nextSlide: HTMLElement, onCancelForTransition: NoneToVoidFunction, ) { + cancelCurrentReleaseAnimation?.(); + if (!isSwipeActive) return; const targetPosition = extractAnimationEndPosition(currentSlide); @@ -108,7 +111,7 @@ export function allowSwipeControlForTransition( }; onRelease = (onCancelForClient: NoneToVoidFunction) => { - const isCanceled = currentDirection === -1; + const isRevertSwipe = currentDirection === -1; function cleanup() { currentSlide.getAnimations().forEach((a) => a.cancel()); @@ -120,21 +123,23 @@ export function allowSwipeControlForTransition( }); } - if (!isCanceled) { + if (!isRevertSwipe) { // For some reason animations are not cleared when CSS class is removed waitForAnimationEnd(currentSlide, cleanup); } - animateNumber({ + cancelCurrentReleaseAnimation = animateNumber({ from: progress, - to: isCanceled ? 0 : 1, + to: isRevertSwipe ? 0 : 1, duration: INERTIA_DURATION, timing: INERTIA_EASING, onUpdate(releaseProgress) { updateAnimationProgress([currentSlide, nextSlide], releaseProgress); }, - onEnd() { - if (isCanceled) { + onEnd(isCanceled = false) { + cancelCurrentReleaseAnimation = undefined; + + if (isCanceled || isRevertSwipe) { cleanup(); onCancelForTransition(); onCancelForClient();