Transition: Fixes for controlled swipe

This commit is contained in:
Alexander Zinchuk 2023-12-04 14:38:16 +01:00
parent e77506cc54
commit 939fff460e
4 changed files with 57 additions and 38 deletions

View File

@ -97,11 +97,12 @@ function Transition({
const rendersRef = useRef<Record<number, React.ReactNode | ChildrenFn>>({}); const rendersRef = useRef<Record<number, React.ReactNode | ChildrenFn>>({});
const prevActiveKey = usePrevious<any>(activeKey); const prevActiveKey = usePrevious<any>(activeKey);
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
const isAnimatingRef = useRef(false);
const isSwipeJustCancelledRef = 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] }; rendersRef.current = { [prevActiveKey]: rendersRef.current[prevActiveKey] };
} }
@ -149,25 +150,26 @@ function Transition({
} }
}); });
if (!activeKeyChanged) { if (!hasActiveKeyChanged) {
if (childElements.length === 1 || (nextKey !== undefined && childElements.length === 2)) { if (isAnimatingRef.current) {
const firstChild = childNodes[activeIndex] as HTMLElement; return;
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);
}
} }
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; return;
} }
@ -185,6 +187,7 @@ function Transition({
cleanup, cleanup,
activeKey, activeKey,
currentKeyRef, currentKeyRef,
isAnimatingRef,
container, container,
childNodes[activeIndex], childNodes[activeIndex],
childNodes[prevActiveIndex], childNodes[prevActiveIndex],
@ -224,6 +227,7 @@ function Transition({
} }
}); });
isAnimatingRef.current = true;
const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent(); const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent();
onStart?.(); onStart?.();
@ -260,6 +264,7 @@ function Transition({
onStop?.(); onStop?.();
dispatchHeavyAnimationStop(); dispatchHeavyAnimationStop();
isAnimatingRef.current = false;
cleanup(); cleanup();
}); });
@ -281,6 +286,7 @@ function Transition({
isSwipeJustCancelledRef.current = true; isSwipeJustCancelledRef.current = true;
onStop?.(); onStop?.();
dispatchHeavyAnimationStop(); dispatchHeavyAnimationStop();
isAnimatingRef.current = false;
}, },
); );
} else { } else {
@ -293,7 +299,7 @@ function Transition({
activeKey, activeKey,
nextKey, nextKey,
prevActiveKey, prevActiveKey,
activeKeyChanged, hasActiveKeyChanged,
isBackwards, isBackwards,
name, name,
onStart, onStart,
@ -373,6 +379,7 @@ function performSlideOptimized(
cleanup: NoneToVoidFunction, cleanup: NoneToVoidFunction,
activeKey: number, activeKey: number,
currentKeyRef: { current: number | undefined }, currentKeyRef: { current: number | undefined },
isAnimatingRef: { current: boolean | undefined },
container: HTMLElement, container: HTMLElement,
toSlide: ChildNode, toSlide: ChildNode,
fromSlide?: ChildNode, fromSlide?: ChildNode,
@ -409,8 +416,8 @@ function performSlideOptimized(
isBackwards = !isBackwards; isBackwards = !isBackwards;
} }
isAnimatingRef.current = true;
const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent(); const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent();
onStart?.(); onStart?.();
toggleExtraClass(container, `Transition-${name}`, !isBackwards); toggleExtraClass(container, `Transition-${name}`, !isBackwards);
@ -476,6 +483,8 @@ function performSlideOptimized(
onStop?.(); onStop?.();
dispatchHeavyAnimationStop(); dispatchHeavyAnimationStop();
isAnimatingRef.current = false;
cleanup(); cleanup();
}); });
}); });

View File

@ -56,7 +56,7 @@ type AnimateNumberProps<T extends number | number[]> = {
duration: number; duration: number;
onUpdate: (value: T) => void; onUpdate: (value: T) => void;
timing?: TimingFn; timing?: TimingFn;
onEnd?: () => void; onEnd?: (isCanceled?: boolean) => void;
}; };
export const timingFunctions = { export const timingFunctions = {
@ -87,13 +87,15 @@ export function animateNumber<T extends number | number[]>({
to, to,
}: AnimateNumberProps<T>) { }: AnimateNumberProps<T>) {
const t0 = Date.now(); const t0 = Date.now();
let canceled = false;
let isCanceled = false;
animateInstantly(() => { animateInstantly(() => {
if (canceled) return false; if (isCanceled) return false;
const t1 = Date.now(); const t1 = Date.now();
let t = (t1 - t0) / duration; const t = Math.min((t1 - t0) / duration, 1);
if (t > 1) t = 1;
const progress = timing(t); const progress = timing(t);
if (typeof from === 'number' && typeof to === 'number') { if (typeof from === 'number' && typeof to === 'number') {
onUpdate((from + ((to - from) * progress)) as T); onUpdate((from + ((to - from) * progress)) as T);
@ -101,13 +103,17 @@ export function animateNumber<T extends number | number[]>({
const result = from.map((f, i) => f + ((to[i] - f) * progress)); const result = from.map((f, i) => f + ((to[i] - f) * progress));
onUpdate(result as T); onUpdate(result as T);
} }
if (t === 1 && onEnd) onEnd();
if (t === 1) {
onEnd?.();
}
return t < 1; return t < 1;
}, requestMeasure); }, requestMeasure);
return () => { return () => {
canceled = true; isCanceled = true;
if (onEnd) onEnd(); onEnd?.(true);
}; };
} }

View File

@ -38,11 +38,10 @@ export function logUnequalProps(currentProps: AnyLiteral, newProps: AnyLiteral,
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(msg); console.log(msg);
currentKeys.forEach((prop) => {
for (const prop of currentKeys) {
if (currentProps[prop] !== newProps[prop]) { if (currentProps[prop] !== newProps[prop]) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(debugKey, prop, ':', currentProps[prop], '=>', newProps[prop]); console.log(debugKey, prop, ':', currentProps[prop], '=>', newProps[prop]);
} }
} });
} }

View File

@ -14,6 +14,7 @@ let isSwipeActive = false;
let swipeOffsets: MoveOffsets | undefined; let swipeOffsets: MoveOffsets | undefined;
let onDrag: ((offsets: MoveOffsets) => void) | undefined; let onDrag: ((offsets: MoveOffsets) => void) | undefined;
let onRelease: ((onCancel: NoneToVoidFunction) => void) | undefined; let onRelease: ((onCancel: NoneToVoidFunction) => void) | undefined;
let cancelCurrentReleaseAnimation: NoneToVoidFunction | undefined;
export function captureControlledSwipe( export function captureControlledSwipe(
element: HTMLElement, options: { element: HTMLElement, options: {
@ -70,6 +71,8 @@ export function allowSwipeControlForTransition(
nextSlide: HTMLElement, nextSlide: HTMLElement,
onCancelForTransition: NoneToVoidFunction, onCancelForTransition: NoneToVoidFunction,
) { ) {
cancelCurrentReleaseAnimation?.();
if (!isSwipeActive) return; if (!isSwipeActive) return;
const targetPosition = extractAnimationEndPosition(currentSlide); const targetPosition = extractAnimationEndPosition(currentSlide);
@ -108,7 +111,7 @@ export function allowSwipeControlForTransition(
}; };
onRelease = (onCancelForClient: NoneToVoidFunction) => { onRelease = (onCancelForClient: NoneToVoidFunction) => {
const isCanceled = currentDirection === -1; const isRevertSwipe = currentDirection === -1;
function cleanup() { function cleanup() {
currentSlide.getAnimations().forEach((a) => a.cancel()); 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 // For some reason animations are not cleared when CSS class is removed
waitForAnimationEnd(currentSlide, cleanup); waitForAnimationEnd(currentSlide, cleanup);
} }
animateNumber({ cancelCurrentReleaseAnimation = animateNumber({
from: progress, from: progress,
to: isCanceled ? 0 : 1, to: isRevertSwipe ? 0 : 1,
duration: INERTIA_DURATION, duration: INERTIA_DURATION,
timing: INERTIA_EASING, timing: INERTIA_EASING,
onUpdate(releaseProgress) { onUpdate(releaseProgress) {
updateAnimationProgress([currentSlide, nextSlide], releaseProgress); updateAnimationProgress([currentSlide, nextSlide], releaseProgress);
}, },
onEnd() { onEnd(isCanceled = false) {
if (isCanceled) { cancelCurrentReleaseAnimation = undefined;
if (isCanceled || isRevertSwipe) {
cleanup(); cleanup();
onCancelForTransition(); onCancelForTransition();
onCancelForClient(); onCancelForClient();