diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0fac30a26430..eebd6ad532d4 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -777,35 +777,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 @@ -1280,7 +1255,7 @@ SPEC CHECKSUMS: rnmapbox-maps: 6f638ec002aa6e906a6f766d69cd45f968d98e64 RNPermissions: 9b086c8f05b2e2faa587fdc31f4c5ab4509728aa RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c - RNReanimated: ab2e96c6d5591c3dfbb38a464f54c8d17fb34a87 + RNReanimated: fdbaa9c964bbab7fac50c90862b6cc5f041679b9 RNScreens: d037903436160a4b039d32606668350d2a808806 RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d diff --git a/package-lock.json b/package-lock.json index ebe9d98ecefb..5206b9bf8618 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,7 +100,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", @@ -44561,9 +44561,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", @@ -84883,9 +84883,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 06380982ed42..8432a773fdf7 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,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 deleted file mode 100644 index 791eb150f8c9..000000000000 --- a/src/components/FloatingActionButton.js +++ /dev/null @@ -1,132 +0,0 @@ -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 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 AnimatedPressable = Animated.createAnimatedComponent(PressableWithFeedback); -AnimatedPressable.displayName = 'AnimatedPressable'; - -const propTypes = { - // Callback to fire on request to toggle the FloatingActionButton - onPress: PropTypes.func.isRequired, - - // 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, -}; - -const defaultProps = { - buttonRef: () => {}, -}; - -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, - 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)]} - > - - - - - ); - } -} - -FloatingActionButton.propTypes = propTypes; -FloatingActionButton.defaultProps = defaultProps; - -const FloatingActionButtonWithLocalize = withLocalize(FloatingActionButton); - -const FloatingActionButtonWithLocalizeWithRef = React.forwardRef((props, ref) => ( - -)); - -FloatingActionButtonWithLocalizeWithRef.displayName = 'FloatingActionButtonWithLocalizeWithRef'; - -export default compose(withThemeStyles, withTheme, withStyleUtils)(FloatingActionButtonWithLocalizeWithRef); diff --git a/src/components/FloatingActionButton/FabPlusIcon.js b/src/components/FloatingActionButton/FabPlusIcon.js new file mode 100644 index 000000000000..09afa00f119d --- /dev/null +++ b/src/components/FloatingActionButton/FabPlusIcon.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React, {useEffect} from 'react'; +import Animated, {Easing, interpolateColor, useAnimatedProps, useSharedValue, withTiming} from 'react-native-reanimated'; +import Svg, {Path} from 'react-native-svg'; +import useTheme from '@hooks/useTheme'; + +const AnimatedPath = Animated.createAnimatedComponent(Path); + +const propTypes = { + /* Current state (active or not active) of the component */ + isActive: PropTypes.bool.isRequired, +}; + +function FabPlusIcon({isActive}) { + const theme = useTheme(); + const animatedValue = useSharedValue(isActive ? 1 : 0); + + useEffect(() => { + animatedValue.value = withTiming(isActive ? 1 : 0, { + duration: 340, + easing: Easing.inOut(Easing.ease), + }); + }, [isActive, animatedValue]); + + const animatedProps = useAnimatedProps(() => { + const fill = interpolateColor(animatedValue.value, [0, 1], [theme.textLight, theme.textDark]); + + return { + fill, + }; + }); + + return ( + + + + ); +} + +FabPlusIcon.propTypes = propTypes; +FabPlusIcon.displayName = 'FabPlusIcon'; + +export default FabPlusIcon; diff --git a/src/components/FloatingActionButton/index.js b/src/components/FloatingActionButton/index.js new file mode 100644 index 000000000000..d341396c44b7 --- /dev/null +++ b/src/components/FloatingActionButton/index.js @@ -0,0 +1,85 @@ +import PropTypes from 'prop-types'; +import React, {useEffect, useRef} from 'react'; +import {View} from 'react-native'; +import Animated, {Easing, interpolateColor, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import FabPlusIcon from './FabPlusIcon'; + +const AnimatedPressable = Animated.createAnimatedComponent(PressableWithFeedback); +AnimatedPressable.displayName = 'AnimatedPressable'; + +const propTypes = { + /* Callback to fire on request to toggle the FloatingActionButton */ + onPress: PropTypes.func.isRequired, + + /* Current state (active or not active) of the component */ + isActive: PropTypes.bool.isRequired, + + /* An accessibility label for the button */ + accessibilityLabel: PropTypes.string.isRequired, + + /* An accessibility role for the button */ + role: PropTypes.string.isRequired, +}; + +const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibilityLabel, role}, ref) => { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const fabPressable = useRef(null); + const animatedValue = useSharedValue(isActive ? 1 : 0); + const buttonRef = ref; + + useEffect(() => { + animatedValue.value = withTiming(isActive ? 1 : 0, { + duration: 340, + easing: Easing.inOut(Easing.ease), + }); + }, [isActive, animatedValue]); + + const animatedStyle = useAnimatedStyle(() => { + const backgroundColor = interpolateColor(animatedValue.value, [0, 1], [theme.success, theme.buttonDefaultBG]); + + return { + transform: [{rotate: `${animatedValue.value * 135}deg`}], + backgroundColor, + borderRadius: styles.floatingActionButton.borderRadius, + }; + }); + + 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.displayName = 'FloatingActionButton'; + +export default FloatingActionButton; diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 80abe1872c12..5f82421c0e8e 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -1,8 +1,8 @@ -import React, {PureComponent} from 'react'; +import React from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; -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 IconWrapperStyles from './IconWrapperStyles'; @@ -41,65 +41,63 @@ type IconProps = { /** Additional styles to add to the Icon */ additionalStyles?: StyleProp; -} & 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 = { - width: variables.iconSizeNormal, - height: variables.iconSizeNormal, - fill: undefined, - small: false, - inline: false, - additionalStyles: [], - hovered: false, - pressed: false, - }; - - 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]; - const fill = this.props.fill ?? this.props.theme.icon; +}; - if (this.props.inline) { - return ( - - - - - - ); - } +function Icon({ + src, + width = variables.iconSizeNormal, + height = variables.iconSizeNormal, + fill = undefined, + small = false, + inline = false, + hovered = false, + pressed = false, + additionalStyles = [], +}: 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; + const IconComponent = src; + 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 1dbe0b8587fd..a48724e72fc9 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -446,13 +446,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, @@ -1015,7 +1008,6 @@ const staticStyleUtils = { combineStyles, displayIfTrue, getAmountFontSizeAndLineHeight, - getAnimatedFABStyle, getAutoCompleteSuggestionContainerStyle, getAvatarBorderRadius, getAvatarBorderStyle,