import type { RefObject } from 'react'; import React, { useEffect, useLayoutEffect, useRef } from '../../lib/teact/teact'; import { addExtraClass, removeExtraClass, toggleExtraClass } from '../../lib/teact/teact-dom'; import { requestMutation, requestForcedReflow } from '../../lib/fasterdom/fasterdom'; import { getGlobal } from '../../global'; import type { GlobalState } from '../../global/types'; import { ANIMATION_LEVEL_MIN } from '../../config'; import buildClassName from '../../util/buildClassName'; import forceReflow from '../../util/forceReflow'; import { waitForAnimationEnd, waitForTransitionEnd } from '../../util/cssAnimationEndListeners'; import useForceUpdate from '../../hooks/useForceUpdate'; import usePrevious from '../../hooks/usePrevious'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import './Transition.scss'; export type ChildrenFn = (isActive: boolean, isFrom: boolean, currentKey: number) => React.ReactNode; export type TransitionProps = { ref?: RefObject; activeKey: number; nextKey?: number; name: ( 'none' | 'slide' | 'slideRtl' | 'slideFade' | 'zoomFade' | 'slideLayers' | 'fade' | 'pushSlide' | 'reveal' | 'slideOptimized' | 'slideOptimizedRtl' | 'semiFade' | 'slideVertical' | 'slideVerticalFade' ); direction?: 'auto' | 'inverse' | 1 | -1; renderCount?: number; shouldRestoreHeight?: boolean; shouldCleanup?: boolean; cleanupExceptionKey?: number; // Used by async components which are usually remounted during first animation shouldWrap?: boolean; wrapExceptionKey?: number; id?: string; className?: string; slideClassName?: string; onStart?: NoneToVoidFunction; onStop?: NoneToVoidFunction; children: React.ReactNode | ChildrenFn; afterChildren?: React.ReactNode; }; const FALLBACK_ANIMATION_END = 1000; const CLASSES = { slide: 'Transition_slide', active: 'Transition_slide-active', from: 'Transition_slide-from', to: 'Transition_slide-to', inactive: 'Transition_slide-inactive', afterSlides: 'Transition_afterSlides', }; function Transition({ ref, activeKey, nextKey, name, direction = 'auto', renderCount, shouldRestoreHeight, shouldCleanup, cleanupExceptionKey, shouldWrap, wrapExceptionKey, id, className, slideClassName, onStart, onStop, children, afterChildren, }: TransitionProps) { // No need for a container to update on change const { animationLevel } = getGlobal().settings.byKey; const currentKeyRef = useRef(); // eslint-disable-next-line no-null/no-null let containerRef = useRef(null); if (ref) { containerRef = ref; } const rendersRef = useRef>({}); const prevActiveKey = usePrevious(activeKey); const forceUpdate = useForceUpdate(); const activeKeyChanged = prevActiveKey !== undefined && activeKey !== prevActiveKey; if (!renderCount && activeKeyChanged) { rendersRef.current = { [prevActiveKey]: rendersRef.current[prevActiveKey] }; } rendersRef.current[activeKey] = children; if (nextKey) { rendersRef.current[nextKey] = children; } const isBackwards = ( direction === -1 || (direction === 'auto' && prevActiveKey > activeKey) || (direction === 'inverse' && prevActiveKey < activeKey) ); useLayoutEffect(() => { function cleanup() { if (!shouldCleanup) { return; } const preservedRender = cleanupExceptionKey !== undefined ? rendersRef.current[cleanupExceptionKey] : undefined; rendersRef.current = preservedRender ? { [cleanupExceptionKey!]: preservedRender } : {}; forceUpdate(); } const isSlideOptimized = name === 'slideOptimized' || name === 'slideOptimizedRtl'; const container = containerRef.current!; const keys = Object.keys(rendersRef.current).map(Number); const prevActiveIndex = renderCount ? prevActiveKey : keys.indexOf(prevActiveKey); const activeIndex = renderCount ? activeKey : keys.indexOf(activeKey); const childNodes = Array.from(container.childNodes) .filter((el) => !(el instanceof HTMLElement && el.classList.contains(CLASSES.afterSlides))); if (!childNodes.length) { return; } const childElements = (Array.from(container.children) as HTMLElement[]) .filter((el) => !el.classList.contains(CLASSES.afterSlides)); childElements.forEach((el) => { addExtraClass(el, CLASSES.slide); if (slideClassName) { addExtraClass(el, slideClassName); } }); if (!activeKeyChanged) { if (childElements.length === 1 || (nextKey !== undefined && childElements.length === 2)) { const firstChild = childNodes[activeIndex] as HTMLElement; addExtraClass(firstChild, CLASSES.active); if (isSlideOptimized) { firstChild.style.transition = 'none'; firstChild.style.transform = 'translate3d(0, 0, 0)'; } if (childElements.length === 2) { const nextChild = childElements[0] === firstChild ? childElements[1] : childElements[0]; addExtraClass(nextChild, CLASSES.inactive); } } return; } currentKeyRef.current = activeKey; if (isSlideOptimized) { performSlideOptimized( animationLevel, name, isBackwards, cleanup, activeKey, currentKeyRef, container, shouldRestoreHeight, onStart, onStop, childNodes[activeIndex] as HTMLElement, childNodes[prevActiveIndex] as HTMLElement, ); return; } if (name === 'none' || animationLevel === ANIMATION_LEVEL_MIN) { childNodes.forEach((node, i) => { if (node instanceof HTMLElement) { removeExtraClass(node, CLASSES.from); removeExtraClass(node, CLASSES.to); toggleExtraClass(node, CLASSES.active, i === activeIndex); toggleExtraClass(node, CLASSES.inactive, i !== activeIndex); } }); cleanup(); return; } childNodes.forEach((node, i) => { if (node instanceof HTMLElement) { removeExtraClass(node, CLASSES.active); toggleExtraClass(node, CLASSES.from, i === prevActiveIndex); toggleExtraClass(node, CLASSES.to, i === activeIndex); toggleExtraClass(node, CLASSES.inactive, i !== prevActiveIndex && i !== activeIndex); } }); const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent(); onStart?.(); toggleExtraClass(container, `Transition-${name}`, !isBackwards); toggleExtraClass(container, `Transition-${name}Backwards`, isBackwards); function onAnimationEnd() { const activeElement = container.querySelector(`.${CLASSES.active}`); const { clientHeight } = activeElement || {}; requestMutation(() => { if (activeKey !== currentKeyRef.current) { return; } removeExtraClass(container, `Transition-${name}`); removeExtraClass(container, `Transition-${name}Backwards`); childNodes.forEach((node, i) => { if (node instanceof HTMLElement) { removeExtraClass(node, CLASSES.from); removeExtraClass(node, CLASSES.to); toggleExtraClass(node, CLASSES.active, i === activeIndex); toggleExtraClass(node, CLASSES.inactive, i !== activeIndex); } }); if (shouldRestoreHeight) { if (activeElement) { activeElement.style.height = 'auto'; container.style.height = `${clientHeight}px`; } } onStop?.(); dispatchHeavyAnimationStop(); cleanup(); }); } const watchedNode = name === 'reveal' && isBackwards ? childNodes[prevActiveIndex] : childNodes[activeIndex]; if (watchedNode) { waitForAnimationEnd(watchedNode, onAnimationEnd, undefined, FALLBACK_ANIMATION_END); } else { onAnimationEnd(); } }, [ activeKey, nextKey, prevActiveKey, activeKeyChanged, isBackwards, name, onStart, onStop, renderCount, shouldRestoreHeight, shouldCleanup, slideClassName, cleanupExceptionKey, animationLevel, forceUpdate, ]); useEffect(() => { if (!shouldRestoreHeight) { return; } const container = containerRef.current!; const activeElement = container.querySelector(`.${CLASSES.active}`) || container.querySelector(`.${CLASSES.from}`); if (!activeElement) { return; } const { clientHeight } = activeElement || {}; if (!clientHeight) { return; } requestMutation(() => { activeElement.style.height = 'auto'; container.style.height = `${clientHeight}px`; container.style.flexBasis = `${clientHeight}px`; }); }, [shouldRestoreHeight, children]); const asFastList = !renderCount; const renders = rendersRef.current; const renderKeys = Object.keys(renderCount ? new Array(renderCount).fill(undefined) : renders).map(Number); const contents = renderKeys.map((key) => { const render = renders[key]; if (!render) { return undefined; } const rendered = typeof render === 'function' ? render(key === activeKey, key === prevActiveKey, activeKey) : render; return (shouldWrap && key !== wrapExceptionKey) || asFastList ?
{rendered}
: rendered; }); if (afterChildren) { contents.push((
{afterChildren}
)); } return (
{contents}
); } export default Transition; function performSlideOptimized( animationLevel: GlobalState['settings']['byKey']['animationLevel'], name: 'slideOptimized' | 'slideOptimizedRtl', isBackwards: boolean, cleanup: NoneToVoidFunction, activeKey: number, currentKeyRef: { current: number | undefined }, container: HTMLElement, shouldRestoreHeight?: boolean, onStart?: NoneToVoidFunction, onStop?: NoneToVoidFunction, toSlide?: HTMLElement, fromSlide?: HTMLElement, ) { if (!fromSlide || !toSlide) { return; } if (animationLevel === ANIMATION_LEVEL_MIN) { toggleExtraClass(container, `Transition-${name}`, !isBackwards); toggleExtraClass(container, `Transition-${name}Backwards`, isBackwards); fromSlide.style.transition = 'none'; fromSlide.style.transform = ''; removeExtraClass(fromSlide, CLASSES.active); toSlide.style.transition = 'none'; toSlide.style.transform = 'translate3d(0, 0, 0)'; addExtraClass(toSlide, CLASSES.active); cleanup(); return; } if (name === 'slideOptimizedRtl') { isBackwards = !isBackwards; } const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent(); onStart?.(); toggleExtraClass(container, `Transition-${name}`, !isBackwards); toggleExtraClass(container, `Transition-${name}Backwards`, isBackwards); fromSlide.style.transition = 'none'; fromSlide.style.transform = 'translate3d(0, 0, 0)'; toSlide.style.transition = 'none'; toSlide.style.transform = `translate3d(${isBackwards ? '-' : ''}100%, 0, 0)`; requestForcedReflow(() => { forceReflow(toSlide); return () => { fromSlide.style.transition = ''; fromSlide.style.transform = `translate3d(${isBackwards ? '' : '-'}100%, 0, 0)`; toSlide.style.transition = ''; toSlide.style.transform = 'translate3d(0, 0, 0)'; removeExtraClass(fromSlide, CLASSES.active); addExtraClass(toSlide, CLASSES.active); }; }); waitForTransitionEnd(fromSlide, () => { const { clientHeight } = toSlide; requestMutation(() => { if (activeKey !== currentKeyRef.current) { return; } fromSlide.style.transition = 'none'; fromSlide.style.transform = ''; if (shouldRestoreHeight) { toSlide.style.height = 'auto'; container.style.height = `${clientHeight}px`; } onStop?.(); dispatchHeavyAnimationStop(); cleanup(); }); }); }