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;