Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate last components to reanimated #55202

14 changes: 13 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,26 @@ const path = require('path');
const restrictedImportPaths = [
{
name: 'react-native',
importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', 'Text', 'ScrollView'],
importNames: [
'useWindowDimensions',
'StatusBar',
'TouchableOpacity',
'TouchableWithoutFeedback',
'TouchableNativeFeedback',
'TouchableHighlight',
'Pressable',
'Text',
'ScrollView',
'Animated',
],
message: [
'',
"For 'useWindowDimensions', please use '@src/hooks/useWindowDimensions' instead.",
"For 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from '@components/Pressable' instead.",
"For 'StatusBar', please use '@libs/StatusBar' instead.",
"For 'Text', please use '@components/Text' instead.",
"For 'ScrollView', please use '@components/ScrollView' instead.",
"For 'Animated', please use 'Animated' from 'react-native-reanimated' instead.",
].join('\n'),
},
{
Expand Down
38 changes: 29 additions & 9 deletions contributingGuides/TS_CHEATSHEET.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,22 +100,42 @@
<a name="animated-style"></a><a name="1.4"></a>

- [1.4](#animated-style) **Animated styles**


The only recommended way to create animations is by using the
`react-native-reanimated` library because it is much more efficient and convenient
than using `Animated` directly from `React Native`.
sumo-slonik marked this conversation as resolved.
Show resolved Hide resolved
```ts
import {useRef} from 'react';
import {Animated, StyleProp, ViewStyle} from 'react-native';
import React from 'react';
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import Animated, { useAnimatedStyle, useSharedValue, withTiming, SharedValue, WithTimingConfig } from 'react-native-reanimated';

type MyComponentProps = {
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
style: {
opacity: SharedValue<number>;
};
};

function MyComponent({ style }: MyComponentProps) {
return <Animated.View style={style} />;
const animatedStyle = useAnimatedStyle(() => {
return {
opacity: style.opacity.get(),
};
});

return <Animated.View style={animatedStyle}/>;
}

function App() {
const anim = useRef(new Animated.Value(0)).current;
return <MyComponent style={{opacity: anim.interpolate({...})}} />;
const opacity = useSharedValue(0);

const animate = () => {
opacity.set(withTiming(1, {
duration: 1000,
easing: Easing.inOut(Easing.quad),
reduceMotion: ReduceMotion.System,
}))}

return <MyComponent style={{ opacity }} />;
}
```
sumo-slonik marked this conversation as resolved.
Show resolved Hide resolved

Expand Down
1 change: 1 addition & 0 deletions src/components/TabSelector/TabIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
// eslint-disable-next-line no-restricted-imports
import {Animated, StyleSheet, View} from 'react-native';
import Icon from '@components/Icon';
import useTheme from '@hooks/useTheme';
Expand Down
1 change: 1 addition & 0 deletions src/components/TabSelector/TabLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
// eslint-disable-next-line no-restricted-imports
import {Animated, StyleSheet, View} from 'react-native';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down
3 changes: 2 additions & 1 deletion src/components/TabSelector/TabSelectorItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, {useState} from 'react';
// eslint-disable-next-line no-restricted-imports
import {Animated} from 'react-native';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -51,7 +52,7 @@ function TabSelectorItem({
return (
<AnimatedPressableWithFeedback
accessibilityLabel={title}
style={[styles.tabSelectorButton, styles.tabBackground(isHovered, isActive, backgroundColor), styles.userSelectNone]}
style={[styles.tabSelectorButton, styles.animatedTabBackground(isHovered, isActive, backgroundColor), styles.userSelectNone]}
wrapperStyle={[styles.flexGrow1]}
onPress={onPress}
onHoverIn={() => setIsHovered(true)}
Expand Down
1 change: 1 addition & 0 deletions src/components/TabSelector/getBackground/index.native.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import type {Animated} from 'react-native';
import type GetBackgroudColor from './types';

Expand Down
1 change: 1 addition & 0 deletions src/components/TabSelector/getBackground/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import type {Animated} from 'react-native';
import type {ThemeColors} from '@styles/theme/types';

Expand Down
1 change: 1 addition & 0 deletions src/components/TabSelector/getOpacity/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import type {Animated} from 'react-native';

/**
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useTabNavigatorFocus/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {useTabAnimation} from '@react-navigation/material-top-tabs';
import {useIsFocused} from '@react-navigation/native';
import {useEffect, useState} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {Animated} from 'react-native';
import DomUtils from '@libs/DomUtils';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import {useCardAnimation} from '@react-navigation/stack';
import React from 'react';
// eslint-disable-next-line no-restricted-imports
import {Animated, View} from 'react-native';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';

type BaseOverlayProps = {
/* Whether to use native styles tailored for native devices */
shouldUseNativeStyles: boolean;

/* Callback to close the modal */
onPress?: () => void;

/* Returns whether a modal is displayed on the left side of the screen. By default, the modal is displayed on the right */
isModalOnTheLeft?: boolean;
};

function BaseOverlay({shouldUseNativeStyles, onPress, isModalOnTheLeft = false}: BaseOverlayProps) {
function BaseOverlay({onPress, isModalOnTheLeft = false}: BaseOverlayProps) {
const styles = useThemeStyles();
const {current} = useCardAnimation();
const {translate} = useLocalize();

return (
<Animated.View
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sumo-slonik Is there a reason why this component is not being migrated to Reanimated?

Copy link
Contributor Author

@sumo-slonik sumo-slonik Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseOverlay also utilizes react-navigation but in this case, it involves the import:
import { useCardAnimation } from '@react-navigation/stack';
Later, we use useCardAnimation to create opacity in the styles for Animated.View to obtain the animation progress like this:

opacity: current.progress.interpolate({
    inputRange: [0, 1],
    outputRange: [0, variables.overlayOpacity],
    extrapolate: 'clamp',
}),

Due to this integration with react-navigation, migrating this component to Reanimated is not possible.

The useReanimatedTransitionProgress() from react-native-screens could potentially address the issue on native platforms. However, on those platforms, this component appears as follows:

function Overlay() {
    return null;
}

Overlay.displayName = 'Overlay';
export default Overlay;

, so migration is not necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mountiny Do you agree with this? Asking for confirmation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the explanation sounds good to me

id="BaseOverlay"
style={shouldUseNativeStyles ? styles.nativeOverlayStyles(current) : styles.overlayStyles(current, isModalOnTheLeft)}
style={styles.overlayStyles(current, isModalOnTheLeft)}
>
<View style={[styles.flex1, styles.flexColumn]}>
{/* In the latest Electron version buttons can't be both clickable and draggable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import BaseOverlay from './BaseOverlay';
function Overlay({...rest}: Omit<BaseOverlayProps, 'shouldUseNativeStyles'>) {
return (
<BaseOverlay
shouldUseNativeStyles={false}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {StackCardInterpolatedStyle, StackCardInterpolationProps} from '@react-navigation/stack';
// eslint-disable-next-line no-restricted-imports
import {Animated} from 'react-native';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
Expand Down
6 changes: 3 additions & 3 deletions src/pages/Search/SearchTypeMenuNarrow.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {EventArg, NavigationContainerEventMap} from '@react-navigation/native';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {Animated, View} from 'react-native';
import {View} from 'react-native';
import type {TextStyle, ViewStyle} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
Expand Down Expand Up @@ -207,7 +207,7 @@ function SearchTypeMenuNarrow({typeMenuItems, activeItemIndex, queryJSON, title,
onPress={openMenu}
>
{({hovered}) => (
<Animated.View style={[styles.tabSelectorButton, styles.tabBackground(hovered, true, theme.border), styles.w100, StyleUtils.getHeight(variables.componentSizeNormal)]}>
<View style={[styles.tabSelectorButton, styles.tabBackground(hovered, true, theme.border), styles.w100, StyleUtils.getHeight(variables.componentSizeNormal)]}>
<View style={[styles.flexRow, styles.gap2, styles.alignItemsCenter, titleViewStyles]}>
<Icon
src={menuIcon}
Expand All @@ -226,7 +226,7 @@ function SearchTypeMenuNarrow({typeMenuItems, activeItemIndex, queryJSON, title,
small
/>
</View>
</Animated.View>
</View>
)}
</PressableWithFeedback>
<EducationalTooltip
Expand Down
20 changes: 6 additions & 14 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type {LineLayerStyleProps} from '@rnmapbox/maps/src/utils/MapboxStyles';
import lodashClamp from 'lodash/clamp';
import type {LineLayer} from 'react-map-gl';
// eslint-disable-next-line no-restricted-imports
import type {Animated, ImageStyle, TextStyle, ViewStyle} from 'react-native';
import {Platform, StyleSheet} from 'react-native';
import type {CustomAnimation} from 'react-native-animatable';
Expand Down Expand Up @@ -2013,19 +2014,6 @@ const styles = (theme: ThemeColors) =>
}),
} satisfies ViewStyle),

nativeOverlayStyles: (current: OverlayStylesParams) =>
({
position: 'absolute',
backgroundColor: theme.overlay,
width: '100%',
height: '100%',
opacity: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [0, variables.overlayOpacity],
extrapolate: 'clamp',
}),
} satisfies ViewStyle),

appContent: {
backgroundColor: theme.appBG,
overflow: 'hidden',
Expand Down Expand Up @@ -4371,7 +4359,11 @@ const styles = (theme: ThemeColors) =>
fontSize: variables.fontSizeLabel,
} satisfies TextStyle),

tabBackground: (hovered: boolean, isFocused: boolean, background: string | Animated.AnimatedInterpolation<string>) => ({
animatedTabBackground: (hovered: boolean, isFocused: boolean, background: string | Animated.AnimatedInterpolation<string>) => ({
backgroundColor: hovered && !isFocused ? theme.highlightBG : background,
}),

tabBackground: (hovered: boolean, isFocused: boolean, background: string) => ({
backgroundColor: hovered && !isFocused ? theme.highlightBG : background,
}),

Expand Down
22 changes: 6 additions & 16 deletions src/styles/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {StyleSheet} from 'react-native';
import type {AnimatableNumericValue, Animated, ColorValue, ImageStyle, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native';
import type {AnimatableNumericValue, ColorValue, ImageStyle, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {EdgeInsets} from 'react-native-safe-area-context';
import type {ValueOf} from 'type-fest';
import type ImageSVGProps from '@components/ImageSVG/types';
import * as Browser from '@libs/Browser';
import {isMobile} from '@libs/Browser';
import getPlatform from '@libs/getPlatform';
import * as UserUtils from '@libs/UserUtils';
import {hashText} from '@libs/UserUtils';
// eslint-disable-next-line no-restricted-imports
import {defaultTheme} from '@styles/theme';
import colors from '@styles/theme/colors';
Expand Down Expand Up @@ -275,7 +275,7 @@ function getAvatarBorderStyle(size: AvatarSizeName, type: string): ViewStyle {
* Helper method to return workspace avatar color styles
*/
function getDefaultWorkspaceAvatarColor(text: string): ViewStyle {
const colorHash = UserUtils.hashText(text.trim(), workspaceColorOptions.length);
const colorHash = hashText(text.trim(), workspaceColorOptions.length);
return workspaceColorOptions.at(colorHash) ?? {backgroundColor: colors.blue200, fill: colors.blue700};
}

Expand All @@ -296,7 +296,7 @@ function getEReceiptColorCode(transaction: OnyxEntry<Transaction>): EReceiptColo
return CONST.ERECEIPT_COLORS.YELLOW;
}

const colorHash = UserUtils.hashText(transactionID.trim(), eReceiptColors.length);
const colorHash = hashText(transactionID.trim(), eReceiptColors.length);

return eReceiptColors.at(colorHash) ?? CONST.ERECEIPT_COLORS.YELLOW;
}
Expand Down Expand Up @@ -747,15 +747,6 @@ function getMaximumWidth(maxWidth: number): ViewStyle {
};
}

/**
* Return style for opacity animation.
*/
function fade(fadeAnimation: Animated.Value): Animated.WithAnimatedValue<ViewStyle> {
return {
opacity: fadeAnimation,
};
}

type AvatarBorderStyleParams = {
theme: ThemeColors;
isHovered: boolean;
Expand Down Expand Up @@ -971,7 +962,7 @@ function getComposeTextAreaPadding(isComposerFullSize: boolean): TextStyle {
* Returns style object for the mobile on WEB
*/
function getOuterModalStyle(windowHeight: number, viewportOffsetTop: number): ViewStyle {
return Browser.isMobile() ? {maxHeight: windowHeight, marginTop: viewportOffsetTop} : {};
return isMobile() ? {maxHeight: windowHeight, marginTop: viewportOffsetTop} : {};
}

/**
Expand Down Expand Up @@ -1192,7 +1183,6 @@ const staticStyleUtils = {
getMinimumWidth,
getMaximumHeight,
getMaximumWidth,
fade,
getHorizontalStackedAvatarBorderStyle,
getHorizontalStackedAvatarStyle,
getHorizontalStackedOverlayAvatarStyle,
Expand Down
Loading