diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx index d2b3f2c3a4ac..16242fce7fca 100644 --- a/src/components/Switch.tsx +++ b/src/components/Switch.tsx @@ -1,8 +1,8 @@ -import React, {useEffect, useRef} from 'react'; -import {Animated, InteractionManager} from 'react-native'; +import React from 'react'; +import {InteractionManager} from 'react-native'; +import Animated, {interpolateColor, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useNativeDriver from '@libs/useNativeDriver'; import CONST from '@src/CONST'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; @@ -35,7 +35,7 @@ const OFFSET_X = { function Switch({isOn, onToggle, accessibilityLabel, disabled, showLockIcon, disabledAction}: SwitchProps) { const styles = useThemeStyles(); - const offsetX = useRef(new Animated.Value(isOn ? OFFSET_X.ON : OFFSET_X.OFF)); + const offsetX = useSharedValue(isOn ? OFFSET_X.ON : OFFSET_X.OFF); const theme = useTheme(); const handleSwitchPress = () => { @@ -44,22 +44,22 @@ function Switch({isOn, onToggle, accessibilityLabel, disabled, showLockIcon, dis disabledAction?.(); return; } + offsetX.set(withTiming(isOn ? OFFSET_X.OFF : OFFSET_X.ON, {duration: 300})); onToggle(!isOn); }); }; - useEffect(() => { - Animated.timing(offsetX.current, { - toValue: isOn ? OFFSET_X.ON : OFFSET_X.OFF, - duration: 300, - useNativeDriver, - }).start(); - }, [isOn]); + const animatedThumbStyle = useAnimatedStyle(() => ({ + transform: [{translateX: offsetX.get()}], + })); + + const animatedSwitchTrackStyle = useAnimatedStyle(() => ({ + backgroundColor: interpolateColor(offsetX.get(), [OFFSET_X.OFF, OFFSET_X.ON], [theme.icon, theme.success]), + })); return ( - {/* eslint-disable-next-line react-compiler/react-compiler */} - - {(!!disabled || !!showLockIcon) && ( - - )} + + + {(!!disabled || !!showLockIcon) && ( + + )} + ); diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index f6611d2ca452..b48cb55ccea1 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -1,8 +1,9 @@ import {Portal} from '@gorhom/portal'; import React, {useMemo, useRef, useState} from 'react'; -import {Animated, InteractionManager, View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {View as RNView} from 'react-native'; +import Animated, {useAnimatedStyle} from 'react-native-reanimated'; import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -46,13 +47,11 @@ function BaseGenericTooltip({ const rootWrapper = useRef(null); const StyleUtils = useStyleUtils(); - - const {animationStyle, rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo( + const {rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo( () => StyleUtils.getTooltipStyles({ // eslint-disable-next-line react-compiler/react-compiler tooltip: rootWrapper.current, - currentSize: animation, windowWidth, xOffset, yOffset, @@ -70,7 +69,6 @@ function BaseGenericTooltip({ }), [ StyleUtils, - animation, windowWidth, xOffset, yOffset, @@ -87,6 +85,10 @@ function BaseGenericTooltip({ ], ); + const animationStyle = useAnimatedStyle(() => { + return StyleUtils.getTooltipAnimatedStyles({tooltipContentWidth: contentMeasuredWidth, tooltipWrapperHeight: wrapperMeasuredHeight, currentSize: animation}); + }); + let content; if (renderTooltipContent) { content = {renderTooltipContent()}; diff --git a/src/components/Tooltip/BaseGenericTooltip/index.tsx b/src/components/Tooltip/BaseGenericTooltip/index.tsx index 28f2458699b7..72f7085b8177 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.tsx @@ -1,7 +1,8 @@ /* eslint-disable react-compiler/react-compiler */ import React, {useLayoutEffect, useMemo, useRef, useState} from 'react'; import ReactDOM from 'react-dom'; -import {Animated, View} from 'react-native'; +import {View} from 'react-native'; +import Animated, {useAnimatedStyle} from 'react-native-reanimated'; import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -15,6 +16,7 @@ import type {BaseGenericTooltipProps} from './types'; // We also update the state on layout changes which will be triggered often. // There will be n number of tooltip components in the page. // It's good to memoize this one. + function BaseGenericTooltip({ animation, windowWidth, @@ -64,11 +66,10 @@ function BaseGenericTooltip({ } }, []); - const {animationStyle, rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo( + const {rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo( () => StyleUtils.getTooltipStyles({ tooltip: rootWrapper.current, - currentSize: animation, windowWidth, xOffset, yOffset, @@ -85,7 +86,6 @@ function BaseGenericTooltip({ }), [ StyleUtils, - animation, windowWidth, xOffset, yOffset, @@ -102,6 +102,10 @@ function BaseGenericTooltip({ ], ); + const animationStyle = useAnimatedStyle(() => { + return StyleUtils.getTooltipAnimatedStyles({tooltipContentWidth: contentMeasuredWidth, tooltipWrapperHeight: wrapperMeasuredHeight, currentSize: animation}); + }); + let content; if (renderTooltipContent) { content = {renderTooltipContent()}; diff --git a/src/components/Tooltip/BaseGenericTooltip/types.ts b/src/components/Tooltip/BaseGenericTooltip/types.ts index 740b825ab725..6b2159d7be5b 100644 --- a/src/components/Tooltip/BaseGenericTooltip/types.ts +++ b/src/components/Tooltip/BaseGenericTooltip/types.ts @@ -1,4 +1,4 @@ -import type {Animated} from 'react-native'; +import type {SharedValue} from 'react-native-reanimated'; import type {SharedTooltipProps} from '@components/Tooltip/types'; type BaseGenericTooltipProps = { @@ -6,7 +6,7 @@ type BaseGenericTooltipProps = { windowWidth: number; /** Tooltip Animation value */ - animation: Animated.Value; + animation: SharedValue; /** The distance between the left side of the wrapper view and the left side of the window */ xOffset: number; diff --git a/src/components/Tooltip/GenericTooltip.tsx b/src/components/Tooltip/GenericTooltip.tsx index 7309359b8e0c..040d0d1002d9 100644 --- a/src/components/Tooltip/GenericTooltip.tsx +++ b/src/components/Tooltip/GenericTooltip.tsx @@ -1,12 +1,11 @@ -import React, {memo, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {memo, useCallback, useEffect, useState} from 'react'; import type {LayoutRectangle} from 'react-native'; -import {Animated} from 'react-native'; +import {cancelAnimation, useSharedValue, withDelay, withTiming} from 'react-native-reanimated'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Log from '@libs/Log'; import StringUtils from '@libs/StringUtils'; -import TooltipRefManager from '@libs/TooltipRefManager'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import callOrReturn from '@src/types/utils/callOrReturn'; @@ -60,9 +59,9 @@ function GenericTooltip({ const [shouldUseOverlay, setShouldUseOverlay] = useState(shouldUseOverlayProp); // Whether the tooltip is first tooltip to activate the TooltipSense - const isTooltipSenseInitiator = useRef(false); - const animation = useRef(new Animated.Value(0)); - const isAnimationCanceled = useRef(false); + const animation = useSharedValue(0); + const isTooltipSenseInitiator = useSharedValue(true); + const isAnimationCanceled = useSharedValue(false); const prevText = usePrevious(text); useEffect(() => { @@ -79,34 +78,40 @@ function GenericTooltip({ setIsRendered(true); setIsVisible(true); - animation.current.stopAnimation(); + cancelAnimation(animation); // When TooltipSense is active, immediately show the tooltip if (TooltipSense.isActive() && !shouldForceAnimate) { - animation.current.setValue(1); + animation.set(1); } else { - isTooltipSenseInitiator.current = true; - Animated.timing(animation.current, { - toValue: 1, - duration: 140, - delay: 500, - useNativeDriver: false, - }).start(({finished}) => { - isAnimationCanceled.current = !finished; - }); + isTooltipSenseInitiator.set(true); + animation.set( + withDelay( + 500, + withTiming( + 1, + { + duration: 140, + }, + (finished) => { + isAnimationCanceled.set(!finished); + }, + ), + ), + ); } TooltipSense.activate(); - }, [shouldForceAnimate]); + }, [animation, isAnimationCanceled, isTooltipSenseInitiator, shouldForceAnimate]); // eslint-disable-next-line rulesdir/prefer-early-return useEffect(() => { // if the tooltip text changed before the initial animation was finished, then the tooltip won't be shown // we need to show the tooltip again - if (isVisible && isAnimationCanceled.current && text && prevText !== text) { - isAnimationCanceled.current = false; + if (isVisible && isAnimationCanceled.get() && text && prevText !== text) { + isAnimationCanceled.set(false); showTooltip(); } - }, [isVisible, text, prevText, showTooltip]); + }, [isVisible, text, prevText, showTooltip, isAnimationCanceled]); /** * Update the tooltip's target bounding rectangle @@ -125,24 +130,19 @@ function GenericTooltip({ * Hide the tooltip in an animation. */ const hideTooltip = useCallback(() => { - animation.current.stopAnimation(); + cancelAnimation(animation); - if (TooltipSense.isActive() && !isTooltipSenseInitiator.current) { - animation.current.setValue(0); + if (TooltipSense.isActive() && !isTooltipSenseInitiator.get()) { + // eslint-disable-next-line react-compiler/react-compiler + animation.set(0); } else { // Hide the first tooltip which initiated the TooltipSense with animation - isTooltipSenseInitiator.current = false; - Animated.timing(animation.current, { - toValue: 0, - duration: 140, - useNativeDriver: false, - }).start(); + isTooltipSenseInitiator.set(false); + animation.set(0); } - TooltipSense.deactivate(); - setIsVisible(false); - }, []); + }, [animation, isTooltipSenseInitiator]); const onPressOverlay = useCallback(() => { if (!shouldUseOverlay) { @@ -153,8 +153,6 @@ function GenericTooltip({ onHideTooltip(); }, [shouldUseOverlay, onHideTooltip, hideTooltip]); - useImperativeHandle(TooltipRefManager.ref, () => ({hideTooltip}), [hideTooltip]); - // Skip the tooltip and return the children if the text is empty, we don't have a render function. if (StringUtils.isEmptyString(text) && renderTooltipContent == null) { // eslint-disable-next-line react-compiler/react-compiler @@ -166,7 +164,7 @@ function GenericTooltip({ {isRendered && ( justifyContent: 'center', borderRadius: 20, padding: 15, - backgroundColor: theme.success, }, switchInactive: { @@ -3146,11 +3145,6 @@ const styles = (theme: ThemeColors) => backgroundColor: theme.appBG, }, - switchThumbTransformation: (translateX: AnimatableNumericValue) => - ({ - transform: [{translateX}], - } satisfies ViewStyle), - radioButtonContainer: { backgroundColor: theme.componentBG, borderRadius: 14, diff --git a/src/styles/utils/generators/TooltipStyleUtils/index.ts b/src/styles/utils/generators/TooltipStyleUtils/index.ts index 07dba25844ea..950ba241570b 100644 --- a/src/styles/utils/generators/TooltipStyleUtils/index.ts +++ b/src/styles/utils/generators/TooltipStyleUtils/index.ts @@ -1,5 +1,6 @@ import type {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; -import {Animated, StyleSheet} from 'react-native'; +import {StyleSheet} from 'react-native'; +import type {SharedValue} from 'react-native-reanimated'; import FontUtils from '@styles/utils/FontUtils'; // eslint-disable-next-line no-restricted-imports import type StyleUtilGenerator from '@styles/utils/generators/types'; @@ -21,7 +22,6 @@ const POINTER_HEIGHT = 4; const POINTER_WIDTH = 12; type TooltipStyles = { - animationStyle: ViewStyle; rootWrapperStyle: ViewStyle; textStyle: TextStyle; pointerWrapperStyle: ViewStyle; @@ -30,7 +30,6 @@ type TooltipStyles = { type TooltipParams = { tooltip: View | HTMLDivElement | null; - currentSize: Animated.Value; windowWidth: number; xOffset: number; yOffset: number; @@ -47,7 +46,13 @@ type TooltipParams = { shouldAddHorizontalPadding?: boolean; }; -type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => TooltipStyles}; +type TooltipAnimationProps = { + tooltipContentWidth?: number; + tooltipWrapperHeight?: number; + currentSize: SharedValue; +}; + +type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => TooltipStyles; getTooltipAnimatedStyles: (props: TooltipAnimationProps) => {transform: [{scale: number}]}}; /** * Generate styles for the tooltip component. @@ -76,7 +81,6 @@ type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => To const createTooltipStyleUtils: StyleUtilGenerator = ({theme, styles}) => ({ getTooltipStyles: ({ tooltip, - currentSize, windowWidth, xOffset, yOffset, @@ -107,8 +111,6 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( const isTooltipSizeReady = tooltipWidth !== undefined && tooltipHeight !== undefined; - // Set the scale to 1 to be able to measure the tooltip size correctly when it's not ready yet. - let scale = new Animated.Value(1); let shouldShowBelow = false; let horizontalShift = 0; let horizontalShiftPointer = 0; @@ -130,9 +132,6 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( !!(tooltip && isOverlappingAtTop(tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight)) || anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP; - // When the tooltip size is ready, we can start animating the scale. - scale = currentSize; - // Determine if we need to shift the tooltip horizontally to prevent it // from displaying too near to the edge of the screen. horizontalShift = computeHorizontalShift(windowWidth, xOffset, tooltipTargetWidth, tooltipWidth, manualShiftHorizontal); @@ -216,12 +215,6 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( } return { - animationStyle: { - // remember Transform causes a new Local cordinate system - // https://drafts.csswg.org/css-transforms-1/#transform-rendering - // so Position fixed children will be relative to this new Local cordinate system - transform: [{scale}], - }, rootWrapperStyle: { ...tooltipPlatformStyle, backgroundColor: theme.heading, @@ -269,6 +262,20 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( }, }; }, + + /** Utility function to create and manage scale animations with React Native Reanimated */ + getTooltipAnimatedStyles: (props: TooltipAnimationProps) => { + const tooltipHorizontalPadding = spacing.ph2.paddingHorizontal * 2; + const tooltipWidth = props.tooltipContentWidth && props.tooltipContentWidth + tooltipHorizontalPadding + 1; + const isTooltipSizeReady = tooltipWidth !== undefined && props.tooltipWrapperHeight !== undefined; + let scale = 1; + if (isTooltipSizeReady) { + scale = props.currentSize.get(); + } + return { + transform: [{scale}], + }; + }, }); export default createTooltipStyleUtils;