diff --git a/android/app/build.gradle b/android/app/build.gradle index c7974572e665..7bf56a7a1177 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001039600 - versionName "1.3.96-0" + versionCode 1001039602 + versionName "1.3.96-2" } flavorDimensions "default" diff --git a/assets/images/bell.svg b/assets/images/bell.svg index 6ba600dc695b..5a6b411185a9 100644 --- a/assets/images/bell.svg +++ b/assets/images/bell.svg @@ -1,6 +1 @@ - - - - - + \ No newline at end of file diff --git a/assets/images/home-background--android.svg b/assets/images/home-background--android.svg index 507aecf04836..2b72b6ccabe9 100644 --- a/assets/images/home-background--android.svg +++ b/assets/images/home-background--android.svg @@ -1,6555 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/babel.config.js b/babel.config.js index 7de6926c850d..64a433936eb7 100644 --- a/babel.config.js +++ b/babel.config.js @@ -17,16 +17,8 @@ const defaultPlugins = [ ]; const webpack = { - env: { - production: { - presets: defaultPresets, - plugins: [...defaultPlugins, 'transform-remove-console'], - }, - development: { - presets: defaultPresets, - plugins: defaultPlugins, - }, - }, + presets: defaultPresets, + plugins: defaultPlugins, }; const metro = { @@ -78,6 +70,11 @@ const metro = { }, ], ], + env: { + production: { + plugins: ['transform-remove-console', {exclude: ['error', 'warn']}], + }, + }, }; /* @@ -102,11 +99,19 @@ if (process.env.CAPTURE_METRICS === 'true') { ]); } -module.exports = ({caller}) => { +module.exports = (api) => { + console.debug('babel.config.js'); + console.debug(' - api.version:', api.version); + console.debug(' - api.env:', api.env()); + console.debug(' - process.env.NODE_ENV:', process.env.NODE_ENV); + console.debug(' - process.env.BABEL_ENV:', process.env.BABEL_ENV); + // For `react-native` (iOS/Android) caller will be "metro" // For `webpack` (Web) caller will be "@babel-loader" // For jest, it will be babel-jest // For `storybook` there won't be any config at all so we must give default argument of an empty object - const runningIn = caller((args = {}) => args.name); + const runningIn = api.caller((args = {}) => args.name); + console.debug(' - running in: ', runningIn); + return ['metro', 'babel-jest'].includes(runningIn) ? metro : webpack; }; diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 94d2986fd111..e31b05dcece6 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.96.0 + 1.3.96.2 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 9478336965cf..1b3695499183 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.3.96.0 + 1.3.96.2 diff --git a/package-lock.json b/package-lock.json index c263632e7615..b4513f4359ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.96-0", + "version": "1.3.96-2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.96-0", + "version": "1.3.96-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index dcbe95560bc7..add29281e08b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.96-0", + "version": "1.3.96-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/ROUTES.ts b/src/ROUTES.ts index bcc4685368cb..864e8934ad88 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2,14 +2,11 @@ import {ValueOf} from 'type-fest'; import CONST from './CONST'; /** - * This is a file containing constants for all of the routes we want to be able to go to + * This is a file containing constants for all the routes we want to be able to go to */ /** - * This is a file containing constants for all of the routes we want to be able to go to - * Returns the URL with an encoded URI component for the backTo param which can be added to the end of URLs - * @param backTo - * @returns + * Builds a URL with an encoded URI component for the `backTo` param which can be added to the end of URLs */ function getUrlWithBackToParam(url: string, backTo?: string): string { const backToParam = backTo ? `${url.includes('?') ? '&' : '?'}backTo=${encodeURIComponent(backTo)}` : ''; @@ -111,7 +108,10 @@ export default { route: 'settings/profile/personal-details/address/country', getRoute: (country: string, backTo?: string) => getUrlWithBackToParam(`settings/profile/personal-details/address/country?country=${country}`, backTo), }, - SETTINGS_CONTACT_METHODS: 'settings/profile/contact-methods', + SETTINGS_CONTACT_METHODS: { + route: 'settings/profile/contact-methods', + getRoute: (backTo?: string) => getUrlWithBackToParam('settings/profile/contact-methods', backTo), + }, SETTINGS_CONTACT_METHOD_DETAILS: { route: 'settings/profile/contact-methods/:contactMethod/details', getRoute: (contactMethod: string) => `settings/profile/contact-methods/${encodeURIComponent(contactMethod)}/details`, diff --git a/src/components/ConnectBankAccountButton.js b/src/components/ConnectBankAccountButton.js index 64d2421c7d37..2c66bcc200da 100644 --- a/src/components/ConnectBankAccountButton.js +++ b/src/components/ConnectBankAccountButton.js @@ -30,7 +30,7 @@ const defaultProps = { }; function ConnectBankAccountButton(props) { - const activeRoute = Navigation.getActiveRoute().replace(/\?.*/, ''); + const activeRoute = Navigation.getActiveRouteWithoutParams(); return props.network.isOffline ? ( {`${props.translate('common.youAppearToBeOffline')} ${props.translate('common.thisFeatureRequiresInternet')}`} diff --git a/src/components/CountrySelector.js b/src/components/CountrySelector.js index 93a90dcf6be9..c2426c5b7b0b 100644 --- a/src/components/CountrySelector.js +++ b/src/components/CountrySelector.js @@ -53,7 +53,7 @@ function CountrySelector({errorText, value: countryCode, onInputChange, forwarde descriptionTextStyle={countryTitleDescStyle} description={translate('common.country')} onPress={() => { - const activeRoute = Navigation.getActiveRoute().replace(/\?.*/, ''); + const activeRoute = Navigation.getActiveRouteWithoutParams(); Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS_COUNTRY.getRoute(countryCode, activeRoute)); }} /> diff --git a/src/components/Image/index.js b/src/components/Image/index.js index c2800511ff45..ef1a69e19c12 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -69,4 +69,5 @@ const ImageWithOnyx = React.memo( imagePropsAreEqual, ); ImageWithOnyx.resizeMode = RESIZE_MODES; + export default ImageWithOnyx; diff --git a/src/components/Image/index.native.js b/src/components/Image/index.native.js index 52ac503081e6..cf5320392d1b 100644 --- a/src/components/Image/index.native.js +++ b/src/components/Image/index.native.js @@ -59,4 +59,5 @@ const ImageWithOnyx = withOnyx({ })(Image); ImageWithOnyx.resizeMode = RESIZE_MODES; ImageWithOnyx.resolveDimensions = resolveDimensions; + export default ImageWithOnyx; diff --git a/src/components/ImageWithSizeCalculation.js b/src/components/ImageWithSizeCalculation.tsx similarity index 66% rename from src/components/ImageWithSizeCalculation.js rename to src/components/ImageWithSizeCalculation.tsx index 5db78e0c1276..fe4cc4a01bc0 100644 --- a/src/components/ImageWithSizeCalculation.js +++ b/src/components/ImageWithSizeCalculation.tsx @@ -1,31 +1,27 @@ -import PropTypes from 'prop-types'; +import delay from 'lodash/delay'; import React, {useEffect, useRef, useState} from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; +import {StyleProp, View, ViewStyle} from 'react-native'; +import {OnLoadEvent} from 'react-native-fast-image'; import Log from '@libs/Log'; import styles from '@styles/styles'; import FullscreenLoadingIndicator from './FullscreenLoadingIndicator'; import Image from './Image'; +import RESIZE_MODES from './Image/resizeModes'; -const propTypes = { +type OnMeasure = (args: {width: number; height: number}) => void; + +type ImageWithSizeCalculationProps = { /** Url for image to display */ - url: PropTypes.string.isRequired, + url: string; /** Any additional styles to apply */ - // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.any, + style?: StyleProp; /** Callback fired when the image has been measured. */ - onMeasure: PropTypes.func, + onMeasure: OnMeasure; /** Whether the image requires an authToken */ - isAuthTokenRequired: PropTypes.bool, -}; - -const defaultProps = { - style: {}, - onMeasure: () => {}, - isAuthTokenRequired: false, + isAuthTokenRequired: boolean; }; /** @@ -33,23 +29,19 @@ const defaultProps = { * Image size must be provided by parent via width and height props. Useful for * performing some calculation on a network image after fetching dimensions so * it can be appropriately resized. - * - * @param {Object} props - * @returns {React.Component} - * */ -function ImageWithSizeCalculation(props) { - const isLoadedRef = useRef(null); +function ImageWithSizeCalculation({url, style, onMeasure, isAuthTokenRequired}: ImageWithSizeCalculationProps) { + const isLoadedRef = useRef(null); const [isImageCached, setIsImageCached] = useState(true); const [isLoading, setIsLoading] = useState(false); const onError = () => { - Log.hmmm('Unable to fetch image to calculate size', {url: props.url}); + Log.hmmm('Unable to fetch image to calculate size', {url}); }; - const imageLoadedSuccessfully = (event) => { + const imageLoadedSuccessfully = (event: OnLoadEvent) => { isLoadedRef.current = true; - props.onMeasure({ + onMeasure({ width: event.nativeEvent.width, height: event.nativeEvent.height, }); @@ -57,10 +49,10 @@ function ImageWithSizeCalculation(props) { /** Delay the loader to detect whether the image is being loaded from the cache or the internet. */ useEffect(() => { - if (isLoadedRef.current || !isLoading) { + if (isLoadedRef.current ?? !isLoading) { return; } - const timeout = _.delay(() => { + const timeout = delay(() => { if (!isLoading || isLoadedRef.current) { return; } @@ -70,14 +62,14 @@ function ImageWithSizeCalculation(props) { }, [isLoading]); return ( - + { - if (isLoadedRef.current || isLoading) { + if (isLoadedRef.current ?? isLoading) { return; } setIsLoading(true); @@ -94,7 +86,5 @@ function ImageWithSizeCalculation(props) { ); } -ImageWithSizeCalculation.propTypes = propTypes; -ImageWithSizeCalculation.defaultProps = defaultProps; ImageWithSizeCalculation.displayName = 'ImageWithSizeCalculation'; export default React.memo(ImageWithSizeCalculation); diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index 8eb1a06d9505..0c5383054d04 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -144,7 +144,7 @@ function LHNOptionsList({ '', )}`; const itemComment = draftComments[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] || ''; - const participantPersonalDetailList = _.values(OptionsListUtils.getPersonalDetailsForAccountIDs(itemFullReport.participantAccountIDs, personalDetails)); + const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(itemFullReport.participantAccountIDs, personalDetails); return ( { if (option.accountID) { - const activeRoute = Navigation.getActiveRoute().replace(/\?.*/, ''); + const activeRoute = Navigation.getActiveRouteWithoutParams(); Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); } else if (option.reportID) { diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.js b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx similarity index 64% rename from src/components/Pressable/GenericPressable/BaseGenericPressable.js rename to src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index d031fdb90ebe..1576fe18da54 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.js +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -1,7 +1,6 @@ -import React, {forwardRef, useCallback, useEffect, useMemo} from 'react'; +import React, {ForwardedRef, forwardRef, useCallback, useEffect, useMemo} from 'react'; // eslint-disable-next-line no-restricted-imports -import {Pressable} from 'react-native'; -import _ from 'underscore'; +import {GestureResponderEvent, Pressable, View, ViewStyle} from 'react-native'; import useSingleExecution from '@hooks/useSingleExecution'; import Accessibility from '@libs/Accessibility'; import HapticFeedback from '@libs/HapticFeedback'; @@ -9,15 +8,12 @@ import KeyboardShortcut from '@libs/KeyboardShortcut'; import styles from '@styles/styles'; import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; -import genericPressablePropTypes from './PropTypes'; +import PressableProps from './types'; /** * Returns the cursor style based on the state of Pressable - * @param {Boolean} isDisabled - * @param {Boolean} isText - * @returns {Object} */ -const getCursorStyle = (isDisabled, isText) => { +function getCursorStyle(isDisabled: boolean, isText: boolean): Pick { if (isDisabled) { return styles.cursorDisabled; } @@ -27,28 +23,34 @@ const getCursorStyle = (isDisabled, isText) => { } return styles.cursorPointer; -}; +} -const GenericPressable = forwardRef((props, ref) => { - const { +function GenericPressable( + { children, - onPress, + onPress = () => {}, onLongPress, - onKeyPress, onKeyDown, disabled, style, - shouldUseHapticsOnLongPress, - shouldUseHapticsOnPress, + disabledStyle = {}, + hoverStyle = {}, + focusStyle = {}, + pressStyle = {}, + screenReaderActiveStyle = {}, + shouldUseHapticsOnLongPress = false, + shouldUseHapticsOnPress = false, nextFocusRef, keyboardShortcut, - shouldUseAutoHitSlop, - enableInScreenReaderStates, + shouldUseAutoHitSlop = false, + enableInScreenReaderStates = CONST.SCREEN_READER_STATES.ALL, onPressIn, onPressOut, + accessible = true, ...rest - } = props; - + }: PressableProps, + ref: ForwardedRef, +) { const {isExecuting, singleExecution} = useSingleExecution(); const isScreenReaderActive = Accessibility.useScreenReaderStatus(); const [hitSlop, onLayout] = Accessibility.useAutoHitSlop(); @@ -63,13 +65,14 @@ const GenericPressable = forwardRef((props, ref) => { shouldBeDisabledByScreenReader = isScreenReaderActive; } - return props.disabled || shouldBeDisabledByScreenReader || isExecuting; - }, [isScreenReaderActive, enableInScreenReaderStates, props.disabled, isExecuting]); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return disabled || shouldBeDisabledByScreenReader || isExecuting; + }, [isScreenReaderActive, enableInScreenReaderStates, disabled, isExecuting]); const shouldUseDisabledCursor = useMemo(() => isDisabled && !isExecuting, [isDisabled, isExecuting]); const onLongPressHandler = useCallback( - (event) => { + (event: GestureResponderEvent) => { if (isDisabled) { return; } @@ -79,8 +82,8 @@ const GenericPressable = forwardRef((props, ref) => { if (shouldUseHapticsOnLongPress) { HapticFeedback.longPress(); } - if (ref && ref.current) { - ref.current.blur(); + if (ref && 'current' in ref) { + ref.current?.blur(); } onLongPress(event); @@ -90,7 +93,7 @@ const GenericPressable = forwardRef((props, ref) => { ); const onPressHandler = useCallback( - (event) => { + (event?: GestureResponderEvent | KeyboardEvent) => { if (isDisabled) { return; } @@ -100,8 +103,8 @@ const GenericPressable = forwardRef((props, ref) => { if (shouldUseHapticsOnPress) { HapticFeedback.press(); } - if (ref && ref.current) { - ref.current.blur(); + if (ref && 'current' in ref) { + ref.current?.blur(); } onPress(event); @@ -110,16 +113,6 @@ const GenericPressable = forwardRef((props, ref) => { [shouldUseHapticsOnPress, onPress, nextFocusRef, ref, isDisabled], ); - const onKeyPressHandler = useCallback( - (event) => { - if (event.key !== 'Enter') { - return; - } - onPressHandler(event); - }, - [onPressHandler], - ); - useEffect(() => { if (!keyboardShortcut) { return () => {}; @@ -135,36 +128,37 @@ const GenericPressable = forwardRef((props, ref) => { ref={ref} onPress={!isDisabled ? singleExecution(onPressHandler) : undefined} onLongPress={!isDisabled && onLongPress ? onLongPressHandler : undefined} - onKeyPress={!isDisabled ? onKeyPressHandler : undefined} onKeyDown={!isDisabled ? onKeyDown : undefined} onPressIn={!isDisabled ? onPressIn : undefined} onPressOut={!isDisabled ? onPressOut : undefined} style={(state) => [ - getCursorStyle(shouldUseDisabledCursor, [props.role, props.role].includes('text')), - StyleUtils.parseStyleFromFunction(props.style, state), - isScreenReaderActive && StyleUtils.parseStyleFromFunction(props.screenReaderActiveStyle, state), - state.focused && StyleUtils.parseStyleFromFunction(props.focusStyle, state), - state.hovered && StyleUtils.parseStyleFromFunction(props.hoverStyle, state), - state.pressed && StyleUtils.parseStyleFromFunction(props.pressStyle, state), - isDisabled && [...StyleUtils.parseStyleFromFunction(props.disabledStyle, state), styles.noSelect], + getCursorStyle(shouldUseDisabledCursor, [rest.accessibilityRole, rest.role].includes('text')), + StyleUtils.parseStyleFromFunction(style, state), + isScreenReaderActive && StyleUtils.parseStyleFromFunction(screenReaderActiveStyle, state), + state.focused && StyleUtils.parseStyleFromFunction(focusStyle, state), + state.hovered && StyleUtils.parseStyleFromFunction(hoverStyle, state), + state.pressed && StyleUtils.parseStyleFromFunction(pressStyle, state), + isDisabled && [StyleUtils.parseStyleFromFunction(disabledStyle, state), styles.noSelect], ]} // accessibility props - aria-checked={props.ariaChecked} + accessibilityState={{ + disabled: isDisabled, + ...rest.accessibilityState, + }} aria-disabled={isDisabled} - aria-keyshortcuts={keyboardShortcut && `${keyboardShortcut.modifiers}+${keyboardShortcut.shortcutKey}`} + aria-keyshortcuts={keyboardShortcut && `${keyboardShortcut.modifiers.join('')}+${keyboardShortcut.shortcutKey}`} // ios-only form of inputs - onMagicTap={!isDisabled && onPressHandler} - onAccessibilityTap={!isDisabled && onPressHandler} + onMagicTap={!isDisabled ? onPressHandler : undefined} + onAccessibilityTap={!isDisabled ? onPressHandler : undefined} + accessible={accessible} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} > - {(state) => (_.isFunction(props.children) ? props.children({...state, isScreenReaderActive, isDisabled}) : props.children)} + {(state) => (typeof children === 'function' ? children({...state, isScreenReaderActive, isDisabled}) : children)} ); -}); +} GenericPressable.displayName = 'GenericPressable'; -GenericPressable.propTypes = genericPressablePropTypes.pressablePropTypes; -GenericPressable.defaultProps = genericPressablePropTypes.defaultProps; -export default GenericPressable; +export default forwardRef(GenericPressable); diff --git a/src/components/Pressable/GenericPressable/PropTypes.js b/src/components/Pressable/GenericPressable/PropTypes.js deleted file mode 100644 index 870c63301239..000000000000 --- a/src/components/Pressable/GenericPressable/PropTypes.js +++ /dev/null @@ -1,142 +0,0 @@ -import PropTypes from 'prop-types'; -import stylePropType from '@styles/stylePropTypes'; -import CONST from '@src/CONST'; - -const stylePropTypeWithFunction = PropTypes.oneOfType([stylePropType, PropTypes.func]); - -/** - * Custom test for required props - * + accessibilityLabel is required when accessible is true - * @param {Object} props - * @returns {Error} Error if prop is required - */ -function requiredPropsCheck(props) { - if (props.accessible !== true || (props.accessibilityLabel !== undefined && typeof props.accessibilityLabel === 'string')) { - return; - } - return new Error(`Provide a valid string for accessibilityLabel prop when accessible is true`); -} - -const pressablePropTypes = { - /** - * onPress callback - */ - onPress: PropTypes.func, - - /** - * Specifies keyboard shortcut to trigger onPressHandler - * @example {shortcutKey: 'a', modifiers: ['ctrl', 'shift'], descriptionKey: 'keyboardShortcut.description'} - */ - keyboardShortcut: PropTypes.shape({ - descriptionKey: PropTypes.string.isRequired, - shortcutKey: PropTypes.string.isRequired, - modifiers: PropTypes.arrayOf(PropTypes.string), - }), - - /** - * Specifies if haptic feedback should be used on press - * @default false - */ - shouldUseHapticsOnPress: PropTypes.bool, - - /** - * Specifies if haptic feedback should be used on long press - * @default false - */ - shouldUseHapticsOnLongPress: PropTypes.bool, - - /** - * style for when the component is disabled. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) - * @default {} - * @example {backgroundColor: 'red'} - * @example state => ({backgroundColor: state.isDisabled ? 'red' : 'blue'}) - */ - disabledStyle: stylePropTypeWithFunction, - - /** - * style for when the component is hovered. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) - * @default {} - * @example {backgroundColor: 'red'} - * @example state => ({backgroundColor: state.hovered ? 'red' : 'blue'}) - */ - hoverStyle: stylePropTypeWithFunction, - - /** - * style for when the component is focused. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) - * @default {} - * @example {backgroundColor: 'red'} - * @example state => ({backgroundColor: state.focused ? 'red' : 'blue'}) - */ - focusStyle: stylePropTypeWithFunction, - - /** - * style for when the component is pressed. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) - * @default {} - * @example {backgroundColor: 'red'} - * @example state => ({backgroundColor: state.pressed ? 'red' : 'blue'}) - */ - pressStyle: stylePropTypeWithFunction, - - /** - * style for when the component is active and the screen reader is on. - * Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) - * @default {} - * @example {backgroundColor: 'red'} - * @example state => ({backgroundColor: state.isScreenReaderActive ? 'red' : 'blue'}) - */ - screenReaderActiveStyle: stylePropTypeWithFunction, - - /** - * Specifies if the component should be accessible when the screen reader is on - * @default 'all' - * @example 'all' - the component is accessible regardless of screen reader state - * @example 'active' - the component is accessible only when the screen reader is on - * @example 'disabled' - the component is not accessible when the screen reader is on - */ - enableInScreenReaderStates: PropTypes.oneOf([CONST.SCREEN_READER_STATES.ALL, CONST.SCREEN_READER_STATES.ACTIVE, CONST.SCREEN_READER_STATES.DISABLED]), - - /** - * Specifies which component should be focused after interacting with this component - */ - nextFocusRef: PropTypes.func, - - /** - * Specifies the accessibility label for the component - * @example 'Search' - * @example 'Close' - */ - accessibilityLabel: requiredPropsCheck, - - /** - * Specifies the accessibility hint for the component - * @example 'Double tap to open' - */ - accessibilityHint: PropTypes.string, - - /** - * Specifies if the component should calculate its hitSlop automatically - * @default true - */ - shouldUseAutoHitSlop: PropTypes.bool, -}; - -const defaultProps = { - onPress: () => {}, - keyboardShortcut: undefined, - shouldUseHapticsOnPress: false, - shouldUseHapticsOnLongPress: false, - disabledStyle: {}, - hoverStyle: {}, - focusStyle: {}, - pressStyle: {}, - screenReaderActiveStyle: {}, - enableInScreenReaderStates: CONST.SCREEN_READER_STATES.ALL, - nextFocusRef: undefined, - shouldUseAutoHitSlop: false, - accessible: true, -}; - -export default { - pressablePropTypes, - defaultProps, -}; diff --git a/src/components/Pressable/GenericPressable/index.js b/src/components/Pressable/GenericPressable/index.js deleted file mode 100644 index 57fc29b8b3f8..000000000000 --- a/src/components/Pressable/GenericPressable/index.js +++ /dev/null @@ -1,25 +0,0 @@ -import React, {forwardRef} from 'react'; -import GenericPressable from './BaseGenericPressable'; -import GenericPressablePropTypes from './PropTypes'; - -const WebGenericPressable = forwardRef((props, ref) => ( - -)); - -WebGenericPressable.propTypes = GenericPressablePropTypes.pressablePropTypes; -WebGenericPressable.defaultProps = GenericPressablePropTypes.defaultProps; -WebGenericPressable.displayName = 'WebGenericPressable'; - -export default WebGenericPressable; diff --git a/src/components/Pressable/GenericPressable/index.native.js b/src/components/Pressable/GenericPressable/index.native.js deleted file mode 100644 index 407d83bf1eff..000000000000 --- a/src/components/Pressable/GenericPressable/index.native.js +++ /dev/null @@ -1,20 +0,0 @@ -import React, {forwardRef} from 'react'; -import GenericPressable from './BaseGenericPressable'; -import GenericPressablePropTypes from './PropTypes'; - -const NativeGenericPressable = forwardRef((props, ref) => ( - -)); - -NativeGenericPressable.propTypes = GenericPressablePropTypes.pressablePropTypes; -NativeGenericPressable.defaultProps = GenericPressablePropTypes.defaultProps; -NativeGenericPressable.displayName = 'WebGenericPressable'; - -export default NativeGenericPressable; diff --git a/src/components/Pressable/GenericPressable/index.native.tsx b/src/components/Pressable/GenericPressable/index.native.tsx new file mode 100644 index 000000000000..5bed0f488063 --- /dev/null +++ b/src/components/Pressable/GenericPressable/index.native.tsx @@ -0,0 +1,21 @@ +import React, {ForwardedRef, forwardRef} from 'react'; +import {View} from 'react-native'; +import GenericPressable from './BaseGenericPressable'; +import PressableProps from './types'; + +function NativeGenericPressable(props: PressableProps, ref: ForwardedRef) { + return ( + + ); +} + +NativeGenericPressable.displayName = 'NativeGenericPressable'; + +export default forwardRef(NativeGenericPressable); diff --git a/src/components/Pressable/GenericPressable/index.tsx b/src/components/Pressable/GenericPressable/index.tsx new file mode 100644 index 000000000000..c8e9560062e0 --- /dev/null +++ b/src/components/Pressable/GenericPressable/index.tsx @@ -0,0 +1,30 @@ +import React, {ForwardedRef, forwardRef} from 'react'; +import {Role, View} from 'react-native'; +import GenericPressable from './BaseGenericPressable'; +import PressableProps from './types'; + +function WebGenericPressable(props: PressableProps, ref: ForwardedRef) { + return ( + + ); +} + +WebGenericPressable.displayName = 'WebGenericPressable'; + +export default forwardRef(WebGenericPressable); diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts new file mode 100644 index 000000000000..35616cb600a3 --- /dev/null +++ b/src/components/Pressable/GenericPressable/types.ts @@ -0,0 +1,147 @@ +import {ElementRef, RefObject} from 'react'; +import {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, StyleProp, ViewStyle} from 'react-native'; +import {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; + +type StylePropWithFunction = StyleProp | ((state: PressableStateCallbackType) => StyleProp); + +type Shortcut = { + displayName: string; + shortcutKey: string; + descriptionKey: string; + modifiers: string[]; +}; + +type RequiredAccessibilityLabel = + | { + /** + * When true, indicates that the view is an accessibility element. + * By default, all the touchable elements are accessible. + */ + accessible?: true | undefined; + + /** + * Specifies the accessibility label for the component + * @example 'Search' + * @example 'Close' + */ + accessibilityLabel: string; + } + | { + /** + * When false, indicates that the view is not an accessibility element. + */ + accessible: false; + + /** + * Specifies the accessibility label for the component + * @example 'Search' + * @example 'Close' + */ + accessibilityLabel?: string; + }; + +type PressableProps = RNPressableProps & + RequiredAccessibilityLabel & { + /** + * onPress callback + */ + onPress: (event?: GestureResponderEvent | KeyboardEvent) => void; + + /** + * Specifies keyboard shortcut to trigger onPressHandler + * @example {shortcutKey: 'a', modifiers: ['ctrl', 'shift'], descriptionKey: 'keyboardShortcut.description'} + */ + keyboardShortcut?: Shortcut; + + /** + * Specifies if haptic feedback should be used on press + * @default false + */ + shouldUseHapticsOnPress?: boolean; + + /** + * Specifies if haptic feedback should be used on long press + * @default false + */ + shouldUseHapticsOnLongPress?: boolean; + + /** + * style for when the component is disabled. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) + * @default {} + * @example {backgroundColor: 'red'} + * @example state => ({backgroundColor: state.isDisabled ? 'red' : 'blue'}) + */ + disabledStyle?: StylePropWithFunction; + + /** + * style for when the component is hovered. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) + * @default {} + * @example {backgroundColor: 'red'} + * @example state => ({backgroundColor: state.hovered ? 'red' : 'blue'}) + */ + hoverStyle?: StylePropWithFunction; + + /** + * style for when the component is focused. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) + * @default {} + * @example {backgroundColor: 'red'} + * @example state => ({backgroundColor: state.focused ? 'red' : 'blue'}) + */ + focusStyle?: StylePropWithFunction; + + /** + * style for when the component is pressed. Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) + * @default {} + * @example {backgroundColor: 'red'} + * @example state => ({backgroundColor: state.pressed ? 'red' : 'blue'}) + */ + pressStyle?: StylePropWithFunction; + + /** + * style for when the component is active and the screen reader is on. + * Can be a function that receives the component's state (active, disabled, hover, focus, pressed, isScreenReaderActive) + * @default {} + * @example {backgroundColor: 'red'} + * @example state => ({backgroundColor: state.isScreenReaderActive ? 'red' : 'blue'}) + */ + screenReaderActiveStyle?: StylePropWithFunction; + + /** + * Specifies if the component should be accessible when the screen reader is on + * @default 'all' + * @example 'all' - the component is accessible regardless of screen reader state + * @example 'active' - the component is accessible only when the screen reader is on + * @example 'disabled' - the component is not accessible when the screen reader is on + */ + enableInScreenReaderStates?: ValueOf; + + /** + * Specifies which component should be focused after interacting with this component + */ + nextFocusRef?: ElementRef> & RefObject; + + /** + * Specifies the accessibility label for the component + * @example 'Search' + * @example 'Close' + */ + accessibilityLabel?: string; + + /** + * Specifies the accessibility hint for the component + * @example 'Double tap to open' + */ + accessibilityHint?: string; + + /** + * Specifies if the component should calculate its hitSlop automatically + * @default true + */ + shouldUseAutoHitSlop?: boolean; + + /** Turns off drag area for the component */ + noDragArea?: boolean; + }; + +export default PressableProps; diff --git a/src/components/Pressable/PressableWithDelayToggle.js b/src/components/Pressable/PressableWithDelayToggle.tsx similarity index 51% rename from src/components/Pressable/PressableWithDelayToggle.js rename to src/components/Pressable/PressableWithDelayToggle.tsx index 1b5da3dca38c..c402710d71bd 100644 --- a/src/components/Pressable/PressableWithDelayToggle.js +++ b/src/components/Pressable/PressableWithDelayToggle.tsx @@ -1,8 +1,9 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +/* eslint-disable react-native-a11y/has-valid-accessibility-descriptors */ +import React, {ForwardedRef, forwardRef} from 'react'; +import {Text as RNText, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import {SvgProps} from 'react-native-svg'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import refPropTypes from '@components/refPropTypes'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; @@ -10,68 +11,61 @@ import getButtonState from '@libs/getButtonState'; import styles from '@styles/styles'; import * as StyleUtils from '@styles/StyleUtils'; import variables from '@styles/variables'; +import PressableProps from './GenericPressable/types'; import PressableWithoutFeedback from './PressableWithoutFeedback'; -const propTypes = { - /** Ref passed to the component by React.forwardRef (do not pass from parent) */ - innerRef: refPropTypes, - +type PressableWithDelayToggleProps = PressableProps & { /** The text to display */ - text: PropTypes.string, + text: string; /** The text to display once the pressable is pressed */ - textChecked: PropTypes.string, + textChecked: string; /** The tooltip text to display */ - tooltipText: PropTypes.string, + tooltipText: string; /** The tooltip text to display once the pressable is pressed */ - tooltipTextChecked: PropTypes.string, + tooltipTextChecked: string; /** Styles to apply to the container */ - // eslint-disable-next-line react/forbid-prop-types - styles: PropTypes.arrayOf(PropTypes.object), + styles?: StyleProp; - /** Styles to apply to the text */ - // eslint-disable-next-line react/forbid-prop-types - textStyles: PropTypes.arrayOf(PropTypes.object), + // /** Styles to apply to the text */ + textStyles?: StyleProp; /** Styles to apply to the icon */ - // eslint-disable-next-line react/forbid-prop-types - iconStyles: PropTypes.arrayOf(PropTypes.object), - - /** Callback to be called on onPress */ - onPress: PropTypes.func.isRequired, + iconStyles?: StyleProp; /** The icon to display */ - icon: PropTypes.func, + icon?: React.FC; /** The icon to display once the pressable is pressed */ - iconChecked: PropTypes.func, + iconChecked?: React.FC; /** * Should be set to `true` if this component is being rendered inline in * another `Text`. This is due to limitations in RN regarding the * vertical text alignment of non-Text elements */ - inline: PropTypes.bool, -}; - -const defaultProps = { - text: '', - textChecked: '', - tooltipText: '', - tooltipTextChecked: '', - styles: [], - textStyles: [], - iconStyles: [], - icon: null, - inline: true, - iconChecked: Expensicons.Checkmark, - innerRef: () => {}, + inline?: boolean; }; -function PressableWithDelayToggle(props) { +function PressableWithDelayToggle( + { + iconChecked = Expensicons.Checkmark, + inline = true, + onPress, + text, + textChecked, + tooltipText, + tooltipTextChecked, + styles: pressableStyle, + textStyles, + iconStyles, + icon, + }: PressableWithDelayToggleProps, + ref: ForwardedRef, +) { const [isActive, temporarilyDisableInteractions] = useThrottledButtonState(); const updatePressState = () => { @@ -79,54 +73,57 @@ function PressableWithDelayToggle(props) { return; } temporarilyDisableInteractions(); - props.onPress(); + onPress(); }; // Due to limitations in RN regarding the vertical text alignment of non-Text elements, // for elements that are supposed to be inline, we need to use a Text element instead // of a Pressable - const PressableView = props.inline ? Text : PressableWithoutFeedback; - const tooltipText = !isActive ? props.tooltipTextChecked : props.tooltipText; + const PressableView = inline ? Text : PressableWithoutFeedback; + const tooltipTexts = !isActive ? tooltipTextChecked : tooltipText; const labelText = ( - {!isActive && props.textChecked ? props.textChecked : props.text} + {!isActive && textChecked ? textChecked : text}   ); return ( <> - {props.inline && labelText} + {inline && labelText} {({hovered, pressed}) => ( <> - {!props.inline && labelText} - {props.icon && ( + {!inline && labelText} + {icon && ( )} @@ -138,18 +135,6 @@ function PressableWithDelayToggle(props) { ); } -PressableWithDelayToggle.propTypes = propTypes; -PressableWithDelayToggle.defaultProps = defaultProps; PressableWithDelayToggle.displayName = 'PressableWithDelayToggle'; -const PressableWithDelayToggleWithRef = React.forwardRef((props, ref) => ( - -)); - -PressableWithDelayToggleWithRef.displayName = 'PressableWithDelayToggleWithRef'; - -export default PressableWithDelayToggleWithRef; +export default forwardRef(PressableWithDelayToggle); diff --git a/src/components/Pressable/PressableWithFeedback.js b/src/components/Pressable/PressableWithFeedback.js deleted file mode 100644 index 2ef0ddeb5483..000000000000 --- a/src/components/Pressable/PressableWithFeedback.js +++ /dev/null @@ -1,95 +0,0 @@ -import propTypes from 'prop-types'; -import React, {forwardRef, useState} from 'react'; -import _ from 'underscore'; -import OpacityView from '@components/OpacityView'; -import variables from '@styles/variables'; -import GenericPressable from './GenericPressable'; -import GenericPressablePropTypes from './GenericPressable/PropTypes'; - -const omittedProps = ['wrapperStyle', 'needsOffscreenAlphaCompositing']; - -const PressableWithFeedbackPropTypes = { - ...GenericPressablePropTypes.pressablePropTypes, - /** - * Determines what opacity value should be applied to the underlaying view when Pressable is pressed. - * To disable dimming, pass 1 as pressDimmingValue - * @default variables.pressDimValue - */ - pressDimmingValue: propTypes.number, - /** - * Determines what opacity value should be applied to the underlaying view when pressable is hovered. - * To disable dimming, pass 1 as hoverDimmingValue - * @default variables.hoverDimValue - */ - hoverDimmingValue: propTypes.number, - /** - * Used to locate this view from native classes. - */ - id: propTypes.string, - - /** Whether the view needs to be rendered offscreen (for Android only) */ - needsOffscreenAlphaCompositing: propTypes.bool, -}; - -const PressableWithFeedbackDefaultProps = { - ...GenericPressablePropTypes.defaultProps, - pressDimmingValue: variables.pressDimValue, - hoverDimmingValue: variables.hoverDimValue, - id: '', - wrapperStyle: [], - needsOffscreenAlphaCompositing: false, -}; - -const PressableWithFeedback = forwardRef((props, ref) => { - const propsWithoutWrapperProps = _.omit(props, omittedProps); - const [isPressed, setIsPressed] = useState(false); - const [isHovered, setIsHovered] = useState(false); - - return ( - - { - setIsHovered(true); - if (props.onHoverIn) { - props.onHoverIn(); - } - }} - onHoverOut={() => { - setIsHovered(false); - if (props.onHoverOut) { - props.onHoverOut(); - } - }} - onPressIn={() => { - setIsPressed(true); - if (props.onPressIn) { - props.onPressIn(); - } - }} - onPressOut={() => { - setIsPressed(false); - if (props.onPressOut) { - props.onPressOut(); - } - }} - > - {(state) => (_.isFunction(props.children) ? props.children(state) : props.children)} - - - ); -}); - -PressableWithFeedback.displayName = 'PressableWithFeedback'; -PressableWithFeedback.propTypes = PressableWithFeedbackPropTypes; -PressableWithFeedback.defaultProps = PressableWithFeedbackDefaultProps; - -export default PressableWithFeedback; diff --git a/src/components/Pressable/PressableWithFeedback.tsx b/src/components/Pressable/PressableWithFeedback.tsx new file mode 100644 index 000000000000..5d7f7c110ea7 --- /dev/null +++ b/src/components/Pressable/PressableWithFeedback.tsx @@ -0,0 +1,90 @@ +import React, {ForwardedRef, forwardRef, useState} from 'react'; +import {StyleProp, View, ViewStyle} from 'react-native'; +import {AnimatedStyle} from 'react-native-reanimated'; +import OpacityView from '@components/OpacityView'; +import variables from '@styles/variables'; +import GenericPressable from './GenericPressable'; +import PressableProps from './GenericPressable/types'; + +type PressableWithFeedbackProps = PressableProps & { + /** Style for the wrapper view */ + wrapperStyle?: StyleProp>; + + /** + * Determines what opacity value should be applied to the underlaying view when Pressable is pressed. + * To disable dimming, pass 1 as pressDimmingValue + * @default variables.pressDimValue + */ + pressDimmingValue?: number; + + /** + * Determines what opacity value should be applied to the underlaying view when pressable is hovered. + * To disable dimming, pass 1 as hoverDimmingValue + * @default variables.hoverDimValue + */ + hoverDimmingValue?: number; + + /** Whether the view needs to be rendered offscreen (for Android only) */ + needsOffscreenAlphaCompositing?: boolean; +}; + +function PressableWithFeedback( + { + children, + wrapperStyle = [], + needsOffscreenAlphaCompositing = false, + pressDimmingValue = variables.pressDimValue, + hoverDimmingValue = variables.hoverDimValue, + ...rest + }: PressableWithFeedbackProps, + ref: ForwardedRef, +) { + const [isPressed, setIsPressed] = useState(false); + const [isHovered, setIsHovered] = useState(false); + + return ( + + { + setIsHovered(true); + if (rest.onHoverIn) { + rest.onHoverIn(event); + } + }} + onHoverOut={(event) => { + setIsHovered(false); + if (rest.onHoverOut) { + rest.onHoverOut(event); + } + }} + onPressIn={(event) => { + setIsPressed(true); + if (rest.onPressIn) { + rest.onPressIn(event); + } + }} + onPressOut={(event) => { + setIsPressed(false); + if (rest.onPressOut) { + rest.onPressOut(event); + } + }} + > + {(state) => (typeof children === 'function' ? children(state) : children)} + + + ); +} + +PressableWithFeedback.displayName = 'PressableWithFeedback'; + +export default forwardRef(PressableWithFeedback); diff --git a/src/components/Pressable/PressableWithoutFeedback.js b/src/components/Pressable/PressableWithoutFeedback.js deleted file mode 100644 index 92e704550dec..000000000000 --- a/src/components/Pressable/PressableWithoutFeedback.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import _ from 'underscore'; -import GenericPressable from './GenericPressable'; -import GenericPressableProps from './GenericPressable/PropTypes'; - -const omittedProps = ['pressStyle', 'hoverStyle', 'focusStyle', 'activeStyle', 'disabledStyle', 'screenReaderActiveStyle', 'shouldUseHapticsOnPress', 'shouldUseHapticsOnLongPress']; - -const PressableWithoutFeedback = React.forwardRef((props, ref) => { - const propsWithoutStyling = _.omit(props, omittedProps); - return ( - - ); -}); - -PressableWithoutFeedback.displayName = 'PressableWithoutFeedback'; -PressableWithoutFeedback.propTypes = _.omit(GenericPressableProps.pressablePropTypes, omittedProps); -PressableWithoutFeedback.defaultProps = _.omit(GenericPressableProps.defaultProps, omittedProps); - -export default PressableWithoutFeedback; diff --git a/src/components/Pressable/PressableWithoutFeedback.tsx b/src/components/Pressable/PressableWithoutFeedback.tsx new file mode 100644 index 000000000000..c3b780e63cfd --- /dev/null +++ b/src/components/Pressable/PressableWithoutFeedback.tsx @@ -0,0 +1,21 @@ +import React, {ForwardedRef} from 'react'; +import {View} from 'react-native'; +import GenericPressable from './GenericPressable'; +import PressableProps from './GenericPressable/types'; + +function PressableWithoutFeedback( + {pressStyle, hoverStyle, focusStyle, disabledStyle, screenReaderActiveStyle, shouldUseHapticsOnPress, shouldUseHapticsOnLongPress, ...rest}: PressableProps, + ref: ForwardedRef, +) { + return ( + + ); +} + +PressableWithoutFeedback.displayName = 'PressableWithoutFeedback'; + +export default React.forwardRef(PressableWithoutFeedback); diff --git a/src/components/Pressable/PressableWithoutFocus.js b/src/components/Pressable/PressableWithoutFocus.js deleted file mode 100644 index 641e695b1013..000000000000 --- a/src/components/Pressable/PressableWithoutFocus.js +++ /dev/null @@ -1,68 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import _ from 'underscore'; -import StylePropType from '@styles/stylePropTypes'; -import GenericPressable from './GenericPressable'; -import genericPressablePropTypes from './GenericPressable/PropTypes'; - -const propTypes = { - /** Element that should be clickable */ - children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, - - /** Callback for onPress event */ - onPress: PropTypes.func.isRequired, - - /** Callback for onLongPress event */ - onLongPress: PropTypes.func, - - /** Styles that should be passed to touchable container */ - style: StylePropType, - - /** Proptypes of pressable component used for implementation */ - ...genericPressablePropTypes.pressablePropTypes, -}; - -const defaultProps = { - style: [], - onLongPress: undefined, -}; - -/** - * This component prevents the tapped element from capturing focus. - * We need to blur this element when clicked as it opens modal that implements focus-trapping. - * When the modal is closed it focuses back to the last active element. - * Therefore it shifts the element to bring it back to focus. - * https://github.com/Expensify/App/issues/6806 - */ -class PressableWithoutFocus extends React.Component { - constructor(props) { - super(props); - this.pressAndBlur = this.pressAndBlur.bind(this); - } - - pressAndBlur() { - this.pressableRef.blur(); - this.props.onPress(); - } - - render() { - const restProps = _.omit(this.props, ['children', 'onPress', 'onLongPress', 'style']); - return ( - (this.pressableRef = el)} - style={this.props.style} - // eslint-disable-next-line react/jsx-props-no-spreading - {...restProps} - > - {this.props.children} - - ); - } -} - -PressableWithoutFocus.propTypes = propTypes; -PressableWithoutFocus.defaultProps = defaultProps; - -export default PressableWithoutFocus; diff --git a/src/components/Pressable/PressableWithoutFocus.tsx b/src/components/Pressable/PressableWithoutFocus.tsx new file mode 100644 index 000000000000..32cb1708baf0 --- /dev/null +++ b/src/components/Pressable/PressableWithoutFocus.tsx @@ -0,0 +1,36 @@ +import React, {useRef} from 'react'; +import {View} from 'react-native'; +import GenericPressable from './GenericPressable'; +import PressableProps from './GenericPressable/types'; + +/** + * This component prevents the tapped element from capturing focus. + * We need to blur this element when clicked as it opens modal that implements focus-trapping. + * When the modal is closed it focuses back to the last active element. + * Therefore it shifts the element to bring it back to focus. + * https://github.com/Expensify/App/issues/6806 + */ +function PressableWithoutFocus({children, onPress, onLongPress, ...rest}: PressableProps) { + const ref = useRef(null); + + const pressAndBlur = () => { + ref?.current?.blur(); + onPress(); + }; + + return ( + + {children} + + ); +} + +PressableWithoutFocus.displayName = 'PressableWithoutFocus'; + +export default PressableWithoutFocus; diff --git a/src/components/Pressable/index.js b/src/components/Pressable/index.ts similarity index 100% rename from src/components/Pressable/index.js rename to src/components/Pressable/index.ts diff --git a/src/components/ReportActionItem/TaskView.js b/src/components/ReportActionItem/TaskView.js index 9aa85392dde7..b12d6ae32128 100644 --- a/src/components/ReportActionItem/TaskView.js +++ b/src/components/ReportActionItem/TaskView.js @@ -58,8 +58,8 @@ function TaskView(props) { Task.clearEditTaskErrors(props.report.reportID)} + errors={lodashGet(props, 'report.errorFields.editTask') || lodashGet(props, 'report.errorFields.createTask')} + onClose={() => Task.clearTaskErrors(props.report.reportID)} errorRowStyles={styles.ph5} > diff --git a/src/hooks/useSingleExecution.ts b/src/hooks/useSingleExecution.ts new file mode 100644 index 000000000000..16a98152def1 --- /dev/null +++ b/src/hooks/useSingleExecution.ts @@ -0,0 +1,40 @@ +import {useCallback, useRef, useState} from 'react'; +import {InteractionManager} from 'react-native'; + +type Action = (...params: T) => void | Promise; + +/** + * With any action passed in, it will only allow 1 such action to occur at a time. + */ +export default function useSingleExecution() { + const [isExecuting, setIsExecuting] = useState(false); + const isExecutingRef = useRef(); + + isExecutingRef.current = isExecuting; + + const singleExecution = useCallback( + (action: Action) => + (...params: T) => { + if (isExecutingRef.current) { + return; + } + + setIsExecuting(true); + isExecutingRef.current = true; + + const execution = action(...params); + InteractionManager.runAfterInteractions(() => { + if (!(execution instanceof Promise)) { + setIsExecuting(false); + return; + } + execution.finally(() => { + setIsExecuting(false); + }); + }); + }, + [], + ); + + return {isExecuting, singleExecution}; +} diff --git a/src/hooks/useSingleExecution/index.native.js b/src/hooks/useSingleExecution/index.native.js index a2b4ccb4cd53..16a98152def1 100644 --- a/src/hooks/useSingleExecution/index.native.js +++ b/src/hooks/useSingleExecution/index.native.js @@ -1,20 +1,20 @@ import {useCallback, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; +type Action = (...params: T) => void | Promise; + /** * With any action passed in, it will only allow 1 such action to occur at a time. - * - * @returns {Object} */ export default function useSingleExecution() { const [isExecuting, setIsExecuting] = useState(false); - const isExecutingRef = useRef(); + const isExecutingRef = useRef(); isExecutingRef.current = isExecuting; const singleExecution = useCallback( - (action) => - (...params) => { + (action: Action) => + (...params: T) => { if (isExecutingRef.current) { return; } diff --git a/src/languages/en.ts b/src/languages/en.ts index 8ce15d16f4fe..38efe0ef92f6 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1637,6 +1637,7 @@ export default { markAsComplete: 'Mark as complete', markAsIncomplete: 'Mark as incomplete', assigneeError: 'There was an error assigning this task, please try another assignee.', + genericCreateTaskFailureMessage: 'Unexpected error create task, please try again later.', }, statementPage: { generatingPDF: "We're generating your PDF right now. Please come back later!", diff --git a/src/languages/es.ts b/src/languages/es.ts index 9d0184ffff30..2bdb71ae82f7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1660,6 +1660,7 @@ export default { markAsComplete: 'Marcar como completada', markAsIncomplete: 'Marcar como incompleta', assigneeError: 'Hubo un error al asignar esta tarea, inténtalo con otro usuario.', + genericCreateTaskFailureMessage: 'Error inesperado al crear el tarea, por favor, inténtalo más tarde.', }, statementPage: { generatingPDF: 'Estamos generando tu PDF ahora mismo. ¡Por favor, vuelve más tarde!', diff --git a/src/libs/Accessibility/index.ts b/src/libs/Accessibility/index.ts index 5eceda8edcb1..aa167b1239b2 100644 --- a/src/libs/Accessibility/index.ts +++ b/src/libs/Accessibility/index.ts @@ -42,7 +42,7 @@ const useAutoHitSlop = () => { }, [frameSize], ); - return [getHitSlopForSize(frameSize), onLayout]; + return [getHitSlopForSize(frameSize), onLayout] as const; }; export default { diff --git a/src/libs/KeyboardShortcut/index.ts b/src/libs/KeyboardShortcut/index.ts index cfcf5d5ef535..1b684a7ab19f 100644 --- a/src/libs/KeyboardShortcut/index.ts +++ b/src/libs/KeyboardShortcut/index.ts @@ -128,7 +128,7 @@ function getPlatformEquivalentForKeys(keys: string[]): string[] { */ function subscribe( key: string, - callback: () => void, + callback: (event?: KeyboardEvent) => void, descriptionKey: string, modifiers: string[] = ['shift'], captureOnInputs = false, diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index ae13e2b07206..de6c4a64237b 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -96,8 +96,8 @@ function navigate(route = ROUTES.HOME, type) { /** * @param {String} fallbackRoute - Fallback route if pop/goBack action should, but is not possible within RHP - * @param {Bool} shouldEnforceFallback - Enforces navigation to fallback route - * @param {Bool} shouldPopToTop - Should we navigate to LHN on back press + * @param {Boolean} shouldEnforceFallback - Enforces navigation to fallback route + * @param {Boolean} shouldPopToTop - Should we navigate to LHN on back press */ function goBack(fallbackRoute, shouldEnforceFallback = false, shouldPopToTop = false) { if (!canNavigate('goBack')) { @@ -207,6 +207,14 @@ function getActiveRoute() { return ''; } +/** + * Returns the current active route without the URL params + * @returns {String} + */ +function getActiveRouteWithoutParams() { + return getActiveRoute().replace(/\?.*/, ''); +} + /** Returns the active route name from a state event from the navigationRef * @param {Object} event * @returns {String | undefined} @@ -270,6 +278,7 @@ export default { dismissModal, isActiveRoute, getActiveRoute, + getActiveRouteWithoutParams, goBack, isNavigationReady, setIsNavigationReady, diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index b2db1758f24b..c017e6c7664e 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -142,7 +142,7 @@ export default { exact: true, }, Settings_ContactMethods: { - path: ROUTES.SETTINGS_CONTACT_METHODS, + path: ROUTES.SETTINGS_CONTACT_METHODS.route, exact: true, }, Settings_ContactMethodDetails: { diff --git a/src/libs/actions/ReportActions.ts b/src/libs/actions/ReportActions.ts index b9cea498a3fa..d7ff96fc6c2e 100644 --- a/src/libs/actions/ReportActions.ts +++ b/src/libs/actions/ReportActions.ts @@ -4,6 +4,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ReportAction from '@src/types/onyx/ReportAction'; +import * as Report from './Report'; function clearReportActionErrors(reportID: string, reportAction: ReportAction) { const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); @@ -24,6 +25,11 @@ function clearReportActionErrors(reportID: string, reportAction: ReportAction) { Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID}`, null); } + // Delete the failed task report too + const taskReportID = reportAction.message?.[0]?.taskReportID; + if (taskReportID) { + Report.deleteReport(taskReportID); + } return; } diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index 959710967881..e884a4d7a6b3 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -15,6 +15,7 @@ import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import * as Report from './Report'; let currentUserEmail; let currentUserAccountID; @@ -134,9 +135,13 @@ function createTaskAndNavigate(parentReportID, title, description, assigneeEmail // FOR TASK REPORT const failureData = [ { - onyxMethod: Onyx.METHOD.SET, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTaskReport.reportID}`, - value: null, + value: { + errorFields: { + createTask: ErrorUtils.getMicroSecondOnyxError('task.genericCreateTaskFailureMessage'), + }, + }, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -186,7 +191,11 @@ function createTaskAndNavigate(parentReportID, title, description, assigneeEmail failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, - value: {[optimisticAddCommentReport.reportAction.reportActionID]: {pendingAction: null}}, + value: { + [optimisticAddCommentReport.reportAction.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxError('task.genericCreateTaskFailureMessage'), + }, + }, }); clearOutTaskInfo(); @@ -879,7 +888,19 @@ function canModifyTask(taskReport, sessionAccountID) { /** * @param {String} reportID */ -function clearEditTaskErrors(reportID) { +function clearTaskErrors(reportID) { + const report = ReportUtils.getReport(reportID); + + // Delete the task preview in the parent report + if (lodashGet(report, 'pendingFields.createChat') === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, { + [report.parentReportActionID]: null, + }); + + Report.navigateToConciergeChatAndDeleteReport(reportID); + return; + } + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { pendingFields: null, errorFields: null, @@ -934,7 +955,7 @@ export { cancelTask, dismissModalAndClearOutTaskInfo, getTaskAssigneeAccountID, - clearEditTaskErrors, + clearTaskErrors, canModifyTask, getTaskReportActionMessage, }; diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index f7375a5583a6..3c91dc4624cd 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -238,7 +238,7 @@ function deleteContactMethod(contactMethod, loginList) { }, {optimisticData, successData, failureData}, ); - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS); + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route); } /** @@ -328,7 +328,7 @@ function addNewContactMethodAndNavigate(contactMethod) { ]; API.write('AddNewContactMethod', {partnerUserID: contactMethod}, {optimisticData, successData, failureData}); - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS); + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route); } /** @@ -755,7 +755,7 @@ function setContactMethodAsDefault(newDefaultContactMethod) { }, ]; API.write('SetContactMethodAsDefault', {partnerUserID: newDefaultContactMethod}, {optimisticData, successData, failureData}); - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS); + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route); } /** diff --git a/src/libs/getButtonState.ts b/src/libs/getButtonState.ts index 6b89e1b7d383..fe593b9f613e 100644 --- a/src/libs/getButtonState.ts +++ b/src/libs/getButtonState.ts @@ -1,12 +1,10 @@ import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; -type GetButtonState = (isActive: boolean, isPressed: boolean, isComplete: boolean, isDisabled: boolean, isInteractive: boolean) => ValueOf; - /** * Get the string representation of a button's state. */ -const getButtonState: GetButtonState = (isActive = false, isPressed = false, isComplete = false, isDisabled = false, isInteractive = true) => { +function getButtonState(isActive = false, isPressed = false, isComplete = false, isDisabled = false, isInteractive = true): ValueOf { if (!isInteractive) { return CONST.BUTTON_STATES.DEFAULT; } @@ -28,6 +26,6 @@ const getButtonState: GetButtonState = (isActive = false, isPressed = false, isC } return CONST.BUTTON_STATES.DEFAULT; -}; +} export default getButtonState; diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 302b7d35a1c9..c958189d68b5 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -184,7 +184,7 @@ function EditRequestPage({betas, report, route, parentReport, policyCategories, }); }} onNavigateToCurrency={() => { - const activeRoute = encodeURIComponent(Navigation.getActiveRoute().replace(/\?.*/, '')); + const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); Navigation.navigate(ROUTES.EDIT_CURRENCY_REQUEST.getRoute(report.reportID, defaultCurrency, activeRoute)); }} /> diff --git a/src/pages/EditSplitBillPage.js b/src/pages/EditSplitBillPage.js index 1342d9297d3e..c4e47e2d4c35 100644 --- a/src/pages/EditSplitBillPage.js +++ b/src/pages/EditSplitBillPage.js @@ -112,7 +112,7 @@ function EditSplitBillPage({route, transaction, draftTransaction}) { }); }} onNavigateToCurrency={() => { - const activeRoute = encodeURIComponent(Navigation.getActiveRoute().replace(/\?.*/, '')); + const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); Navigation.navigate(ROUTES.EDIT_SPLIT_BILL_CURRENCY.getRoute(reportID, reportActionID, defaultCurrency, activeRoute)); }} /> diff --git a/src/pages/TeachersUnite/ImTeacherUpdateEmailPage.js b/src/pages/TeachersUnite/ImTeacherUpdateEmailPage.js index 5d7c1d960e3a..38065ac8ab8e 100644 --- a/src/pages/TeachersUnite/ImTeacherUpdateEmailPage.js +++ b/src/pages/TeachersUnite/ImTeacherUpdateEmailPage.js @@ -17,6 +17,7 @@ const defaultProps = {}; function ImTeacherUpdateEmailPage() { const {translate} = useLocalize(); + const activeRoute = Navigation.getActiveRouteWithoutParams(); return ( @@ -31,7 +32,7 @@ function ImTeacherUpdateEmailPage() { title={translate('teachersUnitePage.updateYourEmail')} subtitle={translate('teachersUnitePage.schoolMailAsDefault')} linkKey="teachersUnitePage.contactMethods" - onLinkPress={() => Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS)} + onLinkPress={() => Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(activeRoute))} iconWidth={variables.signInLogoWidthLargeScreen} iconHeight={variables.lhnLogoWidth} /> @@ -40,7 +41,7 @@ function ImTeacherUpdateEmailPage() { success accessibilityLabel={translate('teachersUnitePage.updateEmail')} text={translate('teachersUnitePage.updateEmail')} - onPress={() => Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS)} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(activeRoute))} /> diff --git a/src/pages/iou/steps/NewRequestAmountPage.js b/src/pages/iou/steps/NewRequestAmountPage.js index e531e6706f55..a045fc6399e9 100644 --- a/src/pages/iou/steps/NewRequestAmountPage.js +++ b/src/pages/iou/steps/NewRequestAmountPage.js @@ -123,7 +123,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { } // Remove query from the route and encode it. - const activeRoute = encodeURIComponent(Navigation.getActiveRoute().replace(/\?.*/, '')); + const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); Navigation.navigate(ROUTES.MONEY_REQUEST_CURRENCY.getRoute(iouType, reportID, currency, activeRoute)); }; diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js index c48191957999..b97bc2521e55 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -123,7 +123,7 @@ class ContactMethodDetailsPage extends Component { // Navigate to methods page on successful magic code verification // validatedDate property is responsible to decide the status of the magic code verification if (!prevValidatedDate && validatedDate) { - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS); + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route); } } @@ -236,8 +236,8 @@ class ContactMethodDetailsPage extends Component { Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS)} - onLinkPress={() => Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS)} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)} + onLinkPress={() => Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)} /> ); @@ -255,7 +255,7 @@ class ContactMethodDetailsPage extends Component { > Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS)} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)} /> Navigation.goBack(ROUTES.SETTINGS_PROFILE)} + onBackButtonPress={() => Navigation.goBack(navigateBackTo)} /> diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js index c72f562e8c0a..ae301a9f3c33 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js @@ -103,7 +103,7 @@ function NewContactMethodPage(props) { > Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS)} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)} /> policy.outputCurrency === CONST.CURRENCY.USD - ? singleExecution(waitForNavigate(() => ReimbursementAccount.navigateToBankAccountRoute(policy.id, Navigation.getActiveRoute().replace(/\?.*/, ''))))() + ? singleExecution(waitForNavigate(() => ReimbursementAccount.navigateToBankAccountRoute(policy.id, Navigation.getActiveRouteWithoutParams())))() : setIsCurrencyModalOpen(true), brickRoadIndicator: !_.isEmpty(props.reimbursementAccount.errors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }, diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index 395013b398c9..74ea0ed06c02 100644 --- a/src/styles/StyleUtils.ts +++ b/src/styles/StyleUtils.ts @@ -1,5 +1,5 @@ import {CSSProperties} from 'react'; -import {Animated, DimensionValue, ImageStyle, PressableStateCallbackType, TextStyle, ViewStyle} from 'react-native'; +import {Animated, DimensionValue, ImageStyle, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native'; import {EdgeInsets} from 'react-native-safe-area-context'; import {ValueOf} from 'type-fest'; import * as Browser from '@libs/Browser'; @@ -16,7 +16,7 @@ import spacing from './utilities/spacing'; import variables from './variables'; type AllStyles = ViewStyle | TextStyle | ImageStyle; -type ParsableStyle = AllStyles | ((state: PressableStateCallbackType) => AllStyles); +type ParsableStyle = StyleProp | ((state: PressableStateCallbackType) => StyleProp); type ColorValue = ValueOf; type AvatarSizeName = ValueOf; @@ -749,9 +749,8 @@ function parseStyleAsArray(styleParam: T | T[]): T[] { /** * Parse style function and return Styles object */ -function parseStyleFromFunction(style: ParsableStyle, state: PressableStateCallbackType): AllStyles[] { - const functionAppliedStyle = typeof style === 'function' ? style(state) : style; - return parseStyleAsArray(functionAppliedStyle); +function parseStyleFromFunction(style: ParsableStyle, state: PressableStateCallbackType): StyleProp { + return typeof style === 'function' ? style(state) : style; } /** diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index a816fc77625b..ec857af2eceb 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -35,7 +35,7 @@ declare module 'react-native' { 'aria-haspopup'?: 'dialog' | 'grid' | 'listbox' | 'menu' | 'tree' | false; 'aria-hidden'?: boolean; 'aria-invalid'?: boolean; - 'aria-keyshortcuts'?: string[]; + 'aria-keyshortcuts'?: string; 'aria-label'?: string; 'aria-labelledby'?: idRef; 'aria-level'?: number; @@ -85,7 +85,7 @@ declare module 'react-native' { accessibilityInvalid?: boolean; accessibilityKeyShortcuts?: string[]; accessibilityLabel?: string; - accessibilityLabelledBy?: idRefList; + accessibilityLabelledBy?: idRef; accessibilityLevel?: number; accessibilityLiveRegion?: 'assertive' | 'none' | 'polite'; accessibilityModal?: boolean; @@ -312,7 +312,10 @@ declare module 'react-native' { readonly hovered: boolean; readonly pressed: boolean; } - interface PressableStateCallbackType extends WebPressableStateCallbackType {} + interface PressableStateCallbackType extends WebPressableStateCallbackType { + readonly isScreenReaderActive: boolean; + readonly isDisabled: boolean; + } // Extracted from react-native-web, packages/react-native-web/src/exports/Pressable/index.js interface WebPressableProps extends WebSharedProps { diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 19908273ad3d..66622f4b29ea 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -43,6 +43,9 @@ type Message = { moderationDecision?: Decision; translationKey?: string; + + /** ID of a task report */ + taskReportID?: string; }; type Person = {