diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 318d62f0a944..77c390c46416 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -802,35 +802,10 @@ PODS: - React-Core - RNReactNativeHapticFeedback (1.14.0): - React-Core - - RNReanimated (3.5.4): - - DoubleConversion - - FBLazyVector - - glog - - hermes-engine - - RCT-Folly - - RCTRequired - - RCTTypeSafety - - React-callinvoker + - RNReanimated (3.6.1): + - RCT-Folly (= 2021.07.22.00) - React-Core - - React-Core/DevSupport - - React-Core/RCTWebSocket - - React-CoreModules - - React-cxxreact - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-RCTActionSheet - - React-RCTAnimation - - React-RCTAppDelegate - - React-RCTBlob - - React-RCTImage - - React-RCTLinking - - React-RCTNetwork - - React-RCTSettings - - React-RCTText - ReactCommon/turbomodule/core - - Yoga - RNScreens (3.21.0): - React-Core - React-RCTImage @@ -1333,7 +1308,7 @@ SPEC CHECKSUMS: rnmapbox-maps: 6f638ec002aa6e906a6f766d69cd45f968d98e64 RNPermissions: 9b086c8f05b2e2faa587fdc31f4c5ab4509728aa RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c - RNReanimated: ab2e96c6d5591c3dfbb38a464f54c8d17fb34a87 + RNReanimated: fdbaa9c964bbab7fac50c90862b6cc5f041679b9 RNScreens: d037903436160a4b039d32606668350d2a808806 RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 diff --git a/package-lock.json b/package-lock.json index 121742f82d79..dd97783aae47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,7 +103,7 @@ "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", - "react-native-reanimated": "3.5.4", + "react-native-reanimated": "^3.6.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.4.1", "react-native-screens": "3.21.0", @@ -47677,9 +47677,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.5.4.tgz", - "integrity": "sha512-8we9LLDO1o4Oj9/DICeEJ2K1tjfqkJagqQUglxeUAkol/HcEJ6PGxIrpBcNryLqCDYEcu6FZWld/FzizBIw6bg==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.6.1.tgz", + "integrity": "sha512-F4vG9Yf9PKmE3GaWtVGUpzj3SM6YY2cx1yRHCwiMd1uY7W0gU017LfcVUorboJnj0y5QZqEriEK1Usq2Y8YZqg==", "dependencies": { "@babel/plugin-transform-object-assign": "^7.16.7", "@babel/preset-typescript": "^7.16.7", @@ -90489,9 +90489,9 @@ "requires": {} }, "react-native-reanimated": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.5.4.tgz", - "integrity": "sha512-8we9LLDO1o4Oj9/DICeEJ2K1tjfqkJagqQUglxeUAkol/HcEJ6PGxIrpBcNryLqCDYEcu6FZWld/FzizBIw6bg==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.6.1.tgz", + "integrity": "sha512-F4vG9Yf9PKmE3GaWtVGUpzj3SM6YY2cx1yRHCwiMd1uY7W0gU017LfcVUorboJnj0y5QZqEriEK1Usq2Y8YZqg==", "requires": { "@babel/plugin-transform-object-assign": "^7.16.7", "@babel/preset-typescript": "^7.16.7", diff --git a/package.json b/package.json index 8fef83717506..8ce26e47d6f7 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", - "react-native-reanimated": "3.5.4", + "react-native-reanimated": "^3.6.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.4.1", "react-native-screens": "3.21.0", diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js index 791eb150f8c9..59e741001063 100644 --- a/src/components/FloatingActionButton.js +++ b/src/components/FloatingActionButton.js @@ -1,132 +1,130 @@ import PropTypes from 'prop-types'; -import React, {PureComponent} from 'react'; -import {Animated, Easing, View} from 'react-native'; -import compose from '@libs/compose'; -import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; +import React, {useEffect, useRef} from 'react'; +import {Platform, View} from 'react-native'; +import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import Svg, {Path} from 'react-native-svg'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import Tooltip from './Tooltip/PopoverAnchorTooltip'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; -import withStyleUtils, {withStyleUtilsPropTypes} from './withStyleUtils'; -import withTheme, {withThemePropTypes} from './withTheme'; -import withThemeStyles, {withThemeStylesPropTypes} from './withThemeStyles'; -const AnimatedIcon = Animated.createAnimatedComponent(Icon); -AnimatedIcon.displayName = 'AnimatedIcon'; +const AnimatedPath = Animated.createAnimatedComponent(Path); +AnimatedPath.displayName = 'AnimatedPath'; const AnimatedPressable = Animated.createAnimatedComponent(PressableWithFeedback); AnimatedPressable.displayName = 'AnimatedPressable'; +const adapter = createAnimatedPropAdapter( + (props) => { + // eslint-disable-next-line rulesdir/prefer-underscore-method + if (Object.keys(props).includes('fill')) { + // eslint-disable-next-line no-param-reassign + props.fill = {type: 0, payload: processColor(props.fill)}; + } + // eslint-disable-next-line rulesdir/prefer-underscore-method + if (Object.keys(props).includes('stroke')) { + // eslint-disable-next-line no-param-reassign + props.stroke = {type: 0, payload: processColor(props.stroke)}; + } + }, + ['fill', 'stroke'], +); +adapter.propTypes = { + fill: PropTypes.string, + stroke: PropTypes.string, +}; + const propTypes = { - // Callback to fire on request to toggle the FloatingActionButton + /* Callback to fire on request to toggle the FloatingActionButton */ onPress: PropTypes.func.isRequired, - // Current state (active or not active) of the component + /* Current state (active or not active) of the component */ isActive: PropTypes.bool.isRequired, - // Ref for the button - buttonRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - - ...withLocalizePropTypes, - ...withThemePropTypes, - ...withThemeStylesPropTypes, - ...withStyleUtilsPropTypes, -}; + /* An accessibility label for the button */ + accessibilityLabel: PropTypes.string.isRequired, -const defaultProps = { - buttonRef: () => {}, + /* An accessibility role for the button */ + role: PropTypes.string.isRequired, }; -class FloatingActionButton extends PureComponent { - constructor(props) { - super(props); - this.animatedValue = new Animated.Value(props.isActive ? 1 : 0); - } - - componentDidUpdate(prevProps) { - if (prevProps.isActive === this.props.isActive) { - return; - } - - this.animateFloatingActionButton(); - } - - /** - * Animates the floating action button - * Method is called when the isActive prop changes - */ - animateFloatingActionButton() { - const animationFinalValue = this.props.isActive ? 1 : 0; - - Animated.timing(this.animatedValue, { - toValue: animationFinalValue, +const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibilityLabel, role}, ref) => { + const {success, buttonDefaultBG, textLight, textDark} = useTheme(); + const styles = useThemeStyles(); + const borderRadius = styles.floatingActionButton.borderRadius; + const {translate} = useLocalize(); + const fabPressable = useRef(null); + const sharedValue = useSharedValue(isActive ? 1 : 0); + const buttonRef = ref; + + useEffect(() => { + sharedValue.value = withTiming(isActive ? 1 : 0, { duration: 340, easing: Easing.inOut(Easing.ease), - useNativeDriver: false, - }).start(); - } - - render() { - const rotate = this.animatedValue.interpolate({ - inputRange: [0, 1], - outputRange: ['0deg', '135deg'], - }); - - const backgroundColor = this.animatedValue.interpolate({ - inputRange: [0, 1], - outputRange: [this.props.theme.success, this.props.theme.buttonDefaultBG], - }); - - const fill = this.animatedValue.interpolate({ - inputRange: [0, 1], - outputRange: [this.props.theme.textLight, this.props.theme.textDark], }); - - return ( - - - { - this.fabPressable = el; - if (this.props.buttonRef) { - this.props.buttonRef.current = el; - } - }} - accessibilityLabel={this.props.accessibilityLabel} - role={this.props.role} - pressDimmingValue={1} - onPress={(e) => { - // Drop focus to avoid blue focus ring. - this.fabPressable.blur(); - this.props.onPress(e); - }} - onLongPress={() => {}} - style={[this.props.themeStyles.floatingActionButton, this.props.StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]} + }, [isActive, sharedValue]); + + const animatedStyle = useAnimatedStyle(() => { + const backgroundColor = interpolateColor(sharedValue.value, [0, 1], [success, buttonDefaultBG]); + + return { + transform: [{rotate: `${sharedValue.value * 135}deg`}], + backgroundColor, + borderRadius, + }; + }); + + const animatedProps = useAnimatedProps( + () => { + const fill = interpolateColor(sharedValue.value, [0, 1], [textLight, textDark]); + + return { + fill, + }; + }, + undefined, + Platform.OS === 'web' ? undefined : adapter, + ); + + return ( + + + { + fabPressable.current = el; + if (buttonRef) { + buttonRef.current = el; + } + }} + accessibilityLabel={accessibilityLabel} + role={role} + pressDimmingValue={1} + onPress={(e) => { + // Drop focus to avoid blue focus ring. + fabPressable.current.blur(); + onPress(e); + }} + onLongPress={() => {}} + style={[styles.floatingActionButton, animatedStyle]} + > + - - - - - ); - } -} + + + + + ); +}); FloatingActionButton.propTypes = propTypes; -FloatingActionButton.defaultProps = defaultProps; - -const FloatingActionButtonWithLocalize = withLocalize(FloatingActionButton); - -const FloatingActionButtonWithLocalizeWithRef = React.forwardRef((props, ref) => ( - -)); - -FloatingActionButtonWithLocalizeWithRef.displayName = 'FloatingActionButtonWithLocalizeWithRef'; +FloatingActionButton.displayName = 'FloatingActionButton'; -export default compose(withThemeStyles, withTheme, withStyleUtils)(FloatingActionButtonWithLocalizeWithRef); +export default FloatingActionButton; diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index e71d21077eda..6b3dcf7f126a 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -1,15 +1,15 @@ import {ImageContentFit} from 'expo-image'; -import React, {PureComponent} from 'react'; +import React from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; import ImageSVG from '@components/ImageSVG'; -import withStyleUtils, {WithStyleUtilsProps} from '@components/withStyleUtils'; -import withTheme, {WithThemeProps} from '@components/withTheme'; -import withThemeStyles, {type WithThemeStylesProps} from '@components/withThemeStyles'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; import IconAsset from '@src/types/utils/IconAsset'; import IconWrapperStyles from './IconWrapperStyles'; -type IconBaseProps = { +type IconProps = { /** The asset to render. */ src: IconAsset; @@ -43,68 +43,67 @@ type IconBaseProps = { /** Determines how the image should be resized to fit its container */ contentFit?: ImageContentFit; }; -type IconProps = IconBaseProps & WithThemeStylesProps & WithThemeProps & WithStyleUtilsProps; - -// We must use a class component to create an animatable component with the Animated API -// eslint-disable-next-line react/prefer-stateless-function -class Icon extends PureComponent { - // eslint-disable-next-line react/static-property-placement - public static defaultProps: Partial = { - width: variables.iconSizeNormal, - height: variables.iconSizeNormal, - fill: undefined, - small: false, - inline: false, - additionalStyles: [], - hovered: false, - pressed: false, - testID: '', - contentFit: 'cover', - }; - - render() { - const width = this.props.small ? variables.iconSizeSmall : this.props.width; - const height = this.props.small ? variables.iconSizeSmall : this.props.height; - const iconStyles = [this.props.StyleUtils.getWidthAndHeightStyle(width ?? 0, height), IconWrapperStyles, this.props.themeStyles.pAbsolute, this.props.additionalStyles]; - - if (this.props.inline) { - return ( - - - - - - ); - } +function Icon({ + src, + width = variables.iconSizeNormal, + height = variables.iconSizeNormal, + fill = undefined, + small = false, + inline = false, + additionalStyles = [], + hovered = false, + pressed = false, + testID = '', + contentFit = 'cover', +}: IconProps) { + const theme = useTheme(); + const StyleUtils = useStyleUtils(); + const styles = useThemeStyles(); + const iconWidth = small ? variables.iconSizeSmall : width; + const iconHeight = small ? variables.iconSizeSmall : height; + const iconStyles = [StyleUtils.getWidthAndHeightStyle(width ?? 0, height), IconWrapperStyles, styles.pAbsolute, additionalStyles]; + const iconFill = fill ?? theme.icon; + + if (inline) { return ( - + + + ); } + + return ( + + + + ); } -export default withTheme(withThemeStyles(withStyleUtils(Icon))); +Icon.displayName = 'Icon'; + +export default Icon; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index a7bc368983b5..469e6c57da5c 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -445,13 +445,6 @@ function getBackgroundColorWithOpacityStyle(backgroundColor: string, opacity: nu return {}; } -function getAnimatedFABStyle(rotate: Animated.Value, backgroundColor: Animated.Value): Animated.WithAnimatedValue { - return { - transform: [{rotate}], - backgroundColor, - }; -} - function getWidthAndHeightStyle(width: number, height?: number): ViewStyle { return { width, @@ -1013,7 +1006,6 @@ const staticStyleUtils = { combineStyles, displayIfTrue, getAmountFontSizeAndLineHeight, - getAnimatedFABStyle, getAutoCompleteSuggestionContainerStyle, getAvatarBorderRadius, getAvatarBorderStyle, diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 0f97d36a9832..f4126ff34313 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -40,6 +40,11 @@ jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ }, })); +jest.mock('react-native-reanimated', () => ({ + ...jest.requireActual('react-native-reanimated/mock'), + createAnimatedPropAdapter: jest.fn, +})); + /** * We need to keep track of the transitionEnd callback so we can trigger it in our tests */