From 35ae2ff9821cae022377726ef904042dc24c7af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= <40713406+tjzel@users.noreply.github.com> Date: Thu, 23 Nov 2023 16:33:36 +0100 Subject: [PATCH] [Web] Fix hiding component with `entering` on every render (#5278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Requires #5282. ## Summary Some TS shenanigans as a necessary bonus.
BEFOREAFTER
https://github.com/software-mansion/react-native-reanimated/assets/40713406/d13c9186-f1e5-4e12-bbb1-704b1eac0c08 https://github.com/software-mansion/react-native-reanimated/assets/40713406/804fbcfb-f0f0-4b3f-9af3-c46617ffed18
## Test plan
```tsx import { StyleSheet, View, Button } from 'react-native'; import Animated, { FadeIn } from 'react-native-reanimated'; import React from 'react'; export default function EmptyExample() { const [shouldBeUseEntering, setShouldBeUseEntering] = React.useState(false); function handlePress() { setShouldBeUseEntering(!shouldBeUseEntering); } return (
--------- Co-authored-by: MichaƂ Bert --- src/createAnimatedComponent/PropsFilter.tsx | 13 +-- .../createAnimatedComponent.tsx | 8 +- .../web/animationsManager.ts | 79 +++++++++++-------- .../layoutReanimation/web/componentUtils.ts | 19 +++-- 4 files changed, 67 insertions(+), 52 deletions(-) diff --git a/src/createAnimatedComponent/PropsFilter.tsx b/src/createAnimatedComponent/PropsFilter.tsx index 2790c1d66c5..2ac804924f4 100644 --- a/src/createAnimatedComponent/PropsFilter.tsx +++ b/src/createAnimatedComponent/PropsFilter.tsx @@ -22,7 +22,6 @@ function dummyListener() { export class PropsFilter implements IPropsFilter { private _initialStyle = {}; - private _isFirstRender = true; public filterNonAnimatedProps( component: React.Component & IAnimatedComponentInternal @@ -39,7 +38,7 @@ export class PropsFilter implements IPropsFilter { if (style && style.viewDescriptors) { // this is how we recognize styles returned by useAnimatedStyle style.viewsRef.add(component); - if (this._isFirstRender) { + if (component._isFirstRender) { this._initialStyle = { ...style.initial.value, ...this._initialStyle, @@ -48,7 +47,7 @@ export class PropsFilter implements IPropsFilter { } return this._initialStyle; } else if (hasInlineStyles(style)) { - return getInlineStyle(style, this._isFirstRender); + return getInlineStyle(style, component._isFirstRender); } else { return style; } @@ -78,7 +77,7 @@ export class PropsFilter implements IPropsFilter { props[key] = dummyListener; } } else if (isSharedValue(value)) { - if (this._isFirstRender) { + if (component._isFirstRender) { props[key] = (value as SharedValue).value; } } else if (key !== 'onGestureHandlerStateChange' || !isChromeDebugger()) { @@ -87,10 +86,4 @@ export class PropsFilter implements IPropsFilter { } return props; } - - public onRender() { - if (this._isFirstRender) { - this._isFirstRender = false; - } - } } diff --git a/src/createAnimatedComponent/createAnimatedComponent.tsx b/src/createAnimatedComponent/createAnimatedComponent.tsx index 8739fec33b0..301270080f8 100644 --- a/src/createAnimatedComponent/createAnimatedComponent.tsx +++ b/src/createAnimatedComponent/createAnimatedComponent.tsx @@ -132,9 +132,12 @@ export function createAnimatedComponent( startWebLayoutAnimation( this.props, this._component as HTMLElement, - LayoutAnimationType.ENTERING + LayoutAnimationType.ENTERING, + !!(this._isFirstRender && this.props.entering) ); } + + this._isFirstRender = false; } componentWillUnmount() { @@ -507,7 +510,6 @@ export function createAnimatedComponent( render() { const props = this._PropsFilter.filterNonAnimatedProps(this); - this._PropsFilter.onRender(); if (isJest()) { props.animatedStyle = this.animatedStyle; @@ -517,7 +519,7 @@ export function createAnimatedComponent( // Because of that we can encounter a situation in which component is visible for a short amount of time, and later on animation triggers. // I've tested that on various browsers and devices and it did not happen to me. To be sure that it won't happen to someone else, // I've decided to hide component at first render. Its visibility is reset in `componentDidMount`. - if (IS_WEB && props.entering) { + if (this._isFirstRender && IS_WEB && props.entering) { props.style = { ...(props.style ?? {}), visibility: 'hidden', // Hide component until `componentDidMount` triggers diff --git a/src/reanimated2/layoutReanimation/web/animationsManager.ts b/src/reanimated2/layoutReanimation/web/animationsManager.ts index 1714dc399c7..f668f248c55 100644 --- a/src/reanimated2/layoutReanimation/web/animationsManager.ts +++ b/src/reanimated2/layoutReanimation/web/animationsManager.ts @@ -36,9 +36,7 @@ function chooseConfig>( function checkUndefinedAnimationFail( initialAnimationName: string, - isLayoutTransition: boolean, - hasEnteringAnimation: boolean, - element: HTMLElement + isLayoutTransition: boolean ) { // This prevents crashes if we try to set animations that are not defined. // We don't care about layout transitions since they're created dynamically @@ -46,10 +44,6 @@ function checkUndefinedAnimationFail( return false; } - if (hasEnteringAnimation) { - makeElementVisible(element); - } - console.warn( "[Reanimated] Couldn't load entering/exiting animation. Current version supports only predefined animations with modifiers: duration, delay, easing, randomizeDelay, wtihCallback, reducedMotion." ); @@ -57,19 +51,11 @@ function checkUndefinedAnimationFail( return true; } -function checkReduceMotionFail( - animationConfig: AnimationConfig, - hasEnteringAnimation: boolean, - element: HTMLElement -) { +function checkReduceMotionFail(animationConfig: AnimationConfig) { if (!animationConfig.reduceMotion) { return false; } - if (hasEnteringAnimation) { - makeElementVisible(element); - } - return true; } @@ -100,33 +86,28 @@ function chooseAction( } } -export function startWebLayoutAnimation< +function tryGetAnimationConfigWithTransform< ComponentProps extends Record >( props: Readonly>, - element: HTMLElement, - animationType: LayoutAnimationType, - transitionData?: TransitionData + animationType: LayoutAnimationType ) { const config = chooseConfig(animationType, props); if (!config) { - return; + return null; } - const hasEnteringAnimation = props.entering !== undefined; const isLayoutTransition = animationType === LayoutAnimationType.LAYOUT; const initialAnimationName = typeof config === 'function' ? config.name : config.constructor.name; const shouldFail = checkUndefinedAnimationFail( initialAnimationName, - isLayoutTransition, - hasEnteringAnimation, - element + isLayoutTransition ); if (shouldFail) { - return; + return null; } const transform = extractTransformFromStyle(props.style as StyleProps); @@ -142,17 +123,46 @@ export function startWebLayoutAnimation< initialAnimationName as AnimationNames ); - if (checkReduceMotionFail(animationConfig, hasEnteringAnimation, element)) { - return; + if (checkReduceMotionFail(animationConfig)) { + return null; } - chooseAction( - animationType, - animationConfig, - element, - transitionData as TransitionData, - transform + return { animationConfig, transform }; +} + +export function startWebLayoutAnimation< + ComponentProps extends Record +>( + props: Readonly>, + element: HTMLElement, + animationType: LayoutAnimationType, + shouldMakeVisible = false, + transitionData?: TransitionData +) { + let visibilityDelay = 0; + + const maybeAnimationConfigWithTransform = tryGetAnimationConfigWithTransform( + props, + animationType ); + + if (maybeAnimationConfigWithTransform) { + const { animationConfig, transform } = maybeAnimationConfigWithTransform; + + visibilityDelay = animationConfig.delay; + + chooseAction( + animationType, + animationConfig, + element, + transitionData as TransitionData, + transform + ); + } + + if (shouldMakeVisible) { + makeElementVisible(element, visibilityDelay); + } } export function tryActivateLayoutTransition< @@ -184,6 +194,7 @@ export function tryActivateLayoutTransition< props, element, LayoutAnimationType.LAYOUT, + false, transitionData ); } diff --git a/src/reanimated2/layoutReanimation/web/componentUtils.ts b/src/reanimated2/layoutReanimation/web/componentUtils.ts index 79a143806b7..f89cd4b2abd 100644 --- a/src/reanimated2/layoutReanimation/web/componentUtils.ts +++ b/src/reanimated2/layoutReanimation/web/componentUtils.ts @@ -136,11 +136,20 @@ export function getProcessedConfig( }; } -export function makeElementVisible(element: HTMLElement) { - _updatePropsJS( - { visibility: 'initial' }, - { _component: element as ReanimatedHTMLElement } - ); +export function makeElementVisible(element: HTMLElement, delay: number) { + if (delay === 0) { + _updatePropsJS( + { visibility: 'initial' }, + { _component: element as ReanimatedHTMLElement } + ); + } else { + setTimeout(() => { + _updatePropsJS( + { visibility: 'initial' }, + { _component: element as ReanimatedHTMLElement } + ); + }, delay * 1000); + } } export function setElementAnimation(