diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx index 114ca59e20..dad82fcc74 100644 --- a/src/components/Appbar/Appbar.tsx +++ b/src/components/Appbar/Appbar.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Animated, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import type { ColorValue, StyleProp, ViewProps, ViewStyle } from 'react-native'; import AppbarContent from './AppbarContent'; @@ -51,7 +51,7 @@ export type Props = Omit, 'style'> & { * @optional */ theme?: ThemeProp; - style?: Animated.WithAnimatedValue>; + style?: StyleProp; }; /** diff --git a/src/components/Appbar/AppbarAction.tsx b/src/components/Appbar/AppbarAction.tsx index 96e802a34f..25a36a43f1 100644 --- a/src/components/Appbar/AppbarAction.tsx +++ b/src/components/Appbar/AppbarAction.tsx @@ -1,11 +1,5 @@ import * as React from 'react'; -import type { - Animated, - ColorValue, - StyleProp, - View, - ViewStyle, -} from 'react-native'; +import type { ColorValue, StyleProp, View, ViewStyle } from 'react-native'; import { useInternalTheme } from '../../core/theming'; import type { Theme, ThemeProp } from '../../types'; @@ -43,7 +37,7 @@ export type Props = React.ComponentPropsWithoutRef & { * Whether it's the leading button. Note: If `Appbar.BackAction` is present, it will be rendered before any `isLeading` icons. */ isLeading?: boolean; - style?: Animated.WithAnimatedValue>; + style?: StyleProp; ref?: React.Ref; /** * @optional diff --git a/src/components/Appbar/AppbarBackAction.tsx b/src/components/Appbar/AppbarBackAction.tsx index 2835c27c91..ff3ab8a402 100644 --- a/src/components/Appbar/AppbarBackAction.tsx +++ b/src/components/Appbar/AppbarBackAction.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import type { - Animated, ColorValue, GestureResponderEvent, StyleProp, @@ -36,7 +35,7 @@ export type Props = $Omit< * Function to execute on press. */ onPress?: (e: GestureResponderEvent) => void; - style?: Animated.WithAnimatedValue>; + style?: StyleProp; ref?: React.Ref; }; diff --git a/src/components/Appbar/AppbarHeader.tsx b/src/components/Appbar/AppbarHeader.tsx index d869242046..fdaf44b201 100644 --- a/src/components/Appbar/AppbarHeader.tsx +++ b/src/components/Appbar/AppbarHeader.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Animated, Platform, StyleSheet, View } from 'react-native'; +import { Platform, StyleSheet, View } from 'react-native'; import type { ColorValue, StyleProp, ViewStyle } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -52,7 +52,7 @@ export type Props = Omit< * @optional */ theme?: ThemeProp; - style?: Animated.WithAnimatedValue>; + style?: StyleProp; }; /** diff --git a/src/components/Appbar/utils.ts b/src/components/Appbar/utils.ts index fbd2e7f461..0fc9e42e45 100644 --- a/src/components/Appbar/utils.ts +++ b/src/components/Appbar/utils.ts @@ -1,6 +1,6 @@ import React from 'react'; import type { ColorValue, StyleProp, ViewStyle } from 'react-native'; -import { StyleSheet, Animated } from 'react-native'; +import { StyleSheet } from 'react-native'; import { white } from '../../theme/colors'; import type { InternalTheme, Theme, ThemeProp } from '../../types'; @@ -54,17 +54,12 @@ export const getAppbarColor = ({ return undefined; }; -export const getAppbarBorders = ( - style: - | Animated.Value - | Animated.AnimatedInterpolation - | Animated.WithAnimatedObject -) => { +export const getAppbarBorders = (style: ViewStyle) => { const borders: Record = {}; for (const property of borderStyleProperties) { const value = style[property as keyof typeof style]; - if (value) { + if (typeof value === 'number') { borders[property] = value; } } diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index 4f28cf0390..b575c32ae8 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { Animated, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import type { StyleProp, ViewStyle } from 'react-native'; import type { LayoutChangeEvent } from 'react-native'; +import Animated from 'react-native-reanimated'; import useLatestCallback from 'use-latest-callback'; import Button from './Button/Button'; @@ -15,6 +16,8 @@ import type { $Omit, $RemoveChildren, Theme, ThemeProp } from '../types'; const DEFAULT_MAX_WIDTH = 960; +type AnimationFinishedCallback = (result: { finished: boolean }) => void; + export type Props = $Omit<$RemoveChildren, 'mode'> & { /** * Whether banner is currently visible. @@ -51,12 +54,12 @@ export type Props = $Omit<$RemoveChildren, 'mode'> & { * @supported Available in v5.x with theme version 3 * Changes Banner shadow and background on iOS and Android. */ - elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value; + elevation?: 0 | 1 | 2 | 3 | 4 | 5; /** * Specifies the largest possible scale a text font can reach. */ maxFontSizeMultiplier?: number; - style?: Animated.WithAnimatedValue>; + style?: StyleProp; ref?: React.RefObject; /** * @optional @@ -66,12 +69,12 @@ export type Props = $Omit<$RemoveChildren, 'mode'> & { * @optional * Optional callback that will be called after the opening animation finished running normally */ - onShowAnimationFinished?: Animated.EndCallback; + onShowAnimationFinished?: AnimationFinishedCallback; /** * @optional * Optional callback that will be called after the closing animation finished running normally */ - onHideAnimationFinished?: Animated.EndCallback; + onHideAnimationFinished?: AnimationFinishedCallback; }; /** @@ -134,9 +137,6 @@ const Banner = ({ }: Props) => { const theme = useInternalTheme(themeOverrides); const { colors } = theme as Theme; - const { current: position } = React.useRef( - new Animated.Value(visible ? 1 : 0) - ); const [layout, setLayout] = React.useState<{ height: number; measured: boolean; @@ -149,30 +149,18 @@ const Banner = ({ const hideCallback = useLatestCallback(onHideAnimationFinished); const { scale } = theme.animation; - - const opacity = position.interpolate({ - inputRange: [0, 0.1, 1], - outputRange: [0, 1, 1], - }); + const animationDuration = (visible ? 250 : 200) * scale; + const opacity = visible ? 1 : 0; React.useEffect(() => { - if (visible) { - // show - Animated.timing(position, { - duration: 250 * scale, - toValue: 1, - useNativeDriver: false, - }).start(showCallback); - } else { - // hide - Animated.timing(position, { - duration: 200 * scale, - toValue: 0, - useNativeDriver: false, - }).start(hideCallback); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [visible, position, scale]); + const callback = visible ? showCallback : hideCallback; + const timeout = setTimeout( + () => callback({ finished: true }), + animationDuration + ); + + return () => clearTimeout(timeout); + }, [animationDuration, hideCallback, showCallback, visible]); const handleLayout = ({ nativeEvent }: LayoutChangeEvent) => { const { height } = nativeEvent.layout; @@ -186,29 +174,48 @@ const Banner = ({ // Once we have the height, we apply the height to the spacer and switch the banner to position: absolute // We need this because we need to move the content below as if banner's height was being animated // However we can't animated banner's height directly as it'll also resize the content inside - const height = Animated.multiply(position, layout.height); + const height = visible ? layout.height : 0; + const translateY = visible ? 0 : -layout.height; + const opacityTransitionStyle: React.ComponentProps< + typeof Animated.View + >['style'] = { + transitionDuration: animationDuration, + transitionProperty: 'opacity', + }; + const heightTransitionStyle: React.ComponentProps< + typeof Animated.View + >['style'] = { + transitionDuration: animationDuration, + transitionProperty: 'height', + }; + const transformTransitionStyle: React.ComponentProps< + typeof Animated.View + >['style'] = { + transitionDuration: animationDuration, + transitionProperty: 'transform', + }; - const translateY = Animated.multiply( - Animated.add(position, -1), - layout.height - ); return ( - + ({ }; return ( - ({ : 'auto' : 'none' } - onLayout={onLayout} - container > - - - {routes.map((route, index) => { - const focused = navigationState.index === index; - const active = tabsAnims[index]; - - // Move down the icon to account for no-label in shifting and smaller label in non-shifting. - const translateY = labeled - ? shifting + + {routes.map((route, index) => { + const focused = navigationState.index === index; + const active = tabsAnims[index]; + + // Move down the icon to account for no-label in shifting and smaller label in non-shifting. + const translateY = labeled + ? shifting + ? active.interpolate({ + inputRange: [0, 1], + outputRange: [7, 0], + }) + : 0 + : 7; + + // We render the active icon and label on top of inactive ones and cross-fade them on change. + // This trick gives the illusion that we are animating between active and inactive colors. + // This is to ensure that we can use native driver, as colors cannot be animated with native driver. + const activeOpacity = active; + const inactiveOpacity = active.interpolate({ + inputRange: [0, 1], + outputRange: [1, 0], + }); + + const v3ActiveOpacity = focused ? 1 : 0; + const v3InactiveOpacity = shifting + ? inactiveOpacity + : focused + ? 0 + : 1; + + // Scale horizontally the outline pill + const outlineScale = focused ? active.interpolate({ inputRange: [0, 1], - outputRange: [7, 0], + outputRange: [0.5, 1], }) - : 0 - : 7; - - // We render the active icon and label on top of inactive ones and cross-fade them on change. - // This trick gives the illusion that we are animating between active and inactive colors. - // This is to ensure that we can use native driver, as colors cannot be animated with native driver. - const activeOpacity = active; - const inactiveOpacity = active.interpolate({ - inputRange: [0, 1], - outputRange: [1, 0], - }); - - const v3ActiveOpacity = focused ? 1 : 0; - const v3InactiveOpacity = shifting - ? inactiveOpacity - : focused - ? 0 - : 1; - - // Scale horizontally the outline pill - const outlineScale = focused - ? active.interpolate({ - inputRange: [0, 1], - outputRange: [0.5, 1], - }) - : 0; - - const badge = getBadge({ route }); - - const activeLabelColor = getLabelColor({ - tintColor: activeTintColor, - hasColor: Boolean(activeColor), - focused, - theme, - }); - - const inactiveLabelColor = getLabelColor({ - tintColor: inactiveTintColor, - hasColor: Boolean(inactiveColor), - focused, - theme, - }); - - const badgeStyle = { - top: typeof badge === 'boolean' ? 4 : 2, - right: - badge != null && typeof badge !== 'boolean' - ? String(badge).length * -2 - : 0, - }; - - const isLegacyOrV3Shifting = shifting && labeled; - - const font = (theme as Theme).fonts.labelMedium; - - return renderTouchable({ - key: route.key, - route, - borderless: true, - centered: true, - rippleColor: 'transparent', - onPress: () => onTabPress(eventForIndex(index)), - onLongPress: () => onTabLongPress?.(eventForIndex(index)), - testID: getTestID({ route }), - 'aria-label': getAccessibilityLabel({ route }), - role: Platform.OS === 'ios' ? 'button' : 'tab', - 'aria-selected': focused, - style: [styles.item, styles.v3Item], - children: ( - - onTabPress(eventForIndex(index)), + onLongPress: () => onTabLongPress?.(eventForIndex(index)), + testID: getTestID({ route }), + 'aria-label': getAccessibilityLabel({ route }), + role: Platform.OS === 'ios' ? 'button' : 'tab', + 'aria-selected': focused, + style: [styles.item, styles.v3Item], + children: ( + - {focused && ( - - )} - - {renderIcon ? ( - renderIcon({ - route, - focused: true, - color: activeTintColor, - }) - ) : ( - - )} - - {renderIcon ? ( - renderIcon({ - route, - focused: false, - color: inactiveTintColor, - }) - ) : ( - )} - - - {typeof badge === 'boolean' ? ( - - ) : ( - {badge} - )} - - - {labeled ? ( - ({ }, ]} > - {renderLabel ? ( - renderLabel({ + {renderIcon ? ( + renderIcon({ route, focused: true, - color: activeLabelColor, + color: activeTintColor, }) ) : ( - - {getLabelText({ route })} - + )} - {shifting ? null : ( + + {renderIcon ? ( + renderIcon({ + route, + focused: false, + color: inactiveTintColor, + }) + ) : ( + + )} + + + {typeof badge === 'boolean' ? ( + + ) : ( + {badge} + )} + + + {labeled ? ( + {renderLabel ? ( renderLabel({ route, - focused: false, - color: inactiveLabelColor, + focused: true, + color: activeLabelColor, }) ) : ( ({ )} - )} - - ) : null} - - ), - }); - })} - - - + {shifting ? null : ( + + {renderLabel ? ( + renderLabel({ + route, + focused: false, + color: inactiveLabelColor, + }) + ) : ( + + {getLabelText({ route })} + + )} + + )} + + ) : null} + + ), + }); + })} + + + + ); }; diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 53d61a2d6f..508e687b31 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Animated, Platform, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import type { ColorValue, GestureResponderEvent, @@ -122,7 +122,7 @@ export type Props = $Omit, 'mode'> & { * Sets additional distance outside of element in which a press can be detected. */ hitSlop?: TouchableRippleProps['hitSlop']; - style?: Animated.WithAnimatedValue>; + style?: StyleProp; /** * Style for the button text. */ @@ -198,9 +198,7 @@ const Button = ({ }, [mode] ); - const { animation } = theme; const uppercase = uppercaseProp ?? false; - const isWeb = Platform.OS === 'web'; const hasPassedTouchHandler = hasTouchHandler({ onPress, @@ -213,43 +211,24 @@ const Button = ({ const initialElevation = 1; const activeElevation = 2; - const { current: elevation } = React.useRef( - new Animated.Value(isElevationEntitled ? initialElevation : 0) - ); - - React.useEffect(() => { - // Workaround not to call setValue on Animated.Value, because it breaks styles. - // https://github.com/callstack/react-native-paper/issues/4559 - Animated.timing(elevation, { - toValue: isElevationEntitled ? initialElevation : 0, - duration: 0, - useNativeDriver: true, - }); - }, [isElevationEntitled, elevation, initialElevation]); + const [pressed, setPressed] = React.useState(false); + const elevation = isElevationEntitled + ? pressed + ? activeElevation + : initialElevation + : 0; const handlePressIn = (e: GestureResponderEvent) => { onPressIn?.(e); if (isMode('elevated')) { - const { scale } = animation; - Animated.timing(elevation, { - toValue: activeElevation, - duration: 200 * scale, - useNativeDriver: - isWeb || Platform.constants.reactNativeVersion.minor <= 72, - }).start(); + setPressed(true); } }; const handlePressOut = (e: GestureResponderEvent) => { onPressOut?.(e); if (isMode('elevated')) { - const { scale } = animation; - Animated.timing(elevation, { - toValue: initialElevation, - duration: 150 * scale, - useNativeDriver: - isWeb || Platform.constants.reactNativeVersion.minor <= 72, - }).start(); + setPressed(false); } }; @@ -320,14 +299,7 @@ const Button = ({ {...rest} ref={ref} testID={`${testID}-container`} - style={ - [ - styles.button, - compact && styles.compact, - buttonStyle, - style, - ] as Animated.WithAnimatedValue> - } + style={[styles.button, compact && styles.compact, buttonStyle, style]} elevation={elevation} container > diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx index e1cc58c52a..ff516d924b 100644 --- a/src/components/Card/Card.tsx +++ b/src/components/Card/Card.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Animated, StyleSheet, Pressable, View } from 'react-native'; +import { StyleSheet, Pressable, View } from 'react-native'; import type { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native'; import useLatestCallback from 'use-latest-callback'; @@ -73,12 +73,12 @@ export type Props = $Omit, 'mode'> & { /** * Changes Card shadow and background on iOS and Android. */ - elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value; + elevation?: 0 | 1 | 2 | 3 | 4 | 5; /** * Style of card's inner content. */ contentStyle?: StyleProp; - style?: Animated.WithAnimatedValue>; + style?: StyleProp; /** * @optional */ @@ -155,12 +155,8 @@ const Card = ({ onPressOut, }); - const { current: elevation } = React.useRef( - new Animated.Value(cardElevation) - ); - const { animation } = theme; - - const animationDuration = 150 * animation.scale; + const [pressed, setPressed] = React.useState(false); + const elevation = isMode('elevated') ? (pressed ? 2 : cardElevation) : 0; const runElevationAnimation = (pressType: HandlePressType) => { if (isMode('contained')) { @@ -168,11 +164,7 @@ const Card = ({ } const isPressTypeIn = pressType === 'in'; - Animated.timing(elevation, { - toValue: isPressTypeIn ? 2 : cardElevation, - duration: animationDuration, - useNativeDriver: false, - }).start(); + setPressed(isPressTypeIn); }; const handlePressIn = useLatestCallback((e: GestureResponderEvent) => { @@ -235,7 +227,7 @@ const Card = ({ style, ]} theme={theme} - elevation={isMode('elevated') ? elevation : 0} + elevation={elevation} testID={`${testID}-container`} container {...rest} diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx index 1b09c327a8..81e5b5b40e 100644 --- a/src/components/Chip/Chip.tsx +++ b/src/components/Chip/Chip.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Animated, Platform, StyleSheet, Pressable, View } from 'react-native'; +import { Platform, StyleSheet, Pressable, View } from 'react-native'; import type { ColorValue, GestureResponderEvent, @@ -123,7 +123,7 @@ export type Props = $Omit, 'mode'> & { * Style of chip's text */ textStyle?: StyleProp; - style?: Animated.WithAnimatedValue>; + style?: StyleProp; /** * Sets additional distance outside of element in which a press can be detected. */ @@ -201,11 +201,9 @@ const Chip = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); - const isWeb = Platform.OS === 'web'; - const { current: elevation } = React.useRef( - new Animated.Value(elevated ? 1 : 0) - ); + const [pressed, setPressed] = React.useState(false); + const elevation = elevated ? (pressed ? 2 : 1) : 0; const hasPassedTouchHandler = hasTouchHandler({ onPress, @@ -217,25 +215,13 @@ const Chip = ({ const isOutlined = mode === 'outlined'; const handlePressIn = useLatestCallback((e: GestureResponderEvent) => { - const { scale } = theme.animation; onPressIn?.(e); - Animated.timing(elevation, { - toValue: elevated ? 2 : 0, - duration: 200 * scale, - useNativeDriver: - isWeb || Platform.constants.reactNativeVersion.minor <= 72, - }).start(); + setPressed(true); }); const handlePressOut = useLatestCallback((e: GestureResponderEvent) => { - const { scale } = theme.animation; onPressOut?.(e); - Animated.timing(elevation, { - toValue: elevated ? 1 : 0, - duration: 150 * scale, - useNativeDriver: - isWeb || Platform.constants.reactNativeVersion.minor <= 72, - }).start(); + setPressed(false); }); const opacity = 0.38; diff --git a/src/components/CrossFadeIcon.tsx b/src/components/CrossFadeIcon.tsx index db1fd244d8..f1a1a1a75a 100644 --- a/src/components/CrossFadeIcon.tsx +++ b/src/components/CrossFadeIcon.tsx @@ -1,7 +1,14 @@ import * as React from 'react'; -import { Animated, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import type { ColorValue } from 'react-native'; +import Animated, { + interpolate, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated'; + import Icon, { isEqualIcon, isValidIcon } from './Icon'; import type { IconSource } from './Icon'; import { useInternalTheme } from '../core/theming'; @@ -44,7 +51,7 @@ const CrossFadeIcon = ({ const [previousIcon, setPreviousIcon] = React.useState( null ); - const { current: fade } = React.useRef(new Animated.Value(1)); + const fade = useSharedValue(1); const { scale } = theme.animation; @@ -55,35 +62,32 @@ const CrossFadeIcon = ({ React.useEffect(() => { if (isValidIcon(previousIcon) && !isEqualIcon(previousIcon, currentIcon)) { - fade.setValue(1); - - Animated.timing(fade, { + fade.value = 1; + fade.value = withTiming(0, { duration: scale * 200, - toValue: 0, - useNativeDriver: true, - }).start(); + }); } }, [currentIcon, previousIcon, fade, scale]); - const opacityPrev = fade; - const opacityNext = previousIcon - ? fade.interpolate({ - inputRange: [0, 1], - outputRange: [1, 0], - }) - : 1; - - const rotatePrev = fade.interpolate({ - inputRange: [0, 1], - outputRange: ['-90deg', '0deg'], - }); + const previousIconStyle = useAnimatedStyle(() => ({ + opacity: fade.value, + transform: [ + { + rotate: `${interpolate(fade.value, [0, 1], [-90, 0])}deg`, + }, + ], + })); - const rotateNext = previousIcon - ? fade.interpolate({ - inputRange: [0, 1], - outputRange: ['0deg', '-180deg'], - }) - : '0deg'; + const currentIconStyle = useAnimatedStyle(() => ({ + opacity: previousIcon ? interpolate(fade.value, [0, 1], [1, 0]) : 1, + transform: [ + { + rotate: `${ + previousIcon ? interpolate(fade.value, [0, 1], [0, -180]) : 0 + }deg`, + }, + ], + })); return ( {previousIcon ? ( ) : null} diff --git a/src/components/DataTable/DataTableTitle.tsx b/src/components/DataTable/DataTableTitle.tsx index 4a972341e6..0311f5a77c 100644 --- a/src/components/DataTable/DataTableTitle.tsx +++ b/src/components/DataTable/DataTableTitle.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Animated, PixelRatio, Pressable, StyleSheet } from 'react-native'; +import { PixelRatio, Pressable, StyleSheet } from 'react-native'; import type { GestureResponderEvent, PressableProps, @@ -8,12 +8,19 @@ import type { ViewStyle, } from 'react-native'; +import Animated from 'react-native-reanimated'; + import { useLocale } from '../../core/locale'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; import MaterialCommunityIcon from '../MaterialCommunityIcon'; import Text from '../Typography/Text'; +const iconTransitionStyle = { + transitionDuration: 150, + transitionProperty: 'transform' as const, +}; + export type Props = PressableProps & { /** * Text content of the `DataTableTitle`. @@ -90,29 +97,23 @@ const DataTableTitle = ({ }: Props) => { const theme = useInternalTheme(themeOverrides); const { direction } = useLocale(); - const { current: spinAnim } = React.useRef( - new Animated.Value(sortDirection === 'ascending' ? 0 : 1) - ); - - React.useEffect(() => { - Animated.timing(spinAnim, { - toValue: sortDirection === 'ascending' ? 0 : 1, - duration: 150, - useNativeDriver: true, - }).start(); - }, [sortDirection, spinAnim]); const textColor = theme.colors.onSurface; const alphaTextColor = theme.colors.onSurfaceVariant; - const spin = spinAnim.interpolate({ - inputRange: [0, 1], - outputRange: ['0deg', '180deg'], - }); + const spin = sortDirection === 'ascending' ? '0deg' : '180deg'; const icon = sortDirection ? ( - + >; + style?: StyleProp; /** * @optional */ diff --git a/src/components/Drawer/DrawerCollapsedItem.tsx b/src/components/Drawer/DrawerCollapsedItem.tsx index 6e4fdebaa6..f90330650e 100644 --- a/src/components/Drawer/DrawerCollapsedItem.tsx +++ b/src/components/Drawer/DrawerCollapsedItem.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Animated, Platform, Pressable, StyleSheet, View } from 'react-native'; +import { Platform, Pressable, StyleSheet, View } from 'react-native'; import type { GestureResponderEvent, NativeSyntheticEvent, @@ -9,6 +9,8 @@ import type { ViewStyle, } from 'react-native'; +import Animated from 'react-native-reanimated'; + import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; import Badge from '../Badge'; @@ -70,6 +72,9 @@ export type Props = ViewProps & { const iconSize = 24; const itemSize = 56; const outlineHeight = 32; +const outlineTransitionStyle = { + transitionProperty: 'transform' as const, +}; /** * Note: Available in v5.x with theme version 3 @@ -112,22 +117,21 @@ const DrawerCollapsedItem = ({ const [numOfLines, setNumOfLines] = React.useState(1); - const { current: animScale } = React.useRef( - new Animated.Value(active ? 1 : 0.5) - ); + const [prevActive, setPrevActive] = React.useState(active); + const [pressedOut, setPressedOut] = React.useState(false); + + if (prevActive !== active) { + setPrevActive(active); - React.useEffect(() => { if (!active) { - animScale.setValue(0.5); + setPressedOut(false); } - }, [animScale, active]); + } + + const outlineScale = active || pressedOut ? 1 : 0.5; const handlePressOut = () => { - Animated.timing(animScale, { - toValue: 1, - duration: 150 * scale, - useNativeDriver: true, - }).start(); + setPressedOut(true); }; const iconPadding = ((!label ? itemSize : outlineHeight) - iconSize) / 2; @@ -181,12 +185,14 @@ const DrawerCollapsedItem = ({ transform: [ label ? { - scaleX: animScale, + scaleX: outlineScale, } - : { scale: animScale }, + : { scale: outlineScale }, ], backgroundColor, + transitionDuration: 150 * scale, }, + outlineTransitionStyle, style, ]} testID={`${testID}-outline`} diff --git a/src/components/IconButton/IconButton.tsx b/src/components/IconButton/IconButton.tsx index 52a64e98d0..44ad3fb8d4 100644 --- a/src/components/IconButton/IconButton.tsx +++ b/src/components/IconButton/IconButton.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Animated, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import type { ColorValue, GestureResponderEvent, @@ -69,7 +69,7 @@ export type Props = Omit<$RemoveChildren, 'style'> & { * Function to execute on press. */ onPress?: (e: GestureResponderEvent) => void; - style?: Animated.WithAnimatedValue>; + style?: StyleProp; ref?: React.Ref; /** * TestID used for testing purposes diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index ca6e849e3d..c0aaf3266d 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -1,14 +1,12 @@ import * as React from 'react'; import { - Animated, Dimensions, - Easing, Keyboard, Platform, + Pressable, ScrollView, StyleSheet, View, - Pressable, } from 'react-native'; import type { KeyboardEvent as RNKeyboardEvent } from 'react-native'; import type { @@ -20,6 +18,7 @@ import type { ViewStyle, } from 'react-native'; +import Animated from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import MenuItem from './MenuItem'; @@ -67,7 +66,7 @@ export type Props = { /** * Style of menu's inner content. */ - contentStyle?: Animated.WithAnimatedValue>; + contentStyle?: StyleProp; style?: StyleProp; /** * Elevation level of the menu's content. Shadow styles are calculated based on this value. Default `backgroundColor` is taken from the corresponding `theme.colors.elevation` property. By default equals `2`. @@ -100,8 +99,6 @@ export type Props = { const SCREEN_INDENT = 8; // From https://material.io/design/motion/speed.html#duration const ANIMATION_DURATION = 250; -// From the 'Standard easing' section of https://material.io/design/motion/speed.html#easing -const EASING = Easing.bezier(0.4, 0, 0.2, 1); const WINDOW_LAYOUT = Dimensions.get('window'); @@ -197,6 +194,7 @@ const Menu = ({ const { colors: md3Colors } = theme as Theme; const insets = useSafeAreaInsets(); const [rendered, setRendered] = React.useState(visible); + const [expanded, setExpanded] = React.useState(false); const [left, setLeft] = React.useState(0); const [top, setTop] = React.useState(0); const [menuLayout, setMenuLayout] = React.useState({ width: 0, height: 0 }); @@ -209,9 +207,10 @@ const Menu = ({ height: WINDOW_LAYOUT.height, }); - const opacityAnimationRef = React.useRef(new Animated.Value(0)); - const scaleAnimationRef = React.useRef(new Animated.ValueXY({ x: 0, y: 0 })); const keyboardHeightRef = React.useRef(0); + const animationTimeoutRef = React.useRef< + ReturnType | undefined + >(undefined); const prevVisible = React.useRef(null); const anchorRef = React.useRef(null); const menuRef = React.useRef(null); @@ -261,6 +260,13 @@ const Menu = ({ isBrowser() && document.removeEventListener('keyup', handleKeypress); }, [handleKeypress]); + const clearAnimationTimeout = React.useCallback(() => { + if (animationTimeoutRef.current) { + clearTimeout(animationTimeoutRef.current); + animationTimeoutRef.current = undefined; + } + }, []); + const attachListeners = React.useCallback(() => { backHandlerSubscriptionRef.current = addEventListener( BackHandler, @@ -343,46 +349,42 @@ const Menu = ({ width: windowLayoutResult.width, }); + clearAnimationTimeout(); + setExpanded(false); + prevRendered.current = true; attachListeners(); requestAnimationFrame(() => { const { animation } = theme; - Animated.parallel([ - Animated.timing(scaleAnimationRef.current, { - toValue: { x: menuLayoutResult.width, y: menuLayoutResult.height }, - duration: ANIMATION_DURATION * animation.scale, - easing: EASING, - useNativeDriver: true, - }), - Animated.timing(opacityAnimationRef.current, { - toValue: 1, - duration: ANIMATION_DURATION * animation.scale, - easing: EASING, - useNativeDriver: true, - }), - ]).start(() => { + setExpanded(true); + + animationTimeoutRef.current = setTimeout(() => { focusFirstDOMNode(menuRef.current); - prevRendered.current = true; - }); + animationTimeoutRef.current = undefined; + }, ANIMATION_DURATION * animation.scale); }); - }, [anchor, attachListeners, measureAnchorLayout, theme]); + }, [ + anchor, + attachListeners, + clearAnimationTimeout, + measureAnchorLayout, + theme, + ]); const hide = React.useCallback(() => { removeListeners(); + clearAnimationTimeout(); + setExpanded(false); const { animation } = theme; - Animated.timing(opacityAnimationRef.current, { - toValue: 0, - duration: ANIMATION_DURATION * animation.scale, - easing: EASING, - useNativeDriver: true, - }).start(() => { + animationTimeoutRef.current = setTimeout(() => { setMenuLayout({ width: 0, height: 0 }); setRendered(false); prevRendered.current = false; focusFirstDOMNode(anchorRef.current); - }); - }, [removeListeners, theme]); + animationTimeoutRef.current = undefined; + }, ANIMATION_DURATION * animation.scale); + }, [clearAnimationTimeout, removeListeners, theme]); const updateVisibility = React.useCallback( async (display: boolean) => { @@ -403,8 +405,6 @@ const Menu = ({ ); React.useEffect(() => { - const opacityAnimation = opacityAnimationRef.current; - const scaleAnimation = scaleAnimationRef.current; keyboardDidShowListenerRef.current = Keyboard.addListener( 'keyboardDidShow', keyboardDidShow @@ -415,13 +415,17 @@ const Menu = ({ ); return () => { + clearAnimationTimeout(); removeListeners(); keyboardDidShowListenerRef.current?.remove(); keyboardDidHideListenerRef.current?.remove(); - scaleAnimation.removeAllListeners(); - opacityAnimation?.removeAllListeners(); }; - }, [removeListeners, keyboardDidHide, keyboardDidShow]); + }, [ + clearAnimationTimeout, + removeListeners, + keyboardDidHide, + keyboardDidShow, + ]); React.useEffect(() => { if (prevVisible.current !== visible) { @@ -462,10 +466,7 @@ const Menu = ({ // Check if menu fits horizontally and if not align it to right. if (left <= windowLayout.width - menuLayout.width - SCREEN_INDENT) { positionTransforms.push({ - translateX: scaleAnimationRef.current.x.interpolate({ - inputRange: [0, menuLayout.width], - outputRange: [-(menuLayout.width / 2), 0], - }), + translateX: expanded ? 0 : -(menuLayout.width / 2), }); // Check if menu position has enough space from left side @@ -474,10 +475,7 @@ const Menu = ({ } } else { positionTransforms.push({ - translateX: scaleAnimationRef.current.x.interpolate({ - inputRange: [0, menuLayout.width], - outputRange: [menuLayout.width / 2, 0], - }), + translateX: expanded ? 0 : menuLayout.width / 2, }); leftTransformation += anchorLayout.width - menuLayout.width; @@ -560,10 +558,9 @@ const Menu = ({ topTransformation <= windowLayout.height - topTransformation) ) { positionTransforms.push({ - translateY: scaleAnimationRef.current.y.interpolate({ - inputRange: [0, menuLayout.height], - outputRange: [-((scrollableMenuHeight || menuLayout.height) / 2), 0], - }), + translateY: expanded + ? 0 + : -((scrollableMenuHeight || menuLayout.height) / 2), }); // Check if menu position has enough space from top side @@ -572,10 +569,9 @@ const Menu = ({ } } else { positionTransforms.push({ - translateY: scaleAnimationRef.current.y.interpolate({ - inputRange: [0, menuLayout.height], - outputRange: [(scrollableMenuHeight || menuLayout.height) / 2, 0], - }), + translateY: expanded + ? 0 + : (scrollableMenuHeight || menuLayout.height) / 2, }); topTransformation += @@ -598,24 +594,33 @@ const Menu = ({ } } - const shadowMenuContainerStyle = { - opacity: opacityAnimationRef.current, + const transitionStyle: React.ComponentProps['style'] = { + transitionDuration: ANIMATION_DURATION * theme.animation.scale, + transitionProperty: 'transform', + }; + + const shadowMenuContainerStyle: React.ComponentProps< + typeof Surface + >['style'] = { + opacity: expanded ? 1 : 0, transform: [ { - scaleX: scaleAnimationRef.current.x.interpolate({ - inputRange: [0, menuLayout.width], - outputRange: [0, 1], - }), + scaleX: expanded ? 1 : 0, }, { - scaleY: scaleAnimationRef.current.y.interpolate({ - inputRange: [0, menuLayout.height], - outputRange: [0, 1], - }), + scaleY: expanded ? 1 : 0, }, ], borderRadius: theme.shapes.corner.extraSmall, ...(scrollableMenuHeight ? { height: scrollableMenuHeight } : {}), + transitionDuration: ANIMATION_DURATION * theme.animation.scale, + transitionProperty: ['opacity', 'transform'], + }; + + const positionTransformStyle: React.ComponentProps< + typeof Animated.View + >['style'] = { + transform: [...positionTransforms], }; const positionStyle = { @@ -659,19 +664,17 @@ const Menu = ({ > >; + contentContainerStyle?: StyleProp; /** * Style for the wrapper of the modal. * Use this prop to change the default wrapper style or to override safe area insets with marginTop and marginBottom. @@ -112,47 +112,32 @@ function Modal({ const onDismissCallback = useLatestCallback(onDismiss); const { scale } = theme.animation; const { top, bottom } = useSafeAreaInsets(); - const opacity = useAnimatedValue(visible ? 1 : 0); const [visibleInternal, setVisibleInternal] = React.useState(visible); + const [animatedVisible, setAnimatedVisible] = React.useState(visible); + const opacity = animatedVisible ? 1 : 0; - const showModalAnimation = React.useCallback(() => { - Animated.timing(opacity, { - toValue: 1, - duration: scale * DEFAULT_DURATION, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }).start(); - }, [opacity, scale]); - - const hideModalAnimation = React.useCallback(() => { - Animated.timing(opacity, { - toValue: 0, - duration: scale * DEFAULT_DURATION, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }).start(({ finished }) => { - if (!finished) { - return; - } + if (visible && !visibleInternal) { + setVisibleInternal(true); + } + + React.useEffect(() => { + const timeout = setTimeout(() => setAnimatedVisible(visible), 0); - setVisibleInternal(false); - }); - }, [opacity, scale]); + return () => clearTimeout(timeout); + }, [visible]); React.useEffect(() => { - if (visibleInternal === visible) { - return; + if (visible || !visibleInternal) { + return undefined; } - if (!visibleInternal && visible) { - setVisibleInternal(true); - return showModalAnimation(); - } + const timeout = setTimeout( + () => setVisibleInternal(false), + scale * DEFAULT_DURATION + ); - if (visibleInternal && !visible) { - return hideModalAnimation(); - } - }, [visible, showModalAnimation, hideModalAnimation, visibleInternal]); + return () => clearTimeout(timeout); + }, [scale, visible, visibleInternal]); React.useEffect(() => { if (!visible) { @@ -175,10 +160,20 @@ function Modal({ return () => subscription.remove(); }, [dismissable, dismissableBackButton, onDismissCallback, visible]); - if (!visibleInternal) { + if (!visible && !visibleInternal) { return null; } + const transitionStyle: React.ComponentProps['style'] = { + transitionDuration: scale * DEFAULT_DURATION, + transitionProperty: 'opacity', + }; + const backdropStyle: React.ComponentProps['style'] = + { + backgroundColor: theme.colors.scrim, + opacity: animatedVisible ? scrimAlpha : 0, + }; + return ( {children} diff --git a/src/components/Searchbar.tsx b/src/components/Searchbar.tsx index 3cc733e126..516b6b12bf 100644 --- a/src/components/Searchbar.tsx +++ b/src/components/Searchbar.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Animated, Platform, StyleSheet, TextInput, View } from 'react-native'; +import { Platform, StyleSheet, TextInput, View } from 'react-native'; import type { ColorValue, GestureResponderEvent, @@ -111,12 +111,12 @@ export type Props = TextInputProps & { * @supported Available in v5.x with theme version 3 * Changes Searchbar shadow and background on iOS and Android. */ - elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value; + elevation?: 0 | 1 | 2 | 3 | 4 | 5; /** * Set style of the TextInput component inside the searchbar */ inputStyle?: StyleProp; - style?: Animated.WithAnimatedValue>; + style?: StyleProp; /** * Custom flag for replacing clear button with activity indicator. */ diff --git a/src/components/SegmentedButtons/SegmentedButtonItem.tsx b/src/components/SegmentedButtons/SegmentedButtonItem.tsx index 4f701f0b48..8b9653c824 100644 --- a/src/components/SegmentedButtons/SegmentedButtonItem.tsx +++ b/src/components/SegmentedButtons/SegmentedButtonItem.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Animated, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import type { GestureResponderEvent, PressableAndroidRippleConfig, @@ -8,6 +8,8 @@ import type { ViewStyle, } from 'react-native'; +import Animated from 'react-native-reanimated'; + import { getSegmentedButtonBorderRadius, getSegmentedButtonColors, @@ -122,25 +124,6 @@ const SegmentedButtonItem = ({ }: Props) => { const theme = useInternalTheme(themeOverrides); - const checkScale = React.useRef(new Animated.Value(0)).current; - - React.useEffect(() => { - if (!showSelectedCheck) { - return; - } - if (checked) { - Animated.spring(checkScale, { - toValue: 1, - useNativeDriver: true, - }).start(); - } else { - Animated.spring(checkScale, { - toValue: 0, - useNativeDriver: true, - }).start(); - } - }, [checked, checkScale, showSelectedCheck]); - const { borderColor, textColor, textOpacity, borderWidth, backgroundColor } = getSegmentedButtonColors({ checked, @@ -159,15 +142,18 @@ const SegmentedButtonItem = ({ const showCheckedIcon = checked && showSelectedCheck; const iconSize = 18; + const iconTransitionStyle: React.ComponentProps< + typeof Animated.View + >['style'] = { + transitionDuration: 150 * theme.animation.scale, + transitionProperty: 'transform', + }; const iconStyle = { marginRight: label ? 5 : showCheckedIcon ? 3 : 0, ...(label && { transform: [ { - scale: checkScale.interpolate({ - inputRange: [0, 1], - outputRange: [1, 0], - }), + scale: checked && showSelectedCheck ? 0 : 1, }, ], }), @@ -212,13 +198,20 @@ const SegmentedButtonItem = ({ {showCheckedIcon ? ( ) : null} {showIcon ? ( - + ) : null} diff --git a/src/components/Snackbar.tsx b/src/components/Snackbar.tsx index 04502eb7de..e832b07310 100644 --- a/src/components/Snackbar.tsx +++ b/src/components/Snackbar.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; -import { Animated, Easing, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import type { StyleProp, ViewStyle } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import useLatestCallback from 'use-latest-callback'; import Button from './Button/Button'; import type { IconSource } from './Icon'; @@ -58,7 +57,7 @@ export type Props = $Omit, 'mode'> & { * @supported Available in v5.x with theme version 3 * Changes Snackbar shadow and background on iOS and Android. */ - elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value; + elevation?: 0 | 1 | 2 | 3 | 4 | 5; /** * Specifies the largest possible scale a text font can reach. */ @@ -71,7 +70,7 @@ export type Props = $Omit, 'mode'> & { * Style for the content of the snackbar */ contentStyle?: StyleProp; - style?: Animated.WithAnimatedValue>; + style?: StyleProp; ref?: React.RefObject; /** * @optional @@ -155,80 +154,42 @@ const Snackbar = ({ const { direction } = useLocale(); const { bottom, right, left } = useSafeAreaInsets(); - const { current: opacity } = React.useRef( - new Animated.Value(0.0) - ); - const hideTimeout = React.useRef(undefined); - const [hidden, setHidden] = React.useState(!visible); const { scale } = theme.animation; + const opacity = visible ? 1 : 0; - const animateShow = useLatestCallback(() => { - if (hideTimeout.current) clearTimeout(hideTimeout.current); - - Animated.timing(opacity, { - toValue: 1, - duration: 200 * scale, - easing: Easing.out(Easing.ease), - useNativeDriver: true, - }).start(({ finished }) => { - if (finished) { - const isInfinity = - duration === Number.POSITIVE_INFINITY || - duration === Number.NEGATIVE_INFINITY; + if (visible && hidden) { + setHidden(false); + } - if (!isInfinity) { - hideTimeout.current = setTimeout( - onDismiss, - duration - ) as unknown as NodeJS.Timeout; - } - } - }); - }); + React.useEffect(() => { + if (!visible || hidden) { + return undefined; + } - const handleOnVisible = useLatestCallback(() => { - // show - setHidden(false); - }); + const isInfinity = + duration === Number.POSITIVE_INFINITY || + duration === Number.NEGATIVE_INFINITY; - const handleOnHidden = useLatestCallback(() => { - // hide - if (hideTimeout.current) { - clearTimeout(hideTimeout.current); + if (isInfinity) { + return undefined; } - Animated.timing(opacity, { - toValue: 0, - duration: 100 * scale, - useNativeDriver: true, - }).start(({ finished }) => { - if (finished) { - setHidden(true); - } - }); - }); + const timeout = setTimeout(onDismiss, duration); + + return () => clearTimeout(timeout); + }, [duration, hidden, onDismiss, visible]); React.useEffect(() => { - if (!hidden) { - animateShow(); + if (visible || hidden) { + return undefined; } - }, [animateShow, hidden]); - React.useEffect(() => { - return () => { - if (hideTimeout.current) clearTimeout(hideTimeout.current); - }; - }, []); + const timeout = setTimeout(() => setHidden(true), 100 * scale); - React.useLayoutEffect(() => { - if (visible) { - handleOnVisible(); - } else { - handleOnHidden(); - } - }, [visible, handleOnVisible, handleOnHidden]); + return () => clearTimeout(timeout); + }, [hidden, scale, visible]); const { colors } = theme as Theme; @@ -255,6 +216,10 @@ const Snackbar = ({ paddingBottom: bottom, paddingHorizontal: Math.max(left, right), }; + const transitionStyle: React.ComponentProps['style'] = { + transitionDuration: (visible ? 200 : 100) * scale, + transitionProperty: ['opacity', 'transform'], + }; const renderChildrenWithWrapper = () => { if (typeof children === 'string') { @@ -294,15 +259,11 @@ const Snackbar = ({ opacity: opacity, transform: [ { - scale: visible - ? opacity.interpolate({ - inputRange: [0, 1], - outputRange: [0.9, 1], - }) - : 1, + scale: visible ? 1 : 0.9, }, ], }, + transitionStyle, style, ]} testID={testID} diff --git a/src/components/Surface.tsx b/src/components/Surface.tsx index 5966469df9..3c89afbf0e 100644 --- a/src/components/Surface.tsx +++ b/src/components/Surface.tsx @@ -1,32 +1,31 @@ import * as React from 'react'; -import { Animated, Platform, StyleSheet, View } from 'react-native'; +import { Platform, StyleSheet, View } from 'react-native'; import type { ColorValue, ShadowStyleIOS, - StyleProp, ViewProps, ViewStyle, } from 'react-native'; +import Animated from 'react-native-reanimated'; + import { useInternalTheme } from '../core/theming'; import { androidElevationLevels, - elevationInputRange, shadow, shadowLayers, } from '../theme/tokens/sys/elevation'; import type { Elevation, Theme, ThemeProp } from '../types'; -import { isAnimatedValue } from '../utils/animations'; import { splitStyles } from '../utils/splitStyles'; -type SurfaceElevation = Elevation | Animated.Value; +type MotionViewStyle = React.ComponentProps['style']; export type Props = Omit & { /** * Content of the `Surface`. */ children: React.ReactNode; - style?: Animated.WithAnimatedValue>; + style?: MotionViewStyle; /** * @supported Available in v5.x with theme version 3 * Changes shadows and background on iOS and Android. @@ -37,7 +36,7 @@ export type Props = Omit & { * Note: In version 2 the `elevation` prop was accepted via `style` prop i.e. `style={{ elevation: 4 }}`. * It's no longer supported with theme version 3 and you should use `elevation` property instead. */ - elevation?: SurfaceElevation; + elevation?: Elevation; /** * @supported Available in v5.x with theme version 3 * Mode of the Surface. @@ -79,32 +78,10 @@ const outerLayerStyleProperties: (keyof ViewStyle)[] = [ ]; function getStyleForShadowLayer( - elevation: SurfaceElevation, + elevation: Elevation, layer: 0 | 1, shadowColor: ColorValue -): Animated.WithAnimatedValue { - if (isAnimatedValue(elevation)) { - return { - shadowColor, - shadowOpacity: elevation.interpolate({ - inputRange: [0, 1], - outputRange: [0, shadowLayers[layer].shadowOpacity], - extrapolate: 'clamp', - }), - shadowOffset: { - width: 0, - height: elevation.interpolate({ - inputRange: elevationInputRange, - outputRange: shadowLayers[layer].height, - }), - }, - shadowRadius: elevation.interpolate({ - inputRange: elevationInputRange, - outputRange: shadowLayers[layer].shadowRadius, - }), - }; - } - +): ShadowStyleIOS { return { shadowColor, shadowOpacity: elevation ? shadowLayers[layer].shadowOpacity : 0, @@ -117,11 +94,15 @@ function getStyleForShadowLayer( } type SurfaceIOSProps = Omit & { - elevation: SurfaceElevation; - backgroundColor?: - | ColorValue - | Animated.AnimatedInterpolation; + elevation: Elevation; + backgroundColor?: ColorValue; shadowColor: ColorValue; + transitionStyle: TransitionStyle; +}; + +type TransitionStyle = { + transitionDuration: number; + transitionProperty: 'all'; }; const SurfaceIOS = ({ @@ -129,6 +110,7 @@ const SurfaceIOS = ({ style, backgroundColor, shadowColor, + transitionStyle, testID, children, mode = 'elevated', @@ -181,13 +163,16 @@ const SurfaceIOS = ({ return [outerLayerViewStyles, innerLayerViewStyles]; }, [style, elevation, backgroundColor, shadowColor, mode, container]); + const outerStyle: MotionViewStyle = [transitionStyle, outerLayerViewStyles]; + const innerStyle: MotionViewStyle = [transitionStyle, innerLayerViewStyles]; + return ( - + {children} @@ -239,36 +224,30 @@ const Surface = ({ const { colors } = theme as Theme; - const backgroundColor = (() => { - if (isAnimatedValue(elevation)) { - return elevation.interpolate({ - inputRange: elevationInputRange, - outputRange: elevationInputRange.map((elevation) => { - return colors.elevation?.[`level${elevation}`]; - }), - }); - } - - return colors.elevation?.[`level${elevation}`]; - })(); + const backgroundColor = colors.elevation?.[`level${elevation}`]; const isElevated = mode === 'elevated'; + const transitionStyle = { + transitionDuration: 150 * theme.animation.scale, + transitionProperty: 'all' as const, + }; if (Platform.OS === 'web') { const { pointerEvents = 'auto' } = props; + const reanimatedViewStyle: MotionViewStyle = [ + transitionStyle, + { backgroundColor }, + elevation && isElevated ? shadow(elevation, theme.colors.shadow) : null, + style, + ]; + return ( {children} @@ -276,16 +255,7 @@ const Surface = ({ } if (Platform.OS === 'android') { - const getElevationAndroid = () => { - if (isAnimatedValue(elevation)) { - return elevation.interpolate({ - inputRange: elevationInputRange, - outputRange: androidElevationLevels, - }); - } - - return androidElevationLevels[elevation]; - }; + const elevationAndroid = androidElevationLevels[elevation]; const { margin, padding, transform, borderRadius } = (StyleSheet.flatten( style @@ -294,22 +264,25 @@ const Surface = ({ const outerLayerStyles = { margin, padding, transform, borderRadius }; const sharedStyle = [{ backgroundColor }, style]; + const reanimatedViewStyle: MotionViewStyle = [ + transitionStyle, + { + backgroundColor, + transform, + }, + outerLayerStyles, + sharedStyle, + isElevated && { + elevation: elevationAndroid, + }, + ]; + return ( {children} @@ -323,6 +296,7 @@ const Surface = ({ elevation={elevation} backgroundColor={backgroundColor} shadowColor={theme.colors.shadow} + transitionStyle={transitionStyle} style={style} testID={testID} mode={mode} diff --git a/src/components/ToggleButton/ToggleButton.tsx b/src/components/ToggleButton/ToggleButton.tsx index 22598c16b2..df213ddcf8 100644 --- a/src/components/ToggleButton/ToggleButton.tsx +++ b/src/components/ToggleButton/ToggleButton.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { StyleSheet, View, Animated } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import type { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native'; import { ToggleButtonGroupContext } from './ToggleButtonGroup'; @@ -42,7 +42,7 @@ export type Props = { * Status of button. */ status?: 'checked' | 'unchecked'; - style?: Animated.WithAnimatedValue>; + style?: StyleProp; /** * @optional */ diff --git a/src/components/TouchableRipple/Pressable.tsx b/src/components/TouchableRipple/Pressable.tsx index 446cb234a7..3fbda644c0 100644 --- a/src/components/TouchableRipple/Pressable.tsx +++ b/src/components/TouchableRipple/Pressable.tsx @@ -1,6 +1,5 @@ import type * as React from 'react'; import type { - Animated, PressableProps as PressableNativeProps, StyleProp, View, @@ -27,12 +26,7 @@ export type PressableProps = Omit< | undefined; style?: | StyleProp - | Animated.WithAnimatedValue> - | (( - state: PressableStateCallbackType - ) => - | StyleProp - | Animated.WithAnimatedValue>) + | ((state: PressableStateCallbackType) => StyleProp) | undefined; }; diff --git a/src/components/Typography/AnimatedText.tsx b/src/components/Typography/AnimatedText.tsx index ea47c43b58..e3c95b782f 100644 --- a/src/components/Typography/AnimatedText.tsx +++ b/src/components/Typography/AnimatedText.tsx @@ -1,14 +1,17 @@ import * as React from 'react'; import type { ReactNode } from 'react'; -import { Animated, StyleSheet, Text } from 'react-native'; -import type { StyleProp, TextProps, TextStyle } from 'react-native'; +import { StyleSheet } from 'react-native'; + +import Animated from 'react-native-reanimated'; import type { VariantProp } from './types'; import { useLocale } from '../../core/locale'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; -type Props = Animated.AnimatedProps & { +type AnimatedTextProps = React.ComponentProps; + +type Props = AnimatedTextProps & { /** * Variant defines appropriate text styles for type role and its size. * Available variants: @@ -24,8 +27,8 @@ type Props = Animated.AnimatedProps & { * Body: `bodyLarge`, `bodyMedium`, `bodySmall` */ variant?: VariantProp; - style?: StyleProp; - ref?: React.Ref; + style?: AnimatedTextProps['style']; + ref?: AnimatedTextProps['ref']; /** * @optional */ diff --git a/src/components/__tests__/Appbar/Appbar.test.tsx b/src/components/__tests__/Appbar/Appbar.test.tsx index 8738f71e37..c4c18f87b0 100644 --- a/src/components/__tests__/Appbar/Appbar.test.tsx +++ b/src/components/__tests__/Appbar/Appbar.test.tsx @@ -1,7 +1,4 @@ -import { Animated } from 'react-native'; - -import { describe, expect, it, jest } from '@jest/globals'; -import { act } from '@testing-library/react-native'; +import { describe, expect, it } from '@jest/globals'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { getTheme } from '../../../core/theming'; @@ -301,40 +298,24 @@ describe('getAppbarColors', () => { }); }); -describe('animated value changes correctly', () => { - it('appbar animated value changes correctly', async () => { - const value = new Animated.Value(1); +describe('style props', () => { + it('appbar style is applied correctly', async () => { await render( - + ); expect(screen.getByTestId('appbar-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); - - expect(screen.getByTestId('appbar-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], + marginTop: 12, }); }); - it('action animated value changes correctly', async () => { - const value = new Animated.Value(1); + it('action style is applied correctly', async () => { await render( @@ -342,32 +323,15 @@ describe('animated value changes correctly', () => { expect( screen.getByTestId('appbar-action-container-outer-layer') ).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); - - expect( - screen.getByTestId('appbar-action-container-outer-layer') - ).toHaveStyle({ - transform: [{ scale: 1.5 }], + marginTop: 12, }); }); - it('back action animated value changes correctly', async () => { - const value = new Animated.Value(1); + it('back action style is applied correctly', async () => { await render( @@ -375,54 +339,20 @@ describe('animated value changes correctly', () => { expect( screen.getByTestId('appbar-back-action-container-outer-layer') ).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); - - expect( - screen.getByTestId('appbar-back-action-container-outer-layer') - ).toHaveStyle({ - transform: [{ scale: 1.5 }], + marginTop: 12, }); }); - it('header animated value changes correctly', async () => { - const value = new Animated.Value(1); + it('header style is applied correctly', async () => { await render( - + {null} ); expect(screen.getByTestId('appbar-header-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); - - expect(screen.getByTestId('appbar-header-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], + marginTop: 12, }); }); diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap index a5d9d95766..b6dc06dc12 100644 --- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap +++ b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap @@ -2,35 +2,15 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` - - + - + - + - + + @@ -270,36 +300,17 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` testID="search-bar-icon-wrapper" > - + @@ -418,35 +458,15 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and AppbarAction 1`] = ` - - + - + + @@ -580,21 +640,24 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A } > @@ -703,36 +766,17 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A - + @@ -823,21 +896,24 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A } > diff --git a/src/components/__tests__/Banner.test.tsx b/src/components/__tests__/Banner.test.tsx index 80bd3e9017..5cbd07b387 100644 --- a/src/components/__tests__/Banner.test.tsx +++ b/src/components/__tests__/Banner.test.tsx @@ -1,4 +1,4 @@ -import { Animated, Image } from 'react-native'; +import { Image } from 'react-native'; import { afterAll, @@ -11,7 +11,7 @@ import { } from '@jest/globals'; import { act } from '@testing-library/react-native'; -import { render, screen } from '../../test-utils'; +import { render } from '../../test-utils'; import Banner from '../Banner'; it('renders hidden banner, without action buttons and without image', async () => { @@ -356,34 +356,4 @@ describe('animations', () => { expect(nextHideCallback).toHaveBeenCalledTimes(1); }); }); - - it('animated value changes correctly', async () => { - const value = new Animated.Value(1); - await render( - - Banner - - ); - expect(screen.getByTestId('banner-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.runAllTimers(); - }); - - expect(screen.getByTestId('banner-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], - }); - }); }); diff --git a/src/components/__tests__/BottomNavigation.test.tsx b/src/components/__tests__/BottomNavigation.test.tsx index 21e495742a..66134d1ebd 100644 --- a/src/components/__tests__/BottomNavigation.test.tsx +++ b/src/components/__tests__/BottomNavigation.test.tsx @@ -601,7 +601,9 @@ it('barStyle animated value changes correctly', async () => { barStyle={[{ transform: [{ scale: value }] }]} /> ); - expect(screen.getByTestId('bottom-navigation-bar-outer-layer')).toHaveStyle({ + expect( + screen.getByTestId('bottom-navigation-bar-animated-wrapper') + ).toHaveStyle({ transform: [{ scale: 1 }], }); @@ -614,7 +616,9 @@ it('barStyle animated value changes correctly', async () => { await act(() => { jest.advanceTimersByTime(200); }); - expect(screen.getByTestId('bottom-navigation-bar-outer-layer')).toHaveStyle({ + expect( + screen.getByTestId('bottom-navigation-bar-animated-wrapper') + ).toHaveStyle({ transform: [{ scale: 1.5 }], }); }); diff --git a/src/components/__tests__/Button.test.tsx b/src/components/__tests__/Button.test.tsx index 4da466837a..7612184e48 100644 --- a/src/components/__tests__/Button.test.tsx +++ b/src/components/__tests__/Button.test.tsx @@ -1,7 +1,7 @@ -import { Animated, StyleSheet } from 'react-native'; +import { StyleSheet } from 'react-native'; import { describe, expect, it, jest } from '@jest/globals'; -import { act, userEvent } from '@testing-library/react-native'; +import { userEvent } from '@testing-library/react-native'; import { getTheme } from '../../core/theming'; import { render, screen } from '../../test-utils'; @@ -709,33 +709,3 @@ describe('getButtonColors - border width', () => { }) ); }); - -it('animated value changes correctly', async () => { - const value = new Animated.Value(1); - await render( - - ); - expect(screen.getByTestId('button-container-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); - expect(screen.getByTestId('button-container-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], - }); -}); diff --git a/src/components/__tests__/Card/Card.test.tsx b/src/components/__tests__/Card/Card.test.tsx index 61659c8a24..1c7de8b351 100644 --- a/src/components/__tests__/Card/Card.test.tsx +++ b/src/components/__tests__/Card/Card.test.tsx @@ -1,7 +1,6 @@ -import { Animated, StyleSheet, Text } from 'react-native'; +import { StyleSheet, Text } from 'react-native'; -import { describe, expect, it, jest } from '@jest/globals'; -import { act } from '@testing-library/react-native'; +import { describe, expect, it } from '@jest/globals'; import { getTheme } from '../../../core/theming'; import { render, screen } from '../../../test-utils'; @@ -223,32 +222,3 @@ describe('getCardCoverStyle - border radius', () => { ).toMatchObject({ borderRadius: getTheme().shapes.corner.medium }); }); }); - -it('animated value changes correctly', async () => { - const value = new Animated.Value(1); - await render( - - {null} - - ); - expect(screen.getByTestId('card-container-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); - expect(screen.getByTestId('card-container-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], - }); -}); diff --git a/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap b/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap index d1492cc475..5346cfadc1 100644 --- a/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap +++ b/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap @@ -2,29 +2,15 @@ exports[`Card renders an outlined card 1`] = ` - + diff --git a/src/components/__tests__/Chip.test.tsx b/src/components/__tests__/Chip.test.tsx index 644906ae33..0e8e959ec3 100644 --- a/src/components/__tests__/Chip.test.tsx +++ b/src/components/__tests__/Chip.test.tsx @@ -1,7 +1,4 @@ -import { Animated } from 'react-native'; - -import { describe, expect, it, jest } from '@jest/globals'; -import { act } from '@testing-library/react-native'; +import { describe, expect, it } from '@jest/globals'; import color from 'color'; import { getTheme } from '../../core/theming'; @@ -373,32 +370,3 @@ describe('getChipColor - border color', () => { }); }); }); - -it('animated value changes correctly', async () => { - const value = new Animated.Value(1); - await render( - {}} - testID="chip" - style={[{ transform: [{ scale: value }] }]} - > - Example Chip - - ); - expect(screen.getByTestId('chip-container-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); - expect(screen.getByTestId('chip-container-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], - }); -}); diff --git a/src/components/__tests__/IconButton.test.tsx b/src/components/__tests__/IconButton.test.tsx index b28456c5ce..a8bc540aa9 100644 --- a/src/components/__tests__/IconButton.test.tsx +++ b/src/components/__tests__/IconButton.test.tsx @@ -1,7 +1,6 @@ -import { Animated, StyleSheet } from 'react-native'; +import { StyleSheet } from 'react-native'; -import { describe, expect, it, jest } from '@jest/globals'; -import { act } from '@testing-library/react-native'; +import { describe, expect, it } from '@jest/globals'; import { getTheme } from '../../core/theming'; import { render, screen } from '../../test-utils'; @@ -318,30 +317,3 @@ describe('getIconButtonColor - border color', () => { }); }); }); - -it('action animated value changes correctly', async () => { - const value = new Animated.Value(1); - await render( - - ); - expect(screen.getByTestId('icon-button-container-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); - expect(screen.getByTestId('icon-button-container-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], - }); -}); diff --git a/src/components/__tests__/Menu.test.tsx b/src/components/__tests__/Menu.test.tsx index 128fb87cdb..9151e99f72 100644 --- a/src/components/__tests__/Menu.test.tsx +++ b/src/components/__tests__/Menu.test.tsx @@ -1,4 +1,4 @@ -import { Animated, Dimensions, StyleSheet, View } from 'react-native'; +import { Dimensions, StyleSheet, View } from 'react-native'; import { expect, it, jest } from '@jest/globals'; import { act, screen, waitFor } from '@testing-library/react-native'; @@ -208,8 +208,7 @@ it('respects anchorPosition bottom', async () => { dimensionsSpy.mockRestore(); }); -it('animated value changes correctly', async () => { - const value = new Animated.Value(1); +it('applies content styles', async () => { await render( { onDismiss={jest.fn()} anchor={} testID="menu" - contentStyle={[{ transform: [{ scale: value }] }]} + contentStyle={{ marginTop: 12 }} > ); expect(screen.getByTestId('menu-surface-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); - expect(screen.getByTestId('menu-surface-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], + marginTop: 12, }); }); diff --git a/src/components/__tests__/Modal.test.tsx b/src/components/__tests__/Modal.test.tsx index 2009f3d444..45fbeaaa55 100644 --- a/src/components/__tests__/Modal.test.tsx +++ b/src/components/__tests__/Modal.test.tsx @@ -1,4 +1,4 @@ -import { Animated, BackHandler as RNBackHandler, Text } from 'react-native'; +import { BackHandler as RNBackHandler, Text } from 'react-native'; import type { BackHandlerStatic as RNBackHandlerStatic } from 'react-native'; import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; @@ -575,33 +575,18 @@ describe('Modal', () => { }); }); - it('animated value changes correctly', async () => { - const value = new Animated.Value(1); + it('applies content container style', async () => { await render( {null} ); expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.runAllTimers(); - }); - - expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], + marginTop: 12, }); }); }); diff --git a/src/components/__tests__/Searchbar.test.tsx b/src/components/__tests__/Searchbar.test.tsx index 0671263277..5fea2800ed 100644 --- a/src/components/__tests__/Searchbar.test.tsx +++ b/src/components/__tests__/Searchbar.test.tsx @@ -1,7 +1,5 @@ -import { Animated } from 'react-native'; - import { expect, it, jest } from '@jest/globals'; -import { act, userEvent } from '@testing-library/react-native'; +import { userEvent } from '@testing-library/react-native'; import { render, screen } from '../../test-utils'; import * as Avatar from '../Avatar/Avatar'; @@ -72,33 +70,6 @@ it('renders clear icon wrapper, which is never target of touch events, if search ).toBe('none'); }); -it('animated value changes correctly', async () => { - const value = new Animated.Value(1); - await render( - - ); - expect(screen.getByTestId('search-bar-container-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); - expect(screen.getByTestId('search-bar-container-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], - }); -}); - it('defines onClearIconPress action and checks if it is called when close button is pressed', async () => { const onClearIconPressMock = jest.fn(); await render( diff --git a/src/components/__tests__/Snackbar.test.tsx b/src/components/__tests__/Snackbar.test.tsx index 16b118da09..5c30f7cc6e 100644 --- a/src/components/__tests__/Snackbar.test.tsx +++ b/src/components/__tests__/Snackbar.test.tsx @@ -1,9 +1,8 @@ -import { Animated, StyleSheet, Text, View } from 'react-native'; +import { StyleSheet, Text, View } from 'react-native'; import { expect, it, jest } from '@jest/globals'; -import { act } from '@testing-library/react-native'; -import { render, screen } from '../../test-utils'; +import { render } from '../../test-utils'; import { red200, white } from '../../theme/colors'; import Snackbar from '../Snackbar'; @@ -92,33 +91,3 @@ it('renders snackbar with View & Text as a child', async () => { expect(tree).toMatchSnapshot(); }); - -it('animated value changes correctly', async () => { - const value = new Animated.Value(1); - await render( - - Snackbar content - - ); - expect(screen.getByTestId('snack-bar-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); - expect(screen.getByTestId('snack-bar-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], - }); -}); diff --git a/src/components/__tests__/ToggleButton.test.tsx b/src/components/__tests__/ToggleButton.test.tsx index 1ea9e20bae..bd798e8180 100644 --- a/src/components/__tests__/ToggleButton.test.tsx +++ b/src/components/__tests__/ToggleButton.test.tsx @@ -1,10 +1,7 @@ -import { Animated } from 'react-native'; - -import { describe, expect, it, jest } from '@jest/globals'; -import { act } from '@testing-library/react-native'; +import { describe, expect, it } from '@jest/globals'; import { getTheme } from '../../core/theming'; -import { render, screen } from '../../test-utils'; +import { render } from '../../test-utils'; import ToggleButton from '../ToggleButton'; import { getToggleButtonColor } from '../ToggleButton/utils'; @@ -55,36 +52,3 @@ describe('getToggleButtonColor', () => { ); }); }); - -it('animated value changes correctly', async () => { - const value = new Animated.Value(1); - await render( - - ); - expect(screen.getByTestId('toggle-button-container-outer-layer')).toHaveStyle( - { - transform: [{ scale: 1 }], - } - ); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); - expect(screen.getByTestId('toggle-button-container-outer-layer')).toHaveStyle( - { - transform: [{ scale: 1.5 }], - } - ); -}); diff --git a/src/components/__tests__/__snapshots__/Banner.test.tsx.snap b/src/components/__tests__/__snapshots__/Banner.test.tsx.snap index a6fb8c8dee..bc5be59900 100644 --- a/src/components/__tests__/__snapshots__/Banner.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Banner.test.tsx.snap @@ -2,36 +2,48 @@ exports[`render visible banner, with custom theme 1`] = ` - + @@ -49,17 +61,26 @@ exports[`render visible banner, with custom theme 1`] = ` } > - + @@ -278,36 +309,48 @@ exports[`render visible banner, with custom theme 1`] = ` exports[`renders hidden banner, without action buttons and without image 1`] = ` - + @@ -325,28 +368,44 @@ exports[`renders hidden banner, without action buttons and without image 1`] = ` } >