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.
BEFORE | AFTER |
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 (
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
box: {
width: 100,
height: 100,
backgroundColor: 'blue',
},
});
```
---------
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(