diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index fe58f3c0a6d8..6717c1736f65 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -24,8 +24,7 @@ const custom = require('../config/webpack/webpack.common')({ module.exports = ({config}) => { config.resolve.alias = { 'react-native-config': 'react-web-config', - 'react-native$': '@expensify/react-native-web', - 'react-native-web': '@expensify/react-native-web', + 'react-native$': 'react-native-web', '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.js'), '@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'), diff --git a/android/app/build.gradle b/android/app/build.gradle index c7974572e665..50de8145e67c 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 1001039603 + versionName "1.3.96-3" } 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..d8ad66917b82 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/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index d12f602260e1..b66b4f67a3b6 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -13,7 +13,7 @@ const includeModules = [ 'react-native-animatable', 'react-native-reanimated', 'react-native-picker-select', - '@expensify/react-native-web', + 'react-native-web', 'react-native-webview', '@react-native-picker', 'react-native-modal', @@ -185,8 +185,7 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ resolve: { alias: { 'react-native-config': 'react-web-config', - 'react-native$': '@expensify/react-native-web', - 'react-native-web': '@expensify/react-native-web', + 'react-native$': 'react-native-web', // Module alias for web & desktop // https://webpack.js.org/configuration/resolve/#resolvealias diff --git a/contributingGuides/FORMS.md b/contributingGuides/FORMS.md index 1fb67483daca..ffec5f20254c 100644 --- a/contributingGuides/FORMS.md +++ b/contributingGuides/FORMS.md @@ -53,17 +53,27 @@ The phone number can be formatted in different ways. ### Native Keyboards -We should always set people up for success on native platforms by enabling the best keyboard for the type of input we’re asking them to provide. See [keyboardType](https://reactnative.dev/docs/0.64/textinput#keyboardtype) in the React Native documentation. +We should always set people up for success on native platforms by enabling the best keyboard for the type of input we’re asking them to provide. See [inputMode](https://reactnative.dev/docs/textinput#inputmode) in the React Native documentation. -We have a couple of keyboard types [defined](https://github.com/Expensify/App/blob/572caa9e7cf32a2d64fe0e93d171bb05a1dfb217/src/CONST.js#L357-L360) and should be used like so: +We have a list of input modes [defined](https://github.com/Expensify/App/blob/9418b870515102631ea2156b5ea253ee05a98ff1/src/CONST.js#L765-L774) and should be used like so: ```jsx ``` +We also have [keyboardType](https://github.com/Expensify/App/blob/9418b870515102631ea2156b5ea253ee05a98ff1/src/CONST.js#L760-L763) and should be used for specific use cases when there is no `inputMode` equivalent of the value exist. and should be used like so: + +```jsx + +``` + + ### Autofill Behavior Forms should autofill information whenever possible i.e. they should work with browsers and password managers auto complete features. diff --git a/docs/assets/images/ExpensifyHelp_RemovingMembers.png b/docs/assets/images/ExpensifyHelp_RemovingMembers.png new file mode 100644 index 000000000000..1e0157476fbf Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_RemovingMembers.png differ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 94d2986fd111..853bed5517f8 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.96.0 + 1.3.96.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 9478336965cf..2f5543ca303e 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.3.96.0 + 1.3.96.3 diff --git a/package-lock.json b/package-lock.json index b7c9f13085c4..32f874bfc3a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,16 @@ { "name": "new.expensify", - "version": "1.3.96-0", + "version": "1.3.96-3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.96-0", + "version": "1.3.96-3", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-web": "0.18.15", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", "@formatjs/intl-listformat": "^7.2.2", @@ -114,6 +113,7 @@ "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "^3.6.0", "react-native-vision-camera": "^2.16.2", + "react-native-web": "^0.19.9", "react-native-web-linear-gradient": "^1.1.2", "react-native-webview": "^11.17.2", "react-pdf": "^6.2.2", @@ -3684,25 +3684,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@expensify/react-native-web": { - "version": "0.18.15", - "resolved": "https://registry.npmjs.org/@expensify/react-native-web/-/react-native-web-0.18.15.tgz", - "integrity": "sha512-xE3WdGKY4SRLfIrimUlgP78ZsDaWy3g+KIO8mpxTm9zCXeX/sgEYs6QvhFghgEhhp7Y1bLH9LWTKiZy9LZM8EA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.6", - "create-react-class": "^15.7.0", - "fbjs": "^3.0.4", - "inline-style-prefixer": "^6.0.1", - "normalize-css-color": "^1.0.2", - "postcss-value-parser": "^4.2.0", - "styleq": "^0.1.2" - }, - "peerDependencies": { - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0" - } - }, "node_modules/@expo/config-plugins": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-4.1.5.tgz", @@ -26262,16 +26243,6 @@ "sha.js": "^2.4.8" } }, - "node_modules/create-react-class": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz", - "integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - }, "node_modules/cross-fetch": { "version": "3.1.5", "license": "MIT", @@ -41728,12 +41699,6 @@ "url": "https://github.com/sponsors/antelle" } }, - "node_modules/normalize-css-color": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/normalize-css-color/-/normalize-css-color-1.0.2.tgz", - "integrity": "sha512-jPJ/V7Cp1UytdidsPqviKEElFQJs22hUUgK5BOPHTwOonNCk7/2qOxhhqzEajmFrWJowADFfOFh1V+aWkRfy+w==", - "license": "BSD-3-Clause" - }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -45106,7 +45071,6 @@ "version": "0.19.9", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.9.tgz", "integrity": "sha512-m69arZbS6FV+BNSKE6R/NQwUX+CzxCkYM7AJlSLlS8dz3BDzlaxG8Bzqtzv/r3r1YFowhnZLBXVKIwovKDw49g==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-color": "^2.1.0", @@ -45133,8 +45097,7 @@ "node_modules/react-native-web/node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", - "peer": true + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" }, "node_modules/react-native-webview": { "version": "11.23.0", @@ -55659,20 +55622,6 @@ } } }, - "@expensify/react-native-web": { - "version": "0.18.15", - "resolved": "https://registry.npmjs.org/@expensify/react-native-web/-/react-native-web-0.18.15.tgz", - "integrity": "sha512-xE3WdGKY4SRLfIrimUlgP78ZsDaWy3g+KIO8mpxTm9zCXeX/sgEYs6QvhFghgEhhp7Y1bLH9LWTKiZy9LZM8EA==", - "requires": { - "@babel/runtime": "^7.18.6", - "create-react-class": "^15.7.0", - "fbjs": "^3.0.4", - "inline-style-prefixer": "^6.0.1", - "normalize-css-color": "^1.0.2", - "postcss-value-parser": "^4.2.0", - "styleq": "^0.1.2" - } - }, "@expo/config-plugins": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-4.1.5.tgz", @@ -72107,15 +72056,6 @@ "sha.js": "^2.4.8" } }, - "create-react-class": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz", - "integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==", - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - }, "cross-fetch": { "version": "3.1.5", "requires": { @@ -83165,11 +83105,6 @@ "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==" }, - "normalize-css-color": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/normalize-css-color/-/normalize-css-color-1.0.2.tgz", - "integrity": "sha512-jPJ/V7Cp1UytdidsPqviKEElFQJs22hUUgK5BOPHTwOonNCk7/2qOxhhqzEajmFrWJowADFfOFh1V+aWkRfy+w==" - }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -85603,7 +85538,6 @@ "version": "0.19.9", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.9.tgz", "integrity": "sha512-m69arZbS6FV+BNSKE6R/NQwUX+CzxCkYM7AJlSLlS8dz3BDzlaxG8Bzqtzv/r3r1YFowhnZLBXVKIwovKDw49g==", - "peer": true, "requires": { "@babel/runtime": "^7.18.6", "@react-native/normalize-color": "^2.1.0", @@ -85618,8 +85552,7 @@ "memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", - "peer": true + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" } } }, diff --git a/package.json b/package.json index 229545f34252..ab38d2bb65b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.96-0", + "version": "1.3.96-3", "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.", @@ -60,7 +60,6 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-web": "0.18.15", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", "@formatjs/intl-listformat": "^7.2.2", @@ -163,6 +162,7 @@ "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "^3.6.0", "react-native-vision-camera": "^2.16.2", + "react-native-web": "^0.19.9", "react-native-web-linear-gradient": "^1.1.2", "react-native-webview": "^11.17.2", "react-pdf": "^6.2.2", diff --git a/patches/eslint-plugin-react-native-a11y+3.3.0.patch b/patches/eslint-plugin-react-native-a11y+3.3.0.patch new file mode 100644 index 000000000000..fe5998118afa --- /dev/null +++ b/patches/eslint-plugin-react-native-a11y+3.3.0.patch @@ -0,0 +1,59 @@ +diff --git a/node_modules/eslint-plugin-react-native-a11y/__tests__/src/rules/has-valid-accessibility-descriptors-test.js b/node_modules/eslint-plugin-react-native-a11y/__tests__/src/rules/has-valid-accessibility-descriptors-test.js +index 9ecf8b1..fef94dd 100644 +--- a/node_modules/eslint-plugin-react-native-a11y/__tests__/src/rules/has-valid-accessibility-descriptors-test.js ++++ b/node_modules/eslint-plugin-react-native-a11y/__tests__/src/rules/has-valid-accessibility-descriptors-test.js +@@ -20,7 +20,7 @@ const ruleTester = new RuleTester(); + + const expectedError = { + message: +- 'Missing a11y props. Expected one of: accessibilityRole OR BOTH accessibilityLabel + accessibilityHint OR BOTH accessibilityActions + onAccessibilityAction', ++ 'Missing a11y props. Expected one of: accessibilityRole OR role OR BOTH accessibilityLabel + accessibilityHint OR BOTH accessibilityActions + onAccessibilityAction', + type: 'JSXOpeningElement', + }; + +@@ -29,6 +29,11 @@ ruleTester.run('has-valid-accessibility-descriptors', rule, { + { + code: ';', + }, ++ { ++ code: ` ++ Back ++ `, ++ }, + { + code: ` + Back +diff --git a/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-descriptors.js b/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-descriptors.js +index 99deb91..555ebd9 100644 +--- a/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-descriptors.js ++++ b/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-descriptors.js +@@ -16,7 +16,7 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de + // ---------------------------------------------------------------------------- + // Rule Definition + // ---------------------------------------------------------------------------- +-var errorMessage = 'Missing a11y props. Expected one of: accessibilityRole OR BOTH accessibilityLabel + accessibilityHint OR BOTH accessibilityActions + onAccessibilityAction'; ++var errorMessage = 'Missing a11y props. Expected one of: accessibilityRole OR role OR BOTH accessibilityLabel + accessibilityHint OR BOTH accessibilityActions + onAccessibilityAction'; + var schema = (0, _schemas.generateObjSchema)(); + + var hasSpreadProps = function hasSpreadProps(attributes) { +@@ -35,7 +35,7 @@ module.exports = { + return { + JSXOpeningElement: function JSXOpeningElement(node) { + if ((0, _isTouchable.default)(node, context) || (0, _jsxAstUtils.elementType)(node) === 'TextInput') { +- if (!(0, _jsxAstUtils.hasAnyProp)(node.attributes, ['accessibilityRole', 'accessibilityLabel', 'accessibilityActions', 'accessible']) && !hasSpreadProps(node.attributes)) { ++ if (!(0, _jsxAstUtils.hasAnyProp)(node.attributes, ['role', 'accessibilityRole', 'accessibilityLabel', 'accessibilityActions', 'accessible']) && !hasSpreadProps(node.attributes)) { + context.report({ + node, + message: errorMessage, +diff --git a/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-role.js b/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-role.js +index fe74702..fa6bdaa 100644 +--- a/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-role.js ++++ b/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-role.js +@@ -13,5 +13,5 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de + // Rule Definition + // ---------------------------------------------------------------------------- + var errorMessage = 'accessibilityRole must be one of defined values'; +-var validValues = ['togglebutton', 'adjustable', 'alert', 'button', 'checkbox', 'combobox', 'header', 'image', 'imagebutton', 'keyboardkey', 'link', 'menu', 'menubar', 'menuitem', 'none', 'progressbar', 'radio', 'radiogroup', 'scrollbar', 'search', 'spinbutton', 'summary', 'switch', 'tab', 'tabbar', 'tablist', 'text', 'timer', 'list', 'toolbar']; ++var validValues = ['img', 'img button', 'img link', 'togglebutton', 'adjustable', 'alert', 'button', 'checkbox', 'combobox', 'header', 'image', 'imagebutton', 'keyboardkey', 'link', 'menu', 'menubar', 'menuitem', 'none', 'progressbar', 'radio', 'radiogroup', 'scrollbar', 'search', 'spinbutton', 'summary', 'switch', 'tab', 'tabbar', 'tablist', 'text', 'timer', 'list', 'toolbar']; + module.exports = (0, _validProp.default)('accessibilityRole', validValues, errorMessage); +\ No newline at end of file diff --git a/patches/react-native-modal+13.0.1.patch b/patches/react-native-modal+13.0.1.patch index 049a7a09d16a..5bfb2cc5f0b0 100644 --- a/patches/react-native-modal+13.0.1.patch +++ b/patches/react-native-modal+13.0.1.patch @@ -11,7 +11,7 @@ index b63bcfc..bd6419e 100644 buildPanResponder: () => void; getAccDistancePerDirection: (gestureState: PanResponderGestureState) => number; diff --git a/node_modules/react-native-modal/dist/modal.js b/node_modules/react-native-modal/dist/modal.js -index 80f4e75..a88a2ca 100644 +index 80f4e75..3ba8b8c 100644 --- a/node_modules/react-native-modal/dist/modal.js +++ b/node_modules/react-native-modal/dist/modal.js @@ -75,6 +75,13 @@ export class ReactNativeModal extends React.Component { @@ -28,23 +28,27 @@ index 80f4e75..a88a2ca 100644 this.shouldPropagateSwipe = (evt, gestureState) => { return typeof this.props.propagateSwipe === 'function' ? this.props.propagateSwipe(evt, gestureState) -@@ -454,9 +461,15 @@ export class ReactNativeModal extends React.Component { +@@ -453,10 +460,18 @@ export class ReactNativeModal extends React.Component { + if (this.state.isVisible) { this.open(); } - BackHandler.addEventListener('hardwareBackPress', this.onBackButtonPress); + if (Platform.OS === 'web') { + document?.body?.addEventListener?.('keyup', this.handleEscape, true); ++ return; + } + BackHandler.addEventListener('hardwareBackPress', this.onBackButtonPress); } componentWillUnmount() { - BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPress); +- BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPress); + if (Platform.OS === 'web') { + document?.body?.removeEventListener?.('keyup', this.handleEscape, true); ++ } else { ++ BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPress); + } if (this.didUpdateDimensionsEmitter) { this.didUpdateDimensionsEmitter.remove(); } -@@ -525,7 +538,7 @@ export class ReactNativeModal extends React.Component { +@@ -525,7 +540,7 @@ export class ReactNativeModal extends React.Component { } return (React.createElement(Modal, Object.assign({ transparent: true, animationType: 'none', visible: this.state.isVisible, onRequestClose: onBackButtonPress }, otherProps), this.makeBackdrop(), diff --git a/patches/react-native-web+0.19.9+001+initial.patch b/patches/react-native-web+0.19.9+001+initial.patch new file mode 100644 index 000000000000..d88ef83d4bcd --- /dev/null +++ b/patches/react-native-web+0.19.9+001+initial.patch @@ -0,0 +1,286 @@ +diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +index c879838..288316c 100644 +--- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +@@ -117,6 +117,14 @@ function findLastWhere(arr, predicate) { + * + */ + class VirtualizedList extends StateSafePureComponent { ++ pushOrUnshift(input, item) { ++ if (this.props.inverted) { ++ input.unshift(item); ++ } else { ++ input.push(item); ++ } ++ } ++ + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params) { + var animated = params ? params.animated : true; +@@ -350,6 +358,7 @@ class VirtualizedList extends StateSafePureComponent { + }; + this._defaultRenderScrollComponent = props => { + var onRefresh = props.onRefresh; ++ var inversionStyle = this.props.inverted ? this.props.horizontal ? styles.rowReverse : styles.columnReverse : null; + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return /*#__PURE__*/React.createElement(View, props); +@@ -367,13 +376,16 @@ class VirtualizedList extends StateSafePureComponent { + refreshing: props.refreshing, + onRefresh: onRefresh, + progressViewOffset: props.progressViewOffset +- }) : props.refreshControl ++ }) : props.refreshControl, ++ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] + })) + ); + } else { + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] +- return /*#__PURE__*/React.createElement(ScrollView, props); ++ return /*#__PURE__*/React.createElement(ScrollView, _extends({}, props, { ++ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] ++ })); + } + }; + this._onCellLayout = (e, cellKey, index) => { +@@ -683,7 +695,7 @@ class VirtualizedList extends StateSafePureComponent { + onViewableItemsChanged = _this$props3.onViewableItemsChanged, + viewabilityConfig = _this$props3.viewabilityConfig; + if (onViewableItemsChanged) { +- this._viewabilityTuples.push({ ++ this.pushOrUnshift(this._viewabilityTuples, { + viewabilityHelper: new ViewabilityHelper(viewabilityConfig), + onViewableItemsChanged: onViewableItemsChanged + }); +@@ -937,10 +949,10 @@ class VirtualizedList extends StateSafePureComponent { + var key = _this._keyExtractor(item, ii, _this.props); + _this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { +- stickyHeaderIndices.push(cells.length); ++ _this.pushOrUnshift(stickyHeaderIndices, cells.length); + } + var shouldListenForLayout = getItemLayout == null || debug || _this._fillRateHelper.enabled(); +- cells.push( /*#__PURE__*/React.createElement(CellRenderer, _extends({ ++ _this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(CellRenderer, _extends({ + CellRendererComponent: CellRendererComponent, + ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : undefined, + ListItemComponent: ListItemComponent, +@@ -1012,14 +1024,14 @@ class VirtualizedList extends StateSafePureComponent { + // 1. Add cell for ListHeaderComponent + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { +- stickyHeaderIndices.push(0); ++ this.pushOrUnshift(stickyHeaderIndices, 0); + } + var _element = /*#__PURE__*/React.isValidElement(ListHeaderComponent) ? ListHeaderComponent : + /*#__PURE__*/ + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListHeaderComponent, null); +- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getCellKey() + '-header', + key: "$header" + }, /*#__PURE__*/React.createElement(View, { +@@ -1038,7 +1050,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListEmptyComponent, null); +- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getCellKey() + '-empty', + key: "$empty" + }, /*#__PURE__*/React.cloneElement(_element2, { +@@ -1077,7 +1089,7 @@ class VirtualizedList extends StateSafePureComponent { + var firstMetrics = this.__getFrameMetricsApprox(section.first, this.props); + var lastMetrics = this.__getFrameMetricsApprox(last, this.props); + var spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset; +- cells.push( /*#__PURE__*/React.createElement(View, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(View, { + key: "$spacer-" + section.first, + style: { + [spacerKey]: spacerSize +@@ -1100,7 +1112,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListFooterComponent, null); +- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getFooterCellKey(), + key: "$footer" + }, /*#__PURE__*/React.createElement(View, { +@@ -1266,7 +1278,7 @@ class VirtualizedList extends StateSafePureComponent { + * suppresses an error found when Flow v0.68 was deployed. To see the + * error delete this comment and run Flow. */ + if (frame.inLayout) { +- framesInLayout.push(frame); ++ this.pushOrUnshift(framesInLayout, frame); + } + } + var windowTop = this.__getFrameMetricsApprox(this.state.cellsAroundViewport.first, this.props).offset; +@@ -1452,6 +1464,12 @@ var styles = StyleSheet.create({ + left: 0, + borderColor: 'red', + borderWidth: 2 ++ }, ++ rowReverse: { ++ flexDirection: 'row-reverse' ++ }, ++ columnReverse: { ++ flexDirection: 'column-reverse' + } + }); + export default VirtualizedList; +\ No newline at end of file +diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +index c7d68bb..46b3fc9 100644 +--- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +@@ -167,6 +167,14 @@ function findLastWhere( + class VirtualizedList extends StateSafePureComponent { + static contextType: typeof VirtualizedListContext = VirtualizedListContext; + ++ pushOrUnshift(input: Array, item: Item) { ++ if (this.props.inverted) { ++ input.unshift(item) ++ } else { ++ input.push(item) ++ } ++ } ++ + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params?: ?{animated?: ?boolean, ...}) { + const animated = params ? params.animated : true; +@@ -438,7 +446,7 @@ class VirtualizedList extends StateSafePureComponent { + } else { + const {onViewableItemsChanged, viewabilityConfig} = this.props; + if (onViewableItemsChanged) { +- this._viewabilityTuples.push({ ++ this.pushOrUnshift(this._viewabilityTuples, { + viewabilityHelper: new ViewabilityHelper(viewabilityConfig), + onViewableItemsChanged: onViewableItemsChanged, + }); +@@ -814,13 +822,13 @@ class VirtualizedList extends StateSafePureComponent { + + this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { +- stickyHeaderIndices.push(cells.length); ++ this.pushOrUnshift(stickyHeaderIndices, (cells.length)); + } + + const shouldListenForLayout = + getItemLayout == null || debug || this._fillRateHelper.enabled(); + +- cells.push( ++ this.pushOrUnshift(cells, + { + // 1. Add cell for ListHeaderComponent + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { +- stickyHeaderIndices.push(0); ++ this.pushOrUnshift(stickyHeaderIndices, 0); + } + const element = React.isValidElement(ListHeaderComponent) ? ( + ListHeaderComponent +@@ -932,7 +940,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[incompatible-type-arg] + + ); +- cells.push( ++ this.pushOrUnshift(cells, + +@@ -963,7 +971,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[incompatible-type-arg] + + )): any); +- cells.push( ++ this.pushOrUnshift(cells, + +@@ -1017,7 +1025,7 @@ class VirtualizedList extends StateSafePureComponent { + const lastMetrics = this.__getFrameMetricsApprox(last, this.props); + const spacerSize = + lastMetrics.offset + lastMetrics.length - firstMetrics.offset; +- cells.push( ++ this.pushOrUnshift(cells, + { + // $FlowFixMe[incompatible-type-arg] + + ); +- cells.push( ++ this.pushOrUnshift(cells, + +@@ -1246,6 +1254,12 @@ class VirtualizedList extends StateSafePureComponent { + * LTI update could not be added via codemod */ + _defaultRenderScrollComponent = props => { + const onRefresh = props.onRefresh; ++ const inversionStyle = this.props.inverted ++ ? this.props.horizontal ++ ? styles.rowReverse ++ : styles.columnReverse ++ : null; ++ + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return ; +@@ -1273,12 +1287,24 @@ class VirtualizedList extends StateSafePureComponent { + props.refreshControl + ) + } ++ contentContainerStyle={[ ++ inversionStyle, ++ this.props.contentContainerStyle, ++ ]} + /> + ); + } else { + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] +- return ; ++ return ( ++ ++ ); + } + }; + +@@ -1432,7 +1458,7 @@ class VirtualizedList extends StateSafePureComponent { + * suppresses an error found when Flow v0.68 was deployed. To see the + * error delete this comment and run Flow. */ + if (frame.inLayout) { +- framesInLayout.push(frame); ++ this.pushOrUnshift(framesInLayout, frame); + } + } + const windowTop = this.__getFrameMetricsApprox( +@@ -2044,6 +2070,12 @@ const styles = StyleSheet.create({ + borderColor: 'red', + borderWidth: 2, + }, ++ rowReverse: { ++ flexDirection: 'row-reverse', ++ }, ++ columnReverse: { ++ flexDirection: 'column-reverse', ++ }, + }); + + export default VirtualizedList; +\ No newline at end of file diff --git a/patches/react-native-web+0.19.9+002+fix-mvcp.patch b/patches/react-native-web+0.19.9+002+fix-mvcp.patch new file mode 100644 index 000000000000..afd681bba3b0 --- /dev/null +++ b/patches/react-native-web+0.19.9+002+fix-mvcp.patch @@ -0,0 +1,687 @@ +diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +index a6fe142..faeb323 100644 +--- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +@@ -293,7 +293,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[missing-local-annot] + + constructor(_props) { +- var _this$props$updateCel; ++ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; + super(_props); + this._getScrollMetrics = () => { + return this._scrollMetrics; +@@ -532,6 +532,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -581,7 +586,7 @@ class VirtualizedList extends StateSafePureComponent { + this._updateCellsToRender = () => { + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + this.setState((state, props) => { +- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); ++ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); + var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); + if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { + return null; +@@ -601,7 +606,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable + }; + }; +@@ -633,12 +638,10 @@ class VirtualizedList extends StateSafePureComponent { + }; + this._getFrameMetrics = (index, props) => { + var data = props.data, +- getItem = props.getItem, + getItemCount = props.getItemCount, + getItemLayout = props.getItemLayout; + invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); +- var item = getItem(data, index); +- var frame = this._frames[this._keyExtractor(item, index, props)]; ++ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -662,7 +665,7 @@ class VirtualizedList extends StateSafePureComponent { + + // The last cell we rendered may be at a new index. Bail if we don't know + // where it is. +- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { ++ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { + return []; + } + var first = focusedCellIndex; +@@ -702,9 +705,15 @@ class VirtualizedList extends StateSafePureComponent { + } + } + var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); ++ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; + this.state = { + cellsAroundViewport: initialRenderRegion, +- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) ++ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), ++ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -715,7 +724,7 @@ class VirtualizedList extends StateSafePureComponent { + var clientLength = this.props.horizontal ? ev.target.clientWidth : ev.target.clientHeight; + var isEventTargetScrollable = scrollLength > clientLength; + var delta = this.props.horizontal ? ev.deltaX || ev.wheelDeltaX : ev.deltaY || ev.wheelDeltaY; +- var leftoverDelta = delta; ++ var leftoverDelta = delta * 0.5; + if (isEventTargetScrollable) { + leftoverDelta = delta < 0 ? Math.min(delta + scrollOffset, 0) : Math.max(delta - (scrollLength - clientLength - scrollOffset), 0); + } +@@ -760,6 +769,26 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } ++ static _findItemIndexWithKey(props, key, hint) { ++ var itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ var curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } ++ } ++ for (var ii = 0; ii < itemCount; ii++) { ++ var _curKey = VirtualizedList._getItemKey(props, ii); ++ if (_curKey === key) { ++ return ii; ++ } ++ } ++ return null; ++ } ++ static _getItemKey(props, index) { ++ var item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); ++ } + static _createRenderMask(props, cellsAroundViewport, additionalRegions) { + var itemCount = props.getItemCount(props.data); + invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); +@@ -808,7 +837,7 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } +- _adjustCellsAroundViewport(props, cellsAroundViewport) { ++ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { + var data = props.data, + getItemCount = props.getItemCount; + var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); +@@ -831,17 +860,9 @@ class VirtualizedList extends StateSafePureComponent { + last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) + }; + } else { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; + } + newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); +@@ -914,16 +935,36 @@ class VirtualizedList extends StateSafePureComponent { + } + } + static getDerivedStateFromProps(newProps, prevState) { ++ var _newProps$maintainVis, _newProps$maintainVis2; + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + var itemCount = newProps.getItemCount(newProps.data); + if (itemCount === prevState.renderMask.numCells()) { + return prevState; + } +- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); ++ var maintainVisibleContentPositionAdjustment = null; ++ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; ++ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; ++ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); ++ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { ++ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, ++ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment ++ } : prevState.cellsAroundViewport, newProps); + return { + cellsAroundViewport: constrainedCells, +- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) ++ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount + }; + } + _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { +@@ -946,7 +987,7 @@ class VirtualizedList extends StateSafePureComponent { + last = Math.min(end, last); + var _loop = function _loop() { + var item = getItem(data, ii); +- var key = _this._keyExtractor(item, ii, _this.props); ++ var key = VirtualizedList._keyExtractor(item, ii, _this.props); + _this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { + _this.pushOrUnshift(stickyHeaderIndices, cells.length); +@@ -981,20 +1022,23 @@ class VirtualizedList extends StateSafePureComponent { + } + static _constrainToItemCount(cells, props) { + var itemCount = props.getItemCount(props.data); +- var last = Math.min(itemCount - 1, cells.last); ++ var lastPossibleCellIndex = itemCount - 1; ++ ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); ++ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); + return { +- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), +- last ++ first: clamp(0, cells.first, maxFirst), ++ last: Math.min(lastPossibleCellIndex, cells.last) + }; + } + _isNestedWithSameOrientation() { + var nestedContext = this.context; + return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); + } +- _keyExtractor(item, index, props +- // $FlowFixMe[missing-local-annot] +- ) { ++ static _keyExtractor(item, index, props) { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -1034,7 +1078,12 @@ class VirtualizedList extends StateSafePureComponent { + this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getCellKey() + '-header', + key: "$header" +- }, /*#__PURE__*/React.createElement(View, { ++ }, /*#__PURE__*/React.createElement(View ++ // We expect that header component will be a single native view so make it ++ // not collapsable to avoid this view being flattened and make this assumption ++ // no longer true. ++ , { ++ collapsable: false, + onLayout: this._onLayoutHeader, + style: [inversionStyle, this.props.ListHeaderComponentStyle] + }, +@@ -1136,7 +1185,11 @@ class VirtualizedList extends StateSafePureComponent { + // TODO: Android support + invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, + stickyHeaderIndices, +- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style ++ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, ++ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) ++ }) : undefined + }); + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; + var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { +@@ -1319,8 +1372,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReached = _this$props8.onStartReached, + onStartReachedThreshold = _this$props8.onStartReachedThreshold, + onEndReached = _this$props8.onEndReached, +- onEndReachedThreshold = _this$props8.onEndReachedThreshold, +- initialScrollIndex = _this$props8.initialScrollIndex; ++ onEndReachedThreshold = _this$props8.onEndReachedThreshold; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + var _this$_scrollMetrics2 = this._scrollMetrics, + contentLength = _this$_scrollMetrics2.contentLength, + visibleLength = _this$_scrollMetrics2.visibleLength, +@@ -1360,16 +1417,10 @@ class VirtualizedList extends StateSafePureComponent { + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed + else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({ +- distanceFromStart +- }); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({ ++ distanceFromStart ++ }); + } + + // If the user scrolls away from the start or end and back again, +@@ -1424,6 +1475,11 @@ class VirtualizedList extends StateSafePureComponent { + } + } + _updateViewableItems(props, cellsAroundViewport) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); + }); +diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +index d896fb1..f303b31 100644 +--- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { + type State = { + renderMask: CellRenderMask, + cellsAroundViewport: {first: number, last: number}, ++ // Used to track items added at the start of the list for maintainVisibleContentPosition. ++ firstVisibleItemKey: ?string, ++ // When > 0 the scroll position available in JS is considered stale and should not be used. ++ pendingScrollUpdateCount: number, + }; + + /** +@@ -455,9 +459,24 @@ class VirtualizedList extends StateSafePureComponent { + + const initialRenderRegion = VirtualizedList._initialRenderRegion(props); + ++ const minIndexForVisible = ++ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ + this.state = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), ++ firstVisibleItemKey: ++ this.props.getItemCount(this.props.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) ++ : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: ++ this.props.initialScrollIndex != null && ++ this.props.initialScrollIndex > 0 ++ ? 1 ++ : 0, + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -470,7 +489,7 @@ class VirtualizedList extends StateSafePureComponent { + const delta = this.props.horizontal + ? ev.deltaX || ev.wheelDeltaX + : ev.deltaY || ev.wheelDeltaY; +- let leftoverDelta = delta; ++ let leftoverDelta = delta * 5; + if (isEventTargetScrollable) { + leftoverDelta = delta < 0 + ? Math.min(delta + scrollOffset, 0) +@@ -542,6 +561,40 @@ class VirtualizedList extends StateSafePureComponent { + } + } + ++ static _findItemIndexWithKey( ++ props: Props, ++ key: string, ++ hint: ?number, ++ ): ?number { ++ const itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ const curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } ++ } ++ for (let ii = 0; ii < itemCount; ii++) { ++ const curKey = VirtualizedList._getItemKey(props, ii); ++ if (curKey === key) { ++ return ii; ++ } ++ } ++ return null; ++ } ++ ++ static _getItemKey( ++ props: { ++ data: Props['data'], ++ getItem: Props['getItem'], ++ keyExtractor: Props['keyExtractor'], ++ ... ++ }, ++ index: number, ++ ): string { ++ const item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); ++ } ++ + static _createRenderMask( + props: Props, + cellsAroundViewport: {first: number, last: number}, +@@ -625,6 +678,7 @@ class VirtualizedList extends StateSafePureComponent { + _adjustCellsAroundViewport( + props: Props, + cellsAroundViewport: {first: number, last: number}, ++ pendingScrollUpdateCount: number, + ): {first: number, last: number} { + const {data, getItemCount} = props; + const onEndReachedThreshold = onEndReachedThresholdOrDefault( +@@ -656,21 +710,9 @@ class VirtualizedList extends StateSafePureComponent { + ), + }; + } else { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if ( +- props.initialScrollIndex && +- !this._scrollMetrics.offset && +- Math.abs(distanceFromEnd) >= Number.EPSILON +- ) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; +@@ -779,14 +821,59 @@ class VirtualizedList extends StateSafePureComponent { + return prevState; + } + ++ let maintainVisibleContentPositionAdjustment: ?number = null; ++ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ const minIndexForVisible = ++ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ const newFirstVisibleItemKey = ++ newProps.getItemCount(newProps.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) ++ : null; ++ if ( ++ newProps.maintainVisibleContentPosition != null && ++ prevFirstVisibleItemKey != null && ++ newFirstVisibleItemKey != null ++ ) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ const hint = ++ itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( ++ newProps, ++ prevFirstVisibleItemKey, ++ hint, ++ ); ++ maintainVisibleContentPositionAdjustment = ++ firstVisibleItemIndex != null ++ ? firstVisibleItemIndex - minIndexForVisible ++ : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ + const constrainedCells = VirtualizedList._constrainToItemCount( +- prevState.cellsAroundViewport, ++ maintainVisibleContentPositionAdjustment != null ++ ? { ++ first: ++ prevState.cellsAroundViewport.first + ++ maintainVisibleContentPositionAdjustment, ++ last: ++ prevState.cellsAroundViewport.last + ++ maintainVisibleContentPositionAdjustment, ++ } ++ : prevState.cellsAroundViewport, + newProps, + ); + + return { + cellsAroundViewport: constrainedCells, + renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: ++ maintainVisibleContentPositionAdjustment != null ++ ? prevState.pendingScrollUpdateCount + 1 ++ : prevState.pendingScrollUpdateCount, + }; + } + +@@ -818,11 +905,11 @@ class VirtualizedList extends StateSafePureComponent { + + for (let ii = first; ii <= last; ii++) { + const item = getItem(data, ii); +- const key = this._keyExtractor(item, ii, this.props); ++ const key = VirtualizedList._keyExtractor(item, ii, this.props); + + this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { +- this.pushOrUnshift(stickyHeaderIndices, (cells.length)); ++ this.pushOrUnshift(stickyHeaderIndices, cells.length); + } + + const shouldListenForLayout = +@@ -861,15 +948,19 @@ class VirtualizedList extends StateSafePureComponent { + props: Props, + ): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); +- const last = Math.min(itemCount - 1, cells.last); ++ const lastPossibleCellIndex = itemCount - 1; + ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( + props.maxToRenderPerBatch, + ); ++ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); + + return { +- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), +- last, ++ first: clamp(0, cells.first, maxFirst), ++ last: Math.min(lastPossibleCellIndex, cells.last), + }; + } + +@@ -891,15 +982,14 @@ class VirtualizedList extends StateSafePureComponent { + _getSpacerKey = (isVertical: boolean): string => + isVertical ? 'height' : 'width'; + +- _keyExtractor( ++ static _keyExtractor( + item: Item, + index: number, + props: { + keyExtractor?: ?(item: Item, index: number) => string, + ... + }, +- // $FlowFixMe[missing-local-annot] +- ) { ++ ): string { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -945,6 +1035,10 @@ class VirtualizedList extends StateSafePureComponent { + cellKey={this._getCellKey() + '-header'} + key="$header"> + { + style: inversionStyle + ? [inversionStyle, this.props.style] + : this.props.style, ++ maintainVisibleContentPosition: ++ this.props.maintainVisibleContentPosition != null ++ ? { ++ ...this.props.maintainVisibleContentPosition, ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: ++ this.props.maintainVisibleContentPosition.minIndexForVisible + ++ (this.props.ListHeaderComponent ? 1 : 0), ++ } ++ : undefined, + }; + + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; +@@ -1255,11 +1359,10 @@ class VirtualizedList extends StateSafePureComponent { + _defaultRenderScrollComponent = props => { + const onRefresh = props.onRefresh; + const inversionStyle = this.props.inverted +- ? this.props.horizontal +- ? styles.rowReverse +- : styles.columnReverse +- : null; +- ++ ? this.props.horizontal ++ ? styles.rowReverse ++ : styles.columnReverse ++ : null; + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return ; +@@ -1542,8 +1645,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReachedThreshold, + onEndReached, + onEndReachedThreshold, +- initialScrollIndex, + } = this.props; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + const {contentLength, visibleLength, offset} = this._scrollMetrics; + let distanceFromStart = offset; + let distanceFromEnd = contentLength - visibleLength - offset; +@@ -1595,14 +1702,8 @@ class VirtualizedList extends StateSafePureComponent { + isWithinStartThreshold && + this._scrollMetrics.contentLength !== this._sentStartForContentLength + ) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({distanceFromStart}); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({distanceFromStart}); + } + + // If the user scrolls away from the start or end and back again, +@@ -1729,6 +1830,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale, + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -1844,6 +1950,7 @@ class VirtualizedList extends StateSafePureComponent { + const cellsAroundViewport = this._adjustCellsAroundViewport( + props, + state.cellsAroundViewport, ++ state.pendingScrollUpdateCount, + ); + const renderMask = VirtualizedList._createRenderMask( + props, +@@ -1874,7 +1981,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable, + }; + }; +@@ -1935,13 +2042,12 @@ class VirtualizedList extends StateSafePureComponent { + inLayout?: boolean, + ... + } => { +- const {data, getItem, getItemCount, getItemLayout} = props; ++ const {data, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); +- const item = getItem(data, index); +- const frame = this._frames[this._keyExtractor(item, index, props)]; ++ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -1976,11 +2082,8 @@ class VirtualizedList extends StateSafePureComponent { + // where it is. + if ( + focusedCellIndex >= itemCount || +- this._keyExtractor( +- props.getItem(props.data, focusedCellIndex), +- focusedCellIndex, +- props, +- ) !== this._lastFocusedCellKey ++ VirtualizedList._getItemKey(props, focusedCellIndex) !== ++ this._lastFocusedCellKey + ) { + return []; + } +@@ -2021,6 +2124,11 @@ class VirtualizedList extends StateSafePureComponent { + props: FrameMetricProps, + cellsAroundViewport: {first: number, last: number}, + ) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate( + props, diff --git a/src/CONST.ts b/src/CONST.ts index de902931ffd8..8cb2bb6c8893 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -869,14 +869,19 @@ const CONST = { RECOVERY_CODE_LENGTH: 8, KEYBOARD_TYPE: { - PHONE_PAD: 'phone-pad', - NUMBER_PAD: 'number-pad', - DECIMAL_PAD: 'decimal-pad', VISIBLE_PASSWORD: 'visible-password', - EMAIL_ADDRESS: 'email-address', ASCII_CAPABLE: 'ascii-capable', + }, + + INPUT_MODE: { + NONE: 'none', + TEXT: 'text', + DECIMAL: 'decimal', + NUMERIC: 'numeric', + TEL: 'tel', + SEARCH: 'search', + EMAIL: 'email', URL: 'url', - DEFAULT: 'default', }, YOUR_LOCATION_TEXT: 'Your Location', @@ -1331,6 +1336,8 @@ const CONST = { SPECIAL_CHAR: /[,/?"{}[\]()&^%;`$=#<>!*]/g, + FIRST_SPACE: /.+?(?=\s)/, + get SPECIAL_CHAR_OR_EMOJI() { return new RegExp(`[~\\n\\s]|(_\\b(?!$))|${this.SPECIAL_CHAR.source}|${this.EMOJI.source}`, 'gu'); }, @@ -2698,13 +2705,13 @@ const CONST = { BUTTON: 'button', LINK: 'link', MENUITEM: 'menuitem', - TEXT: 'text', + TEXT: 'presentation', RADIO: 'radio', - IMAGEBUTTON: 'imagebutton', + IMAGEBUTTON: 'img button', CHECKBOX: 'checkbox', SWITCH: 'switch', - ADJUSTABLE: 'adjustable', - IMAGE: 'image', + ADJUSTABLE: 'slider', + IMAGE: 'img', }, TRANSLATION_KEYS: { ATTACHMENT: 'common.attachment', diff --git a/src/Expensify.js b/src/Expensify.js index b7e3f0f60567..1b692f86a197 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -90,6 +90,8 @@ const defaultProps = { isCheckingPublicRoom: true, }; +const SplashScreenHiddenContext = React.createContext({}); + function Expensify(props) { const appStateChangeListener = useRef(null); const [isNavigationReady, setIsNavigationReady] = useState(false); @@ -105,6 +107,14 @@ function Expensify(props) { }, [props.isCheckingPublicRoom]); const isAuthenticated = useMemo(() => Boolean(lodashGet(props.session, 'authToken', null)), [props.session]); + + const contextValue = useMemo( + () => ({ + isSplashHidden, + }), + [isSplashHidden], + ); + const shouldInit = isNavigationReady && (!isAuthenticated || props.isSidebarLoaded) && hasAttemptedToOpenPublicRoom; const shouldHideSplash = shouldInit && !isSplashHidden; @@ -216,10 +226,12 @@ function Expensify(props) { {hasAttemptedToOpenPublicRoom && ( - + + + )} {shouldHideSplash && } @@ -251,3 +263,5 @@ export default compose( }, }), )(Expensify); + +export {SplashScreenHiddenContext}; diff --git a/src/components/AmountTextInput.js b/src/components/AmountTextInput.js index 5899e68bedb3..43fd5e6a1b98 100644 --- a/src/components/AmountTextInput.js +++ b/src/components/AmountTextInput.js @@ -50,11 +50,11 @@ function AmountTextInput(props) { ref={props.forwardedRef} value={props.formattedAmount} placeholder={props.placeholder} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + inputMode={CONST.INPUT_MODE.NUMERIC} blurOnSubmit={false} selection={props.selection} onSelectionChange={props.onSelectionChange} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} onKeyPress={props.onKeyPress} /> ); diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js index fd6c3d358a33..1e2d18bc4691 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js @@ -58,7 +58,7 @@ function BaseAnchorForAttachmentsOnly(props) { onPressOut={props.onPressOut} onLongPress={(event) => showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} accessibilityLabel={fileName} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > (linkRef = el)} style={StyleSheet.flatten([style, defaultTextStyle])} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.LINK} + role={CONST.ACCESSIBILITY_ROLE.LINK} hrefAttrs={{ rel, target: isEmail || !linkProps.href ? '_self' : target, diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index 53a8606c927f..38f70057be61 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -61,8 +61,7 @@ function CarouselItem({item, isFocused, onPress}) { onPress={() => setIsHidden(!isHidden)} > {isHidden ? translate('moderation.revealMessage') : translate('moderation.hideMessage')} @@ -81,7 +80,7 @@ function CarouselItem({item, isFocused, onPress}) { {children} diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js index 307dbe8e9ddb..d88eb81506ca 100755 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js @@ -27,7 +27,7 @@ function AttachmentViewImage({source, file, isAuthTokenRequired, loadComplete, o onPress={onPress} disabled={loadComplete} style={[styles.flex1, styles.flexRow, styles.alignSelfStretch]} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={file.name || translate('attachmentView.unknownFilename')} > {children} diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js index cb1190fa1fdd..8b29d8d5ba3d 100755 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js @@ -35,7 +35,7 @@ function AttachmentViewImage({source, file, isAuthTokenRequired, isFocused, isUs onPress={onPress} disabled={loadComplete} style={[styles.flex1, styles.flexRow, styles.alignSelfStretch]} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={file.name || translate('attachmentView.unknownFilename')} > {children} diff --git a/src/components/Avatar.js b/src/components/Avatar.js index 4b8ddd45aa95..5e414486cc70 100644 --- a/src/components/Avatar.js +++ b/src/components/Avatar.js @@ -88,10 +88,7 @@ function Avatar(props) { const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(props.name) : props.fallbackIcon || Expensicons.FallbackAvatar; return ( - + {_.isFunction(props.source) || (imageError && _.isFunction(fallbackAvatar)) ? ( runOnUI(sliderOnPress)(e.nativeEvent.locationX)} accessibilityLabel="slider" - accessibilityRole={CONST.ACCESSIBILITY_ROLE.ADJUSTABLE} + role={CONST.ACCESSIBILITY_ROLE.ADJUSTABLE} > showActorDetails(props.report, props.shouldEnableDetailPageNavigation)} accessibilityLabel={title} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > {shouldShowSubscriptAvatar ? ( ReportUtils.navigateToDetailsPage(props.report)} style={[styles.flexRow, styles.alignItemsCenter, styles.flex1]} accessibilityLabel={title} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > {headerView} diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index 871d967e23dc..87bd382e806b 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -264,7 +264,7 @@ class AvatarWithImagePicker extends React.Component { this.setState((prev) => ({isMenuVisible: !prev.isMenuVisible}))} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={this.props.translate('avatarWithImagePicker.editImage')} disabled={this.state.isAvatarCropModalOpen} ref={this.anchorRef} diff --git a/src/components/Badge.js b/src/components/Badge.js index 0a6b72201655..49b330ae37b2 100644 --- a/src/components/Badge.js +++ b/src/components/Badge.js @@ -59,8 +59,9 @@ function Badge(props) { diff --git a/src/components/BigNumberPad.js b/src/components/BigNumberPad.js index ecbde3a5afe6..451b8fc3e0bf 100644 --- a/src/components/BigNumberPad.js +++ b/src/components/BigNumberPad.js @@ -16,14 +16,14 @@ const propTypes = { longPressHandlerStateChanged: PropTypes.func, /** Used to locate this view from native classes. */ - nativeID: PropTypes.string, + id: PropTypes.string, ...withLocalizePropTypes, }; const defaultProps = { longPressHandlerStateChanged: () => {}, - nativeID: 'numPadView', + id: 'numPadView', }; const padNumbers = [ @@ -59,7 +59,7 @@ function BigNumberPad(props) { return ( {_.map(padNumbers, (row, rowIndex) => ( {this.renderContent()} diff --git a/src/components/Checkbox.js b/src/components/Checkbox.js index 51b9212133a4..5734ad2fed26 100644 --- a/src/components/Checkbox.js +++ b/src/components/Checkbox.js @@ -93,8 +93,8 @@ function Checkbox(props) { ref={props.forwardedRef} style={[StyleUtils.getCheckboxPressableStyle(props.containerBorderRadius + 2), props.style]} // to align outline on focus, border-radius of pressable should be 2px more than Checkbox onKeyDown={handleSpaceKey} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.CHECKBOX} - accessibilityState={{checked: props.isChecked}} + role={CONST.ACCESSIBILITY_ROLE.CHECKBOX} + ariaChecked={props.isChecked} accessibilityLabel={props.accessibilityLabel} pressDimmingValue={1} > diff --git a/src/components/CheckboxWithLabel.js b/src/components/CheckboxWithLabel.js index 4bffadecb733..86dba1d2a932 100644 --- a/src/components/CheckboxWithLabel.js +++ b/src/components/CheckboxWithLabel.js @@ -108,7 +108,7 @@ function CheckboxWithLabel(props) { accessibilityLabel={props.accessibilityLabel || props.label} /> {this.props.title} diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.js index aca2a9d06f7a..08d3e45e671f 100644 --- a/src/components/Composer/index.android.js +++ b/src/components/Composer/index.android.js @@ -4,6 +4,7 @@ import {StyleSheet} from 'react-native'; import _ from 'underscore'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; +import styles from '@styles/styles'; import themeColors from '@styles/themes/default'; const propTypes = { @@ -103,7 +104,7 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC return maxLines; }, [isComposerFullSize, maxLines]); - const styles = useMemo(() => { + const composerStyles = useMemo(() => { StyleSheet.flatten(props.style); }, [props.style]); @@ -114,16 +115,15 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC ref={setTextInputRef} onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} rejectResponderTermination={false} - textAlignVertical="center" // Setting a really high number here fixes an issue with the `maxNumberOfLines` prop on TextInput, where on Android the text input would collapse to only one line, // when it should actually expand to the container (https://github.com/Expensify/App/issues/11694#issuecomment-1560520670) // @Szymon20000 is working on fixing this (android-only) issue in the in the upstream PR (https://github.com/facebook/react-native/pulls?q=is%3Apr+is%3Aopen+maxNumberOfLines) // TODO: remove this comment once upstream PR is merged and available in a future release maxNumberOfLines={maxNumberOfLines} - style={styles} + style={[composerStyles, styles.verticalAlignMiddle]} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} - editable={!isDisabled} + readOnly={isDisabled} /> ); } diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js index e5dab3756594..a1b8c1a4ffe6 100644 --- a/src/components/Composer/index.ios.js +++ b/src/components/Composer/index.ios.js @@ -4,6 +4,7 @@ import {StyleSheet} from 'react-native'; import _ from 'underscore'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; +import styles from '@styles/styles'; import themeColors from '@styles/themes/default'; const propTypes = { @@ -103,7 +104,7 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC return maxLines; }, [isComposerFullSize, maxLines]); - const styles = useMemo(() => { + const composerStyles = useMemo(() => { StyleSheet.flatten(props.style); }, [props.style]); @@ -118,13 +119,12 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC ref={setTextInputRef} onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} rejectResponderTermination={false} - textAlignVertical="center" smartInsertDelete={false} maxNumberOfLines={maxNumberOfLines} - style={styles} + style={[composerStyles, styles.verticalAlignMiddle]} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...propsToPass} - editable={!isDisabled} + readOnly={isDisabled} /> ); } diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 924705e0fd39..d5d905f7d639 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -467,7 +467,7 @@ function Composer({ /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} - numberOfLines={numberOfLines} + rows={numberOfLines} disabled={isDisabled} onKeyPress={handleKeyPress} onFocus={(e) => { diff --git a/src/components/CurrencySymbolButton.js b/src/components/CurrencySymbolButton.js index 695cb2bc10c8..ca7816a9f117 100644 --- a/src/components/CurrencySymbolButton.js +++ b/src/components/CurrencySymbolButton.js @@ -22,7 +22,7 @@ function CurrencySymbolButton({onCurrencyButtonPress, currencySymbol}) { {currencySymbol} diff --git a/src/components/DatePicker/datepickerPropTypes.js b/src/components/DatePicker/datepickerPropTypes.js index 26424f2d8283..02d11806b8af 100644 --- a/src/components/DatePicker/datepickerPropTypes.js +++ b/src/components/DatePicker/datepickerPropTypes.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import {defaultProps as defaultFieldPropTypes, propTypes as fieldPropTypes} from '@components/TextInput/baseTextInputPropTypes'; +import {defaultProps as defaultFieldPropTypes, propTypes as fieldPropTypes} from '@components/TextInput/BaseTextInput/baseTextInputPropTypes'; import CONST from '@src/CONST'; const propTypes = { diff --git a/src/components/DatePicker/index.android.js b/src/components/DatePicker/index.android.js index 3b2a6ec3e650..17d1e2e14e71 100644 --- a/src/components/DatePicker/index.android.js +++ b/src/components/DatePicker/index.android.js @@ -7,7 +7,7 @@ import styles from '@styles/styles'; import CONST from '@src/CONST'; import {defaultProps, propTypes} from './datepickerPropTypes'; -function DatePicker({value, defaultValue, label, placeholder, errorText, containerStyles, disabled, onBlur, onInputChange, maxDate, minDate}, outerRef) { +const DatePicker = forwardRef(({value, defaultValue, label, placeholder, errorText, containerStyles, disabled, onBlur, onInputChange, maxDate, minDate}, outerRef) => { const ref = useRef(); const [isPickerVisible, setIsPickerVisible] = useState(false); @@ -46,7 +46,7 @@ function DatePicker({value, defaultValue, label, placeholder, errorText, contain ); -} +}); DatePicker.propTypes = propTypes; DatePicker.defaultProps = defaultProps; DatePicker.displayName = 'DatePicker'; -export default forwardRef(DatePicker); +export default DatePicker; diff --git a/src/components/DatePicker/index.ios.js b/src/components/DatePicker/index.ios.js index 60307f70e954..8b884c29b07f 100644 --- a/src/components/DatePicker/index.ios.js +++ b/src/components/DatePicker/index.ios.js @@ -85,14 +85,14 @@ function DatePicker({value, defaultValue, innerRef, onInputChange, preferredLoca forceActiveLabel label={label} accessibilityLabel={label} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} value={dateAsText} placeholder={placeholder} errorText={errorText} containerStyles={containerStyles} textInputContainerStyles={[isPickerVisible && styles.borderColorFocus]} onPress={showPicker} - editable={false} + readOnly disabled={disabled} onBlur={onBlur} ref={inputRef} diff --git a/src/components/DatePicker/index.js b/src/components/DatePicker/index.js index 33266242c5db..16d2eb2668a3 100644 --- a/src/components/DatePicker/index.js +++ b/src/components/DatePicker/index.js @@ -61,7 +61,7 @@ function DatePicker({maxDate, minDate, onInputChange, innerRef, label, value, pl onFocus={showDatepicker} label={label} accessibilityLabel={label} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} onInputChange={setDate} value={value} placeholder={placeholder} diff --git a/src/components/DistanceRequest/index.js b/src/components/DistanceRequest/index.js index 3a41d52eb7f4..eb6811d02323 100644 --- a/src/components/DistanceRequest/index.js +++ b/src/components/DistanceRequest/index.js @@ -150,13 +150,8 @@ function DistanceRequest({transactionID, report, transaction, route, isEditingRe return ErrorUtils.getLatestErrorField(transaction, 'route'); } - // Initially, both waypoints will be null, and if we give fallback value as empty string that will result in true condition, that's why different default values. - if (_.keys(waypoints).length === 2 && lodashGet(waypoints, 'waypoint0.address', 'address1') === lodashGet(waypoints, 'waypoint1.address', 'address2')) { - return {0: translate('iou.error.duplicateWaypointsErrorMessage')}; - } - if (_.size(validatedWaypoints) < 2) { - return {0: translate('iou.error.emptyWaypointsErrorMessage')}; + return {0: translate('iou.error.atLeastTwoDifferentWaypoints')}; } }; diff --git a/src/components/EmojiPicker/CategoryShortcutButton.js b/src/components/EmojiPicker/CategoryShortcutButton.js index 132da7c540c2..3b12afce0d08 100644 --- a/src/components/EmojiPicker/CategoryShortcutButton.js +++ b/src/components/EmojiPicker/CategoryShortcutButton.js @@ -38,7 +38,7 @@ function CategoryShortcutButton(props) { onHoverOut={() => setIsHighlighted(false)} style={({pressed}) => [StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), styles.categoryShortcutButton, isHighlighted && styles.emojiItemHighlighted]} accessibilityLabel={`emojiPicker.headers.${props.code}`} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > { outerStyle={StyleUtils.getOuterModalStyle(windowHeight, props.viewportOffsetTop)} innerContainerStyle={styles.popoverInnerContainer} avoidKeyboard + shouldUseTargetLocation > {({hovered, pressed}) => ( diff --git a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js index 8a5a66444fda..63a6c33a437f 100644 --- a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js +++ b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js @@ -38,6 +38,7 @@ function EmojiPickerButtonDropdown(props) { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, shiftVertical: 4, + shouldUseTargetLocation: true, }); }; @@ -48,9 +49,9 @@ function EmojiPickerButtonDropdown(props) { style={styles.emojiPickerButtonDropdown} disabled={props.isDisabled} onPress={onPress} - nativeID="emojiDropdownButton" + id="emojiDropdownButton" accessibilityLabel="statusEmoji" - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > {({hovered, pressed}) => ( diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 7dc53e958849..0ee12579733d 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -471,15 +471,18 @@ function EmojiPickerMenu(props) { const overflowLimit = Math.floor(height / CONST.EMOJI_PICKER_ITEM_HEIGHT) * 8; return ( {translate('common.noResultsFound')}} + ListEmptyComponent={() => {translate('common.noResultsFound')}} /> 0} /> diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js index 90f7f966172f..451e2e939a09 100644 --- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js @@ -100,7 +100,7 @@ class EmojiPickerMenuItem extends PureComponent { styles.emojiItem, ]} accessibilityLabel={this.props.emoji} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > {this.props.emoji} diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js index 099adf620af7..6ebaa3391992 100644 --- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js @@ -77,7 +77,7 @@ class EmojiPickerMenuItem extends PureComponent { styles.emojiItem, ]} accessibilityLabel={this.props.emoji} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > {this.props.emoji} diff --git a/src/components/EmojiPicker/EmojiSkinToneList.js b/src/components/EmojiPicker/EmojiSkinToneList.js index edb8bf49e77f..29c39c335b14 100644 --- a/src/components/EmojiPicker/EmojiSkinToneList.js +++ b/src/components/EmojiPicker/EmojiSkinToneList.js @@ -48,7 +48,7 @@ function EmojiSkinToneList(props) { onPress={toggleIsSkinToneListVisible} style={[styles.flexRow, styles.alignSelfCenter, styles.justifyContentStart, styles.alignItemsCenter]} accessibilityLabel={props.translate('emojiPicker.skinTonePickerLabel')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > {currentSkinTone.code} diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js index 22f88cc53f59..d8a5a0256e62 100644 --- a/src/components/FloatingActionButton.js +++ b/src/components/FloatingActionButton.js @@ -90,7 +90,7 @@ class FloatingActionButton extends PureComponent { } }} accessibilityLabel={this.props.accessibilityLabel} - accessibilityRole={this.props.accessibilityRole} + role={this.props.role} pressDimmingValue={1} onPress={(e) => { // Drop focus to avoid blue focus ring. diff --git a/src/components/FormElement.js b/src/components/FormElement.js index cc9423a6147f..d929ddb5f2e4 100644 --- a/src/components/FormElement.js +++ b/src/components/FormElement.js @@ -4,7 +4,7 @@ import * as ComponentUtils from '@libs/ComponentUtils'; const FormElement = forwardRef((props, ref) => ( diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js index f6d37f661252..8461f714373b 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js @@ -76,7 +76,7 @@ function ImageRenderer(props) { ReportUtils.isArchivedRoom(report), ) } - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={translate('accessibilityHints.viewAttachment')} > { onPressIn={props.onPressIn} onPressOut={props.onPressOut} onLongPress={(event) => showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} accessibilityLabel={props.translate('accessibilityHints.prestyledText')} > diff --git a/src/components/HeaderWithBackButton/index.js b/src/components/HeaderWithBackButton/index.js index 67e8790560dc..6a8f630d1e78 100755 --- a/src/components/HeaderWithBackButton/index.js +++ b/src/components/HeaderWithBackButton/index.js @@ -76,7 +76,7 @@ function HeaderWithBackButton({ onBackButtonPress(); }} style={[styles.touchableButtonImage]} - accessibilityRole="button" + role="button" accessibilityLabel={translate('common.back')} > Navigation.navigate(ROUTES.GET_ASSISTANCE.getRoute(guidesCallTaskID))))} style={[styles.touchableButtonImage]} - accessibilityRole="button" + role="button" accessibilityLabel={translate('getAssistancePage.questionMarkButtonTooltip')} > ( // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} + contentContainerStyle={styles.justifyContentEnd} CellRendererComponent={CellRendererComponent} /** * To achieve absolute positioning and handle overflows for list items, the property must be disabled diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index 3986773aca87..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 ( diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 1a5bcf5ffb60..685b8763781d 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -205,7 +205,7 @@ function OptionRowLHN(props) { props.isFocused ? styles.sidebarLinkActive : null, (hovered || isContextMenuActive) && !props.isFocused ? props.hoverStyle : null, ]} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={translate('accessibilityHints.navigatesToChat')} needsOffscreenAlphaCompositing={props.optionItem.icons.length >= 2} > diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index 4db32ed8bd2a..ca579d175cac 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -16,7 +16,7 @@ const propTypes = { isFocused: PropTypes.bool, /** List of users' personal details */ - personalDetails: PropTypes.arrayOf(participantPropTypes), + personalDetails: PropTypes.objectOf(participantPropTypes), /** The preferred language for the app */ preferredLocale: PropTypes.string, diff --git a/src/components/LocationErrorMessage/BaseLocationErrorMessage.js b/src/components/LocationErrorMessage/BaseLocationErrorMessage.js index b5114acaa36b..5880d3475650 100644 --- a/src/components/LocationErrorMessage/BaseLocationErrorMessage.js +++ b/src/components/LocationErrorMessage/BaseLocationErrorMessage.js @@ -62,7 +62,7 @@ function BaseLocationErrorMessage({onClose, onAllowLocationLinkPress, locationEr onPress={onClose} onMouseDown={(e) => e.preventDefault()} style={[styles.touchableButtonImage]} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={translate('common.close')} > diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index 8119248c760d..978b101a6cce 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -319,7 +319,6 @@ function MagicCodeInput(props) { value={input} hideFocusedState autoComplete={index === 0 ? props.autoComplete : 'off'} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} onChangeText={(value) => { // Do not run when the event comes from an input that is // not currently being responsible for the input, this is @@ -337,7 +336,7 @@ function MagicCodeInput(props) { selectionColor="transparent" textInputContainerStyles={[styles.borderNone]} inputStyle={[styles.inputTransparent]} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} /> diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index 08535f1724fb..103d063f9024 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -184,7 +184,7 @@ const MenuItem = React.forwardRef((props, ref) => { ]} disabled={props.disabled} ref={ref} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.MENUITEM} + role={CONST.ACCESSIBILITY_ROLE.MENUITEM} accessibilityLabel={props.title ? props.title.toString() : ''} > {({pressed}) => ( diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js index 6ed3b16c676d..bf1fdc8ee7de 100644 --- a/src/components/Modal/BaseModal.js +++ b/src/components/Modal/BaseModal.js @@ -183,6 +183,7 @@ function BaseModal({ onModalShow={handleShowModal} propagateSwipe={propagateSwipe} onModalHide={hideModal} + onModalWillShow={() => ComposerFocusManager.resetReadyToFocus()} onDismiss={handleDismissModal} onSwipeComplete={onClose} swipeDirection={swipeDirection} diff --git a/src/components/MultipleAvatars.js b/src/components/MultipleAvatars.js index 85b6f7995693..209540189b69 100644 --- a/src/components/MultipleAvatars.js +++ b/src/components/MultipleAvatars.js @@ -227,8 +227,7 @@ function MultipleAvatars(props) { ]} > {`+${avatars.length - props.maxAvatarsInRow}`} @@ -293,8 +292,7 @@ function MultipleAvatars(props) { {`+${props.icons.length - 1}`} diff --git a/src/components/NewDatePicker/CalendarPicker/YearPickerModal.js b/src/components/NewDatePicker/CalendarPicker/YearPickerModal.js index d784f439dfee..f040a99450f1 100644 --- a/src/components/NewDatePicker/CalendarPicker/YearPickerModal.js +++ b/src/components/NewDatePicker/CalendarPicker/YearPickerModal.js @@ -76,7 +76,7 @@ function YearPickerModal(props) { textInputValue={searchText} textInputMaxLength={4} onChangeText={(text) => setSearchText(text.replace(CONST.REGEX.NON_NUMERIC, '').trim())} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + inputMode={CONST.INPUT_MODE.NUMERIC} headerMessage={headerMessage} sections={sections} onSelectRow={(option) => props.onYearChange(option.value)} diff --git a/src/components/NewDatePicker/CalendarPicker/index.js b/src/components/NewDatePicker/CalendarPicker/index.js index 4b17766feb17..58ab42a9b56a 100644 --- a/src/components/NewDatePicker/CalendarPicker/index.js +++ b/src/components/NewDatePicker/CalendarPicker/index.js @@ -213,7 +213,7 @@ class CalendarPicker extends React.PureComponent { onPress={() => this.onDayPressed(day)} style={styles.calendarDayRoot} accessibilityLabel={day ? day.toString() : undefined} - focusable={Boolean(day)} + tabIndex={day ? 0 : -1} accessible={Boolean(day)} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} > diff --git a/src/components/NewDatePicker/index.js b/src/components/NewDatePicker/index.js index f03b4e2cb796..351e5034cfb4 100644 --- a/src/components/NewDatePicker/index.js +++ b/src/components/NewDatePicker/index.js @@ -6,7 +6,7 @@ import {View} from 'react-native'; import InputWrapper from '@components/Form/InputWrapper'; import * as Expensicons from '@components/Icon/Expensicons'; import TextInput from '@components/TextInput'; -import {propTypes as baseTextInputPropTypes, defaultProps as defaultBaseTextInputPropTypes} from '@components/TextInput/baseTextInputPropTypes'; +import {propTypes as baseTextInputPropTypes, defaultProps as defaultBaseTextInputPropTypes} from '@components/TextInput/BaseTextInput/baseTextInputPropTypes'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import styles from '@styles/styles'; import CONST from '@src/CONST'; @@ -75,7 +75,7 @@ function NewDatePicker({containerStyles, defaultValue, disabled, errorText, inpu icon={Expensicons.Calendar} label={label} accessibilityLabel={label} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} value={value || selectedDate || ''} placeholder={placeholder || translate('common.dateFormat')} errorText={errorText} @@ -83,7 +83,7 @@ function NewDatePicker({containerStyles, defaultValue, disabled, errorText, inpu textInputContainerStyles={[styles.borderColorFocus]} inputStyle={[styles.pointerEventsNone]} disabled={disabled} - editable={false} + readOnly /> diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js index c2f272663c20..1a60cc0280b6 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.js @@ -188,7 +188,7 @@ function OptionRow(props) { props.isSelected && props.highlightSelected && styles.optionRowSelected, ]} accessibilityLabel={props.option.text} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} hoverDimmingValue={1} hoverStyle={props.hoverStyle} needsOffscreenAlphaCompositing={lodashGet(props.option, 'icons.length', 0) >= 2} @@ -263,7 +263,7 @@ function OptionRow(props) { props.onSelectedStatePressed(props.option)} disabled={isDisabled} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.CHECKBOX} + role={CONST.ACCESSIBILITY_ROLE.CHECKBOX} accessibilityLabel={CONST.ACCESSIBILITY_ROLE.CHECKBOX} > diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index fb312125efc0..8c480c27f20f 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -378,7 +378,7 @@ class BaseOptionsSelector extends Component { value={this.props.value} label={this.props.textInputLabel} accessibilityLabel={this.props.textInputLabel} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} onChangeText={this.updateSearchValue} errorText={this.state.errorMessage} onSubmitEditing={this.selectFocusedOption} diff --git a/src/components/OptionsSelector/optionsSelectorPropTypes.js b/src/components/OptionsSelector/optionsSelectorPropTypes.js index 37c15b6e3ae2..94aab8fac5f6 100644 --- a/src/components/OptionsSelector/optionsSelectorPropTypes.js +++ b/src/components/OptionsSelector/optionsSelectorPropTypes.js @@ -40,8 +40,8 @@ const propTypes = { /** Label to display for the text input */ textInputLabel: PropTypes.string, - /** Optional keyboard type for the input */ - keyboardType: PropTypes.string, + /** Optional input mode precedence over keyboardType */ + inputMode: PropTypes.string, /** Optional placeholder text for the selector */ placeholderText: PropTypes.string, @@ -144,7 +144,7 @@ const defaultProps = { onSelectRow: undefined, textInputLabel: '', placeholderText: '', - keyboardType: 'default', + inputMode: CONST.INPUT_MODE.TEXT, selectedOptions: [], headerMessage: '', canSelectMultipleOptions: false, diff --git a/src/components/PDFView/PDFPasswordForm.js b/src/components/PDFView/PDFPasswordForm.js index 2105bc13cb00..e495057dec46 100644 --- a/src/components/PDFView/PDFPasswordForm.js +++ b/src/components/PDFView/PDFPasswordForm.js @@ -122,7 +122,7 @@ function PDFPasswordForm({isFocused, isPasswordInvalid, shouldShowLoadingIndicat ref={textInputRef} label={translate('common.password')} accessibilityLabel={translate('common.password')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} /** * This is a workaround to bypass Safari's autofill odd behaviour. * This tricks the browser not to fill the username somewhere else and still fill the password correctly. @@ -131,7 +131,7 @@ function PDFPasswordForm({isFocused, isPasswordInvalid, shouldShowLoadingIndicat autoCorrect={false} textContentType="password" onChangeText={updatePassword} - returnKeyType="go" + enterKeyHint="done" onSubmitEditing={submitPassword} errorText={errorText} onFocus={() => onPasswordFieldFocused(true)} diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js index af153d69d166..fd01176d9fb2 100644 --- a/src/components/PDFView/index.js +++ b/src/components/PDFView/index.js @@ -274,7 +274,7 @@ class PDFView extends Component { return ( {this.renderPDFView()} diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 7c6514c1e035..c8636b2dc50f 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -180,7 +180,7 @@ class PDFView extends Component { {this.renderPDFView()} diff --git a/src/components/ParentNavigationSubtitle.js b/src/components/ParentNavigationSubtitle.js index b29794e62856..5b4825392719 100644 --- a/src/components/ParentNavigationSubtitle.js +++ b/src/components/ParentNavigationSubtitle.js @@ -41,7 +41,7 @@ function ParentNavigationSubtitle(props) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.parentReportID)); }} accessibilityLabel={translate('threads.parentNavigationSummary', {rootReportName, workspaceName})} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.LINK} + role={CONST.ACCESSIBILITY_ROLE.LINK} style={[...props.pressableStyles]} > - {props.label && ( - - {props.label} - - )} + {props.label && {props.label}} Report.togglePinnedState(props.report.reportID, props.report.isPinned))} style={[styles.touchableButtonImage]} - accessibilityState={{checked: props.report.isPinned}} + ariaChecked={props.report.isPinned} accessibilityLabel={props.report.isPinned ? props.translate('common.unPin') : props.translate('common.pin')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > { let horizontalConstraint; switch (props.anchorAlignment.horizontal) { @@ -103,13 +111,18 @@ function PopoverWithMeasuredContent(props) { break; case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT: default: - horizontalConstraint = {left: props.anchorPosition.horizontal}; + horizontalConstraint = {left: clickedTargetLocation.horizontal}; } let verticalConstraint; + const anchorLocationVertical = clickedTargetLocation.vertical; + switch (props.anchorAlignment.vertical) { case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM: - verticalConstraint = {top: props.anchorPosition.vertical - popoverHeight}; + if (!anchorLocationVertical) { + break; + } + verticalConstraint = {top: anchorLocationVertical - popoverHeight}; break; case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.CENTER: verticalConstraint = { @@ -125,7 +138,7 @@ function PopoverWithMeasuredContent(props) { ...horizontalConstraint, ...verticalConstraint, }; - }, [props.anchorPosition, props.anchorAlignment, popoverWidth, popoverHeight]); + }, [props.anchorPosition, props.anchorAlignment, clickedTargetLocation.vertical, clickedTargetLocation.horizontal, popoverWidth, popoverHeight]); const horizontalShift = computeHorizontalShift(adjustedAnchorPosition.left, popoverWidth, windowWidth); const verticalShift = computeVerticalShift(adjustedAnchorPosition.top, popoverHeight, windowHeight); diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.js b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx similarity index 65% rename from src/components/Pressable/GenericPressable/BaseGenericPressable.js rename to src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index a3ce55003cdd..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,39 +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.accessibilityRole, 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 accessibilityState={{ disabled: isDisabled, - ...props.accessibilityState, + ...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 8247d0c35670..000000000000 --- a/src/components/Pressable/GenericPressable/index.js +++ /dev/null @@ -1,26 +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 14a2c2bcbf82..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 50% rename from src/components/Pressable/PressableWithDelayToggle.js rename to src/components/Pressable/PressableWithDelayToggle.tsx index c9f05e5adfee..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 ad29204bb018..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. - */ - nativeID: 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, - nativeID: '', - 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/RadioButton.js b/src/components/RadioButton.js index 9d8e739d723c..bb32f4a2c37b 100644 --- a/src/components/RadioButton.js +++ b/src/components/RadioButton.js @@ -37,7 +37,7 @@ function RadioButton(props) { hoverDimmingValue={1} pressDimmingValue={1} accessibilityLabel={props.accessibilityLabel} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.RADIO} + role={CONST.ACCESSIBILITY_ROLE.RADIO} > props.onPress()} style={[styles.flexRow, styles.flexWrap, styles.flexShrink1, styles.alignItemsCenter]} diff --git a/src/components/Reactions/AddReactionBubble.js b/src/components/Reactions/AddReactionBubble.js index 4e12ace9cc6c..653236f35831 100644 --- a/src/components/Reactions/AddReactionBubble.js +++ b/src/components/Reactions/AddReactionBubble.js @@ -98,7 +98,7 @@ function AddReactionBubble(props) { e.preventDefault(); }} accessibilityLabel={props.translate('emojiReactions.addReactionTooltip')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} // disable dimming pressDimmingValue={1} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} diff --git a/src/components/Reactions/EmojiReactionBubble.js b/src/components/Reactions/EmojiReactionBubble.js index a61923e49860..822b15711c50 100644 --- a/src/components/Reactions/EmojiReactionBubble.js +++ b/src/components/Reactions/EmojiReactionBubble.js @@ -82,7 +82,7 @@ function EmojiReactionBubble(props) { // Prevent text input blur when emoji reaction is left clicked e.preventDefault(); }} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={props.emojiCodes.join('')} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} > diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 3d696747de3d..f05f678eb6ed 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -175,6 +175,8 @@ function MoneyRequestPreview(props) { const receiptImages = hasReceipt ? [ReceiptUtils.getThumbnailAndImageURIs(props.transaction)] : []; + const hasPendingWaypoints = lodashGet(props.transaction, 'pendingFields.waypoints', null); + const getSettledMessage = () => { if (isExpensifyCardTransaction) { return props.translate('common.done'); @@ -223,7 +225,7 @@ function MoneyRequestPreview(props) { const getDisplayAmountText = () => { if (isDistanceRequest) { - return requestAmount ? CurrencyUtils.convertToDisplayString(requestAmount, props.transaction.currency) : props.translate('common.tbd'); + return requestAmount && !hasPendingWaypoints ? CurrencyUtils.convertToDisplayString(requestAmount, props.transaction.currency) : props.translate('common.tbd'); } if (isScanning) { @@ -319,7 +321,9 @@ function MoneyRequestPreview(props) { {shouldShowMerchant && ( - {requestMerchant} + + {hasPendingWaypoints ? requestMerchant.replace(CONST.REGEX.FIRST_SPACE, props.translate('common.tbd')) : requestMerchant} + )} diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index aa1813fa6e4d..7c5ffe88ef0a 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -99,7 +99,8 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); let formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; - if (isDistanceRequest && !formattedTransactionAmount) { + const hasPendingWaypoints = lodashGet(transaction, 'pendingFields.waypoints', null); + if (isDistanceRequest && (!formattedTransactionAmount || hasPendingWaypoints)) { formattedTransactionAmount = translate('common.tbd'); } const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); @@ -206,7 +207,7 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should {receiptImageComponent} diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 00a4526b382f..45fe7d42e299 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -129,17 +129,24 @@ function ReportPreview(props) { const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(props.iouReportID); - const hasOnlyOneReceiptRequest = numberOfRequests === 1 && hasReceipts; - const previewSubtitle = hasOnlyOneReceiptRequest - ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) - : props.translate('iou.requestCount', { - count: numberOfRequests, - scanningReceipts: numberOfScanningReceipts, - }); + let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; + const hasPendingWaypoints = formattedMerchant && hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => lodashGet(transaction, 'pendingFields.waypoints', null)); + if (hasPendingWaypoints) { + formattedMerchant = formattedMerchant.replace(CONST.REGEX.FIRST_SPACE, props.translate('common.tbd')); + } + const previewSubtitle = + formattedMerchant || + props.translate('iou.requestCount', { + count: numberOfRequests, + scanningReceipts: numberOfScanningReceipts, + }); const shouldShowSubmitButton = isReportDraft && reimbursableSpend !== 0; const getDisplayAmount = () => { + if (hasPendingWaypoints) { + return props.translate('common.tbd'); + } if (totalDisplaySpend) { return CurrencyUtils.convertToDisplayString(totalDisplaySpend, props.iouReport.currency); } @@ -192,7 +199,7 @@ function ReportPreview(props) { onPressOut={() => ControlSelection.unblock()} onLongPress={(event) => showContextMenuForReport(event, props.contextMenuAnchor, props.chatReportID, props.action, props.checkIfContextMenuActive)} style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox]} - accessibilityRole="button" + role="button" accessibilityLabel={props.translate('iou.viewDetails')} > diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index af5b1e25f2a9..b31de0d22f4c 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -105,7 +105,7 @@ function TaskPreview(props) { onPressOut={() => ControlSelection.unblock()} onLongPress={(event) => showContextMenuForReport(event, props.contextMenuAnchor, props.chatReportID, props.action, props.checkIfContextMenuActive)} style={[styles.flexRow, styles.justifyContentBetween]} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={props.translate('task.task')} > diff --git a/src/components/ReportHeaderSkeletonView.js b/src/components/ReportHeaderSkeletonView.js index 6d2a8e343e3b..1b183fa9604c 100644 --- a/src/components/ReportHeaderSkeletonView.js +++ b/src/components/ReportHeaderSkeletonView.js @@ -32,7 +32,7 @@ function ReportHeaderSkeletonView(props) { {}} style={[styles.LHNToggle]} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={props.translate('common.back')} > diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js index 6f191f82ba35..13d07bb0278a 100644 --- a/src/components/RoomHeaderAvatars.js +++ b/src/components/RoomHeaderAvatars.js @@ -39,7 +39,7 @@ function RoomHeaderAvatars(props) { + {_.map(iconsToDisplay, (icon, index) => ( gestureState.numberActiveTouches === CONST.TEST_TOOL.NUMBER_OF_TAPS, - onPanResponderRelease: toggleTestToolsModal, - }), - ).current; - - const keyboardDissmissPanResponder = useRef( - PanResponder.create({ - onMoveShouldSetPanResponderCapture: (e, gestureState) => { - const isHorizontalSwipe = Math.abs(gestureState.dx) > Math.abs(gestureState.dy); - const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && isKeyboardShown && Browser.isMobile(); - - return isHorizontalSwipe && shouldDismissKeyboard; - }, - onPanResponderGrant: Keyboard.dismiss, - }), - ).current; - - useEffect(() => { - const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', (event) => { - // Prevent firing the prop callback when user is exiting the page. - if (lodashGet(event, 'data.closing')) { - return; - } - - setDidScreenTransitionEnd(true); - onEntryTransitionEnd(); - }); - - // We need to have this prop to remove keyboard before going away from the screen, to avoid previous screen look weird for a brief moment, - // also we need to have generic control in future - to prevent closing keyboard for some rare cases in which beforeRemove has limitations - // described here https://reactnavigation.org/docs/preventing-going-back/#limitations - const beforeRemoveSubscription = shouldDismissKeyboardBeforeClose - ? navigation.addListener('beforeRemove', () => { - if (!isKeyboardShownRef.current) { - return; - } - Keyboard.dismiss(); - }) - : undefined; - - return () => { - unsubscribeTransitionEnd(); - - if (beforeRemoveSubscription) { - beforeRemoveSubscription(); - } - }; - // Rule disabled because this effect is only for component did mount & will component unmount lifecycle event - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - - {({insets, paddingTop, paddingBottom, safeAreaPaddingBottomStyle}) => { - const paddingStyle = {}; - - if (includePaddingTop) { - paddingStyle.paddingTop = paddingTop; +const ScreenWrapper = React.forwardRef( + ( + { + shouldEnableMaxHeight, + shouldEnableMinHeight, + includePaddingTop, + keyboardAvoidingViewBehavior, + includeSafeAreaPaddingBottom, + shouldEnableKeyboardAvoidingView, + shouldEnablePickerAvoiding, + headerGapStyles, + children, + shouldShowOfflineIndicator, + offlineIndicatorStyle, + style, + shouldDismissKeyboardBeforeClose, + onEntryTransitionEnd, + testID, + }, + ref, + ) => { + const {windowHeight, isSmallScreenWidth} = useWindowDimensions(); + const {initialHeight} = useInitialDimensions(); + const keyboardState = useKeyboardState(); + const {isDevelopment} = useEnvironment(); + const {isOffline} = useNetwork(); + const navigation = useNavigation(); + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; + const minHeight = shouldEnableMinHeight ? initialHeight : undefined; + const isKeyboardShown = lodashGet(keyboardState, 'isKeyboardShown', false); + + const isKeyboardShownRef = useRef(); + + isKeyboardShownRef.current = lodashGet(keyboardState, 'isKeyboardShown', false); + + const panResponder = useRef( + PanResponder.create({ + onStartShouldSetPanResponderCapture: (e, gestureState) => gestureState.numberActiveTouches === CONST.TEST_TOOL.NUMBER_OF_TAPS, + onPanResponderRelease: toggleTestToolsModal, + }), + ).current; + + const keyboardDissmissPanResponder = useRef( + PanResponder.create({ + onMoveShouldSetPanResponderCapture: (e, gestureState) => { + const isHorizontalSwipe = Math.abs(gestureState.dx) > Math.abs(gestureState.dy); + const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && isKeyboardShown && Browser.isMobile(); + + return isHorizontalSwipe && shouldDismissKeyboard; + }, + onPanResponderGrant: Keyboard.dismiss, + }), + ).current; + + useEffect(() => { + const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', (event) => { + // Prevent firing the prop callback when user is exiting the page. + if (lodashGet(event, 'data.closing')) { + return; } - // We always need the safe area padding bottom if we're showing the offline indicator since it is bottom-docked. - if (includeSafeAreaPaddingBottom || (isOffline && shouldShowOfflineIndicator)) { - paddingStyle.paddingBottom = paddingBottom; + setDidScreenTransitionEnd(true); + onEntryTransitionEnd(); + }); + + // We need to have this prop to remove keyboard before going away from the screen, to avoid previous screen look weird for a brief moment, + // also we need to have generic control in future - to prevent closing keyboard for some rare cases in which beforeRemove has limitations + // described here https://reactnavigation.org/docs/preventing-going-back/#limitations + const beforeRemoveSubscription = shouldDismissKeyboardBeforeClose + ? navigation.addListener('beforeRemove', () => { + if (!isKeyboardShownRef.current) { + return; + } + Keyboard.dismiss(); + }) + : undefined; + + return () => { + unsubscribeTransitionEnd(); + + if (beforeRemoveSubscription) { + beforeRemoveSubscription(); } - - return ( - + }; + // Rule disabled because this effect is only for component did mount & will component unmount lifecycle event + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {({insets, paddingTop, paddingBottom, safeAreaPaddingBottomStyle}) => { + const paddingStyle = {}; + + if (includePaddingTop) { + paddingStyle.paddingTop = paddingTop; + } + + // We always need the safe area padding bottom if we're showing the offline indicator since it is bottom-docked. + if (includeSafeAreaPaddingBottom || (isOffline && shouldShowOfflineIndicator)) { + paddingStyle.paddingBottom = paddingBottom; + } + + return ( - - - - {isDevelopment && } - {isDevelopment && } - { - // If props.children is a function, call it to provide the insets to the children. - _.isFunction(children) - ? children({ - insets, - safeAreaPaddingBottomStyle, - didScreenTransitionEnd, - }) - : children - } - {isSmallScreenWidth && shouldShowOfflineIndicator && } - - + + + {isDevelopment && } + {isDevelopment && } + { + // If props.children is a function, call it to provide the insets to the children. + _.isFunction(children) + ? children({ + insets, + safeAreaPaddingBottomStyle, + didScreenTransitionEnd, + }) + : children + } + {isSmallScreenWidth && shouldShowOfflineIndicator && } + + + - - ); - }} - - ); -} + ); + }} + + ); + }, +); ScreenWrapper.displayName = 'ScreenWrapper'; ScreenWrapper.propTypes = propTypes; diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.js index 3dd4417367b2..5f9fced94cb2 100644 --- a/src/components/SelectionList/BaseListItem.js +++ b/src/components/SelectionList/BaseListItem.js @@ -40,7 +40,7 @@ function BaseListItem({ onPress={() => onSelectRow(item)} disabled={isDisabled} accessibilityLabel={item.text} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} hoverDimmingValue={1} hoverStyle={styles.hoveredComponentBG} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 2a7947733a9e..e3ba0dbd7c2f 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -40,7 +40,7 @@ function BaseSelectionList({ textInputPlaceholder = '', textInputValue = '', textInputMaxLength, - keyboardType = CONST.KEYBOARD_TYPE.DEFAULT, + inputMode = CONST.INPUT_MODE.TEXT, onChangeText, initiallyFocusedOptionKey = '', onScroll, @@ -389,12 +389,12 @@ function BaseSelectionList({ }} label={textInputLabel} accessibilityLabel={textInputLabel} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} value={textInputValue} placeholder={textInputPlaceholder} maxLength={textInputMaxLength} onChangeText={onChangeText} - keyboardType={keyboardType} + inputMode={inputMode} selectTextOnFocus spellCheck={false} onSubmitEditing={selectFocusedOption} @@ -417,7 +417,7 @@ function BaseSelectionList({ style={[styles.peopleRow, styles.userSelectNone, styles.ph5, styles.pb3]} onPress={selectAllRow} accessibilityLabel={translate('workspace.people.selectAll')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role="button" accessibilityState={{checked: flattenedSections.allSelected}} disabled={flattenedSections.allOptions.length === flattenedSections.disabledOptionsIndexes.length} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} diff --git a/src/components/SelectionList/selectionListPropTypes.js b/src/components/SelectionList/selectionListPropTypes.js index c3bae89eaba2..5b95f7dd0cbf 100644 --- a/src/components/SelectionList/selectionListPropTypes.js +++ b/src/components/SelectionList/selectionListPropTypes.js @@ -138,8 +138,8 @@ const propTypes = { /** Callback to fire when the text input changes */ onChangeText: PropTypes.func, - /** Keyboard type for the text input */ - keyboardType: PropTypes.string, + /** Input mode for the text input */ + inputMode: PropTypes.string, /** Item `keyForList` to focus initially */ initiallyFocusedOptionKey: PropTypes.string, diff --git a/src/components/SignInButtons/GoogleSignIn/index.website.js b/src/components/SignInButtons/GoogleSignIn/index.website.js index 7f3a3019c318..d65af124bfe8 100644 --- a/src/components/SignInButtons/GoogleSignIn/index.website.js +++ b/src/components/SignInButtons/GoogleSignIn/index.website.js @@ -5,6 +5,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import styles from '@styles/styles'; import * as Session from '@userActions/Session'; import CONFIG from '@src/CONFIG'; +import CONST from '@src/CONST'; const propTypes = { /** Whether we're rendering in the Desktop Flow, if so show a different button. */ @@ -74,7 +75,7 @@ function GoogleSignIn({translate, isDesktopFlow}) {
@@ -82,7 +83,7 @@ function GoogleSignIn({translate, isDesktopFlow}) {
diff --git a/src/components/SignInButtons/IconButton.js b/src/components/SignInButtons/IconButton.js index 0d18779ea9ba..ce932b875542 100644 --- a/src/components/SignInButtons/IconButton.js +++ b/src/components/SignInButtons/IconButton.js @@ -37,7 +37,7 @@ function IconButton({onPress, translate, provider}) { onSelectOption(option)} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityState={{checked: selectedOptionKey === option.key}} aria-checked={selectedOptionKey === option.key} accessibilityLabel={option.label} diff --git a/src/components/Switch.js b/src/components/Switch.js index 755cd60f2597..c5adbbef61da 100644 --- a/src/components/Switch.js +++ b/src/components/Switch.js @@ -38,9 +38,8 @@ function Switch(props) { style={[styles.switchTrack, !props.isOn && styles.switchInactive]} onPress={() => props.onToggle(!props.isOn)} onLongPress={() => props.onToggle(!props.isOn)} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.SWITCH} - accessibilityState={{checked: props.isOn}} - aria-checked={props.isOn} + role={CONST.ACCESSIBILITY_ROLE.SWITCH} + ariaChecked={props.isOn} accessibilityLabel={props.accessibilityLabel} // disable hover dim for switch hoverDimmingValue={1} diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js similarity index 100% rename from src/components/TextInput/baseTextInputPropTypes.js rename to src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput/index.js similarity index 92% rename from src/components/TextInput/BaseTextInput.js rename to src/components/TextInput/BaseTextInput/index.js index c9b1944b5b81..e643c6ff6b4f 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput/index.js @@ -10,9 +10,10 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import RNTextInput from '@components/RNTextInput'; import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; +import * as styleConst from '@components/TextInput/styleConst'; +import TextInputLabel from '@components/TextInput/TextInputLabel'; import withLocalize from '@components/withLocalize'; import * as Browser from '@libs/Browser'; -import getSecureEntryKeyboardType from '@libs/getSecureEntryKeyboardType'; import isInputAutoFilled from '@libs/isInputAutoFilled'; import useNativeDriver from '@libs/useNativeDriver'; import styles from '@styles/styles'; @@ -21,8 +22,6 @@ import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import * as baseTextInputPropTypes from './baseTextInputPropTypes'; -import * as styleConst from './styleConst'; -import TextInputLabel from './TextInputLabel'; function BaseTextInput(props) { const initialValue = props.value || props.defaultValue || ''; @@ -214,7 +213,7 @@ function BaseTextInput(props) { // eslint-disable-next-line react/forbid-foreign-prop-types const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); const hasLabel = Boolean(props.label.length); - const isEditable = _.isUndefined(props.editable) ? !props.disabled : props.editable; + const isReadOnly = _.isUndefined(props.readOnly) ? props.disabled : props.readOnly; const inputHelpText = props.errorText || props.hint; const placeholder = props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : null; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; @@ -231,7 +230,7 @@ function BaseTextInput(props) { /* To prevent text jumping caused by virtual DOM calculations on Safari and mobile Chrome, make sure to include the `lineHeight`. Reference: https://github.com/Expensify/App/issues/26735 - + For other platforms, explicitly remove `lineHeight` from single-line inputs to prevent long text from disappearing once it exceeds the input space. See https://github.com/Expensify/App/issues/13802 */ @@ -256,7 +255,7 @@ function BaseTextInput(props) { > {/* Adding this background to the label only for multiline text input, to prevent text overlapping with label when scrolling */} - {isMultiline && ( - - )} + {isMultiline && } ) : null} - + {Boolean(props.prefixCharacter) && ( {props.prefixCharacter} @@ -346,6 +336,7 @@ function BaseTextInput(props) { props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), // Add disabled color theme when field is not editable. props.disabled && styles.textInputDisabled, + styles.pointerEventsAuto, ]} multiline={isMultiline} maxLength={props.maxLength} @@ -355,10 +346,10 @@ function BaseTextInput(props) { secureTextEntry={passwordHidden} onPressOut={props.onPress} showSoftInputOnFocus={!props.disableKeyboard} - keyboardType={getSecureEntryKeyboardType(props.keyboardType, props.secureTextEntry, passwordHidden)} + inputMode={props.inputMode} value={props.value} selection={props.selection} - editable={isEditable} + readOnly={isReadOnly} defaultValue={props.defaultValue} // FormSubmit Enter key handler does not have access to direct props. // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. @@ -385,7 +376,7 @@ function BaseTextInput(props) { )} {!props.secureTextEntry && Boolean(props.icon) && ( - + {/* - Text input component doesn't support auto grow by default. - We're using a hidden text input to achieve that. - This text view is used to calculate width or height of the input value given textStyle in this component. - This Text component is intentionally positioned out of the screen. - */} + Text input component doesn't support auto grow by default. + We're using a hidden text input to achieve that. + This text view is used to calculate width or height of the input value given textStyle in this component. + This Text component is intentionally positioned out of the screen. + */} {(props.autoGrow || props.autoGrowHeight) && ( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value // https://github.com/Expensify/App/issues/8158 diff --git a/src/components/TextInput/BaseTextInput/index.native.js b/src/components/TextInput/BaseTextInput/index.native.js new file mode 100644 index 000000000000..5c3e19a2d94c --- /dev/null +++ b/src/components/TextInput/BaseTextInput/index.native.js @@ -0,0 +1,401 @@ +import Str from 'expensify-common/lib/str'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {ActivityIndicator, Animated, StyleSheet, View} from 'react-native'; +import _ from 'underscore'; +import Checkbox from '@components/Checkbox'; +import FormHelpMessage from '@components/FormHelpMessage'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import RNTextInput from '@components/RNTextInput'; +import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; +import Text from '@components/Text'; +import * as styleConst from '@components/TextInput/styleConst'; +import TextInputLabel from '@components/TextInput/TextInputLabel'; +import withLocalize from '@components/withLocalize'; +import getSecureEntryKeyboardType from '@libs/getSecureEntryKeyboardType'; +import isInputAutoFilled from '@libs/isInputAutoFilled'; +import useNativeDriver from '@libs/useNativeDriver'; +import styles from '@styles/styles'; +import * as StyleUtils from '@styles/StyleUtils'; +import themeColors from '@styles/themes/default'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import * as baseTextInputPropTypes from './baseTextInputPropTypes'; + +function BaseTextInput(props) { + const initialValue = props.value || props.defaultValue || ''; + const initialActiveLabel = props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); + + const [isFocused, setIsFocused] = useState(false); + const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); + const [textInputWidth, setTextInputWidth] = useState(0); + const [textInputHeight, setTextInputHeight] = useState(0); + const [height, setHeight] = useState(variables.componentSizeLarge); + const [width, setWidth] = useState(); + const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; + const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; + + const input = useRef(null); + const isLabelActive = useRef(initialActiveLabel); + + // AutoFocus which only works on mount: + useEffect(() => { + // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514 + if (!props.autoFocus || !input.current) { + return; + } + + if (props.shouldDelayFocus) { + const focusTimeout = setTimeout(() => input.current.focus(), CONST.ANIMATED_TRANSITION); + return () => clearTimeout(focusTimeout); + } + input.current.focus(); + // We only want this to run on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const animateLabel = useCallback( + (translateY, scale) => { + Animated.parallel([ + Animated.spring(labelTranslateY, { + toValue: translateY, + duration: styleConst.LABEL_ANIMATION_DURATION, + useNativeDriver, + }), + Animated.spring(labelScale, { + toValue: scale, + duration: styleConst.LABEL_ANIMATION_DURATION, + useNativeDriver, + }), + ]).start(); + }, + [labelScale, labelTranslateY], + ); + + const activateLabel = useCallback(() => { + const value = props.value || ''; + + if (value.length < 0 || isLabelActive.current) { + return; + } + + animateLabel(styleConst.ACTIVE_LABEL_TRANSLATE_Y, styleConst.ACTIVE_LABEL_SCALE); + isLabelActive.current = true; + }, [animateLabel, props.value]); + + const deactivateLabel = useCallback(() => { + const value = props.value || ''; + + if (props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { + return; + } + + animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); + isLabelActive.current = false; + }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, props.value]); + + const onFocus = (event) => { + if (props.onFocus) { + props.onFocus(event); + } + setIsFocused(true); + }; + + const onBlur = (event) => { + if (props.onBlur) { + props.onBlur(event); + } + setIsFocused(false); + }; + + const onPress = (event) => { + if (props.disabled) { + return; + } + + if (props.onPress) { + props.onPress(event); + } + + if (!event.isDefaultPrevented()) { + input.current.focus(); + } + }; + + const onLayout = useCallback( + (event) => { + if (!props.autoGrowHeight && props.multiline) { + return; + } + + const layout = event.nativeEvent.layout; + + setWidth((prevWidth) => (props.autoGrowHeight ? layout.width : prevWidth)); + setHeight((prevHeight) => (!props.multiline ? layout.height : prevHeight)); + }, + [props.autoGrowHeight, props.multiline], + ); + + // The ref is needed when the component is uncontrolled and we don't have a value prop + const hasValueRef = useRef(initialValue.length > 0); + const inputValue = props.value || ''; + const hasValue = inputValue.length > 0 || hasValueRef.current; + + // Activate or deactivate the label when either focus changes, or for controlled + // components when the value prop changes: + useEffect(() => { + if ( + hasValue || + isFocused || + // If the text has been supplied by Chrome autofill, the value state is not synced with the value + // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. + isInputAutoFilled(input.current) + ) { + activateLabel(); + } else { + deactivateLabel(); + } + }, [activateLabel, deactivateLabel, hasValue, isFocused]); + + // When the value prop gets cleared externally, we need to keep the ref in sync: + useEffect(() => { + // Return early when component uncontrolled, or we still have a value + if (props.value === undefined || !_.isEmpty(props.value)) { + return; + } + hasValueRef.current = false; + }, [props.value]); + + /** + * Set Value & activateLabel + * + * @param {String} value + * @memberof BaseTextInput + */ + const setValue = (value) => { + if (props.onInputChange) { + props.onInputChange(value); + } + + Str.result(props.onChangeText, value); + + if (value && value.length > 0) { + hasValueRef.current = true; + // When the componment is uncontrolled, we need to manually activate the label: + if (props.value === undefined) { + activateLabel(); + } + } else { + hasValueRef.current = false; + } + }; + + const togglePasswordVisibility = useCallback(() => { + setPasswordHidden((prevPasswordHidden) => !prevPasswordHidden); + }, []); + + // When adding a new prefix character, adjust this method to add expected character width. + // This is because character width isn't known before it's rendered to the screen, and once it's rendered, + // it's too late to calculate it's width because the change in padding would cause a visible jump. + // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size + // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, + // this method will produce reliable results. + const getCharacterPadding = (prefix) => { + switch (prefix) { + case CONST.POLICY.ROOM_PREFIX: + return 10; + default: + throw new Error(`Prefix ${prefix} has no padding assigned.`); + } + }; + + // eslint-disable-next-line react/forbid-foreign-prop-types + const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); + const hasLabel = Boolean(props.label.length); + const isReadOnly = _.isUndefined(props.readOnly) ? props.disabled : props.readOnly; + const inputHelpText = props.errorText || props.hint; + const placeholder = props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : null; + const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; + const textInputContainerStyles = StyleSheet.flatten([ + styles.textInputContainer, + ...props.textInputContainerStyles, + props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), + !props.hideFocusedState && isFocused && styles.borderColorFocus, + (props.hasError || props.errorText) && styles.borderColorDanger, + props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, + ]); + const isMultiline = props.multiline || props.autoGrowHeight; + + return ( + <> + + + + {hasLabel ? ( + <> + {/* Adding this background to the label only for multiline text input, + to prevent text overlapping with label when scrolling */} + {isMultiline && } + + + ) : null} + + {Boolean(props.prefixCharacter) && ( + + + {props.prefixCharacter} + + + )} + { + if (typeof props.innerRef === 'function') { + props.innerRef(ref); + } else if (props.innerRef && _.has(props.innerRef, 'current')) { + // eslint-disable-next-line no-param-reassign + props.innerRef.current = ref; + } + input.current = ref; + }} + // eslint-disable-next-line + {...inputProps} + autoCorrect={props.secureTextEntry ? false : props.autoCorrect} + placeholder={placeholder} + placeholderTextColor={themeColors.placeholderText} + underlineColorAndroid="transparent" + style={[ + styles.flex1, + styles.w100, + props.inputStyle, + (!hasLabel || isMultiline) && styles.pv0, + props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), + props.secureTextEntry && styles.secureInput, + + !isMultiline && {height, lineHeight: undefined}, + + // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. + props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), + // Add disabled color theme when field is not editable. + props.disabled && styles.textInputDisabled, + styles.pointerEventsAuto, + ]} + multiline={isMultiline} + maxLength={props.maxLength} + onFocus={onFocus} + onBlur={onBlur} + onChangeText={setValue} + secureTextEntry={passwordHidden} + onPressOut={props.onPress} + showSoftInputOnFocus={!props.disableKeyboard} + keyboardType={getSecureEntryKeyboardType(props.keyboardType, props.secureTextEntry, passwordHidden)} + inputMode={!props.disableKeyboard ? props.inputMode : CONST.INPUT_MODE.NONE} + value={props.value} + selection={props.selection} + readOnly={isReadOnly} + defaultValue={props.defaultValue} + // FormSubmit Enter key handler does not have access to direct props. + // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. + dataSet={{submitOnEnter: isMultiline && props.submitOnEnter}} + /> + {props.isLoading && ( + + )} + {Boolean(props.secureTextEntry) && ( + e.preventDefault()} + accessibilityLabel={props.translate('common.visible')} + > + + + )} + {!props.secureTextEntry && Boolean(props.icon) && ( + + + + )} + + + + {!_.isEmpty(inputHelpText) && ( + + )} + + {/* + Text input component doesn't support auto grow by default. + We're using a hidden text input to achieve that. + This text view is used to calculate width or height of the input value given textStyle in this component. + This Text component is intentionally positioned out of the screen. + */} + {(props.autoGrow || props.autoGrowHeight) && ( + // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value + // https://github.com/Expensify/App/issues/8158 + // https://github.com/Expensify/App/issues/26628 + { + setTextInputWidth(e.nativeEvent.layout.width); + setTextInputHeight(e.nativeEvent.layout.height); + }} + > + {/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */} + {props.value ? `${props.value}${props.value.endsWith('\n') ? '\u200B' : ''}` : props.placeholder} + + )} + + ); +} + +BaseTextInput.displayName = 'BaseTextInput'; +BaseTextInput.propTypes = baseTextInputPropTypes.propTypes; +BaseTextInput.defaultProps = baseTextInputPropTypes.defaultProps; + +export default withLocalize(BaseTextInput); diff --git a/src/components/TextInput/TextInputLabel/index.js b/src/components/TextInput/TextInputLabel/index.js index f777eff039bd..b49635b91d96 100644 --- a/src/components/TextInput/TextInputLabel/index.js +++ b/src/components/TextInput/TextInputLabel/index.js @@ -18,9 +18,8 @@ function TextInputLabel({for: inputId, label, labelTranslateY, labelScale}) { return ( {label} diff --git a/src/components/TextInput/index.js b/src/components/TextInput/index.js index 5f6164d3bc9a..044399ec6e11 100644 --- a/src/components/TextInput/index.js +++ b/src/components/TextInput/index.js @@ -5,7 +5,7 @@ import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; import styles from '@styles/styles'; import BaseTextInput from './BaseTextInput'; -import * as baseTextInputPropTypes from './baseTextInputPropTypes'; +import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes'; import * as styleConst from './styleConst'; function TextInput(props) { diff --git a/src/components/TextInput/index.native.js b/src/components/TextInput/index.native.js index d28824a9977b..a4d0c76337ab 100644 --- a/src/components/TextInput/index.native.js +++ b/src/components/TextInput/index.native.js @@ -2,7 +2,7 @@ import React, {forwardRef, useEffect} from 'react'; import {AppState, Keyboard} from 'react-native'; import styles from '@styles/styles'; import BaseTextInput from './BaseTextInput'; -import * as baseTextInputPropTypes from './baseTextInputPropTypes'; +import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes'; const TextInput = forwardRef((props, ref) => { useEffect(() => { diff --git a/src/components/TextLink.js b/src/components/TextLink.js index 79f3d43a7743..9292ac51e78f 100644 --- a/src/components/TextLink.js +++ b/src/components/TextLink.js @@ -66,7 +66,7 @@ function TextLink(props) { return ( - - - {props.leadingText} - - - - {props.trailingText} - - - ); -} - -TextWithEllipsis.propTypes = propTypes; -TextWithEllipsis.defaultProps = defaultProps; -TextWithEllipsis.displayName = 'TextWithEllipsis'; - -export default TextWithEllipsis; diff --git a/src/components/TextWithEllipsis/index.tsx b/src/components/TextWithEllipsis/index.tsx new file mode 100644 index 000000000000..1afa05309337 --- /dev/null +++ b/src/components/TextWithEllipsis/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import Text from '@components/Text'; +import styles from '@styles/styles'; + +type TextWithEllipsisProps = { + /** Leading text before the ellipsis */ + leadingText: string; + + /** Text after the ellipsis */ + trailingText: string; + + /** Styles for leading and trailing text */ + textStyle?: StyleProp; + + /** Styles for leading text View */ + leadingTextParentStyle?: StyleProp; + + /** Styles for parent View */ + wrapperStyle?: StyleProp; +}; + +function TextWithEllipsis({leadingText, trailingText, textStyle, leadingTextParentStyle, wrapperStyle}: TextWithEllipsisProps) { + return ( + + + + {leadingText} + + + + {trailingText} + + + ); +} + +TextWithEllipsis.displayName = 'TextWithEllipsis'; + +export default TextWithEllipsis; diff --git a/src/components/ThreeDotsMenu/index.js b/src/components/ThreeDotsMenu/index.js index 973aa7e5e189..a1c75a81a92f 100644 --- a/src/components/ThreeDotsMenu/index.js +++ b/src/components/ThreeDotsMenu/index.js @@ -110,7 +110,7 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me }} ref={buttonRef} style={[styles.touchableButtonImage, ...iconStyles]} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={translate(iconTooltip)} > diff --git a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js index ceb10de0f909..589866eecc67 100755 --- a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js +++ b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js @@ -101,7 +101,7 @@ function BaseVideoChatButtonAndMenu(props) { })} style={styles.touchableButtonImage} accessibilityLabel={props.translate('videoChatButtonAndMenu.tooltip')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > { - if (!isScreenTransitionEnded || !isInputInitialized || !inputRef.current) { + if (!isScreenTransitionEnded || !isInputInitialized || !inputRef.current || !isSplashHidden) { return; } inputRef.current.focus(); setIsScreenTransitionEnded(false); - }, [isScreenTransitionEnded, isInputInitialized]); + }, [isScreenTransitionEnded, isInputInitialized, isSplashHidden]); useFocusEffect( useCallback(() => { diff --git a/src/hooks/useSingleExecution.js b/src/hooks/useSingleExecution/index.native.ts similarity index 83% rename from src/hooks/useSingleExecution.js rename to src/hooks/useSingleExecution/index.native.ts index a2b4ccb4cd53..16a98152def1 100644 --- a/src/hooks/useSingleExecution.js +++ b/src/hooks/useSingleExecution/index.native.ts @@ -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/hooks/useSingleExecution/index.ts b/src/hooks/useSingleExecution/index.ts new file mode 100644 index 000000000000..c37087d27c5f --- /dev/null +++ b/src/hooks/useSingleExecution/index.ts @@ -0,0 +1,20 @@ +import {useCallback} from 'react'; + +type Action = (...params: T) => void | Promise; + +/** + * This hook was specifically written for native issue + * more information: https://github.com/Expensify/App/pull/24614 https://github.com/Expensify/App/pull/24173 + * on web we don't need this mechanism so we just call the action directly. + */ +export default function useSingleExecution() { + const singleExecution = useCallback( + (action: Action) => + (...params: T) => { + action(...params); + }, + [], + ); + + return {isExecuting: false, singleExecution}; +} diff --git a/src/languages/en.ts b/src/languages/en.ts index 38efe0ef92f6..d99b3c7d04d1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -592,8 +592,7 @@ export default { genericDeleteFailureMessage: 'Unexpected error deleting the money request, please try again later', genericEditFailureMessage: 'Unexpected error editing the money request, please try again later', genericSmartscanFailureMessage: 'Transaction is missing fields', - duplicateWaypointsErrorMessage: 'Please remove duplicate waypoints', - emptyWaypointsErrorMessage: 'Please enter at least two waypoints', + atLeastTwoDifferentWaypoints: 'Please enter at least two different addresses', splitBillMultipleParticipantsErrorMessage: 'Split bill is only allowed between a single workspace or individual users. Please update your selection.', }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Started settling up, payment is held until ${submitterDisplayName} enables their Wallet`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 2bdb71ae82f7..dea7760a35ce 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -586,8 +586,7 @@ export default { genericDeleteFailureMessage: 'Error inesperado eliminando la solicitud de dinero. Por favor, inténtalo más tarde', genericEditFailureMessage: 'Error inesperado al guardar la solicitud de dinero. Por favor, inténtalo más tarde', genericSmartscanFailureMessage: 'La transacción tiene campos vacíos', - duplicateWaypointsErrorMessage: 'Por favor elimina los puntos de ruta duplicados', - emptyWaypointsErrorMessage: 'Por favor introduce al menos dos puntos de ruta', + atLeastTwoDifferentWaypoints: 'Por favor introduce al menos dos direcciones diferentes', splitBillMultipleParticipantsErrorMessage: 'Solo puedes dividir una cuenta entre un único espacio de trabajo o con usuarios individuales. Por favor actualiza tu selección.', }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`, 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/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 34837135d22d..33ddd77ed8c8 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -155,9 +155,9 @@ function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, sessio const shortcutsOverviewShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SHORTCUTS; const searchShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SEARCH; const chatShortcutConfig = CONST.KEYBOARD_SHORTCUTS.NEW_CHAT; - const shouldGetAllData = isUsingMemoryOnlyKeys || SessionUtils.didUserLogInDuringSession(); const currentUrl = getCurrentUrl(); const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(currentUrl, session.email); + const shouldGetAllData = isUsingMemoryOnlyKeys || SessionUtils.didUserLogInDuringSession() || isLoggingInAsNewUser; // Sign out the current user if we're transitioning with a different user const isTransitioning = currentUrl.includes(ROUTES.TRANSITION_BETWEEN_APPS); if (isLoggingInAsNewUser && isTransitioning) { diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay.js b/src/libs/Navigation/AppNavigator/Navigators/Overlay.js index a36f98076d22..c030b91cf930 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/Overlay.js +++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay.js @@ -27,13 +27,13 @@ function Overlay(props) { style={[styles.draggableTopBar]} onPress={props.onPress} accessibilityLabel={translate('common.close')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} /> diff --git a/src/libs/Navigation/FreezeWrapper.js b/src/libs/Navigation/FreezeWrapper.js index 592d869dc0de..16a353ebddea 100644 --- a/src/libs/Navigation/FreezeWrapper.js +++ b/src/libs/Navigation/FreezeWrapper.js @@ -3,6 +3,7 @@ import lodashFindIndex from 'lodash/findIndex'; import PropTypes from 'prop-types'; import React, {useEffect, useRef, useState} from 'react'; import {Freeze} from 'react-freeze'; +import {InteractionManager} from 'react-native'; const propTypes = { /** Prop to disable freeze */ @@ -35,7 +36,7 @@ function FreezeWrapper(props) { // we don't want to freeze the screen if it's the previous screen because the freeze placeholder // would be visible at the beginning of the back animation then if (navigation.getState().index - screenIndexRef.current > 1) { - setIsScreenBlurred(true); + InteractionManager.runAfterInteractions(() => setIsScreenBlurred(true)); } else { setIsScreenBlurred(false); } diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 38b4823d54c6..5ee177b8f831 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -36,17 +36,19 @@ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string const hasEReceipt = transaction?.hasEReceipt; - if (hasEReceipt) { - return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; - } + if (!Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { + if (hasEReceipt) { + return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; + } - // For local files, we won't have a thumbnail yet - if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) { - return {thumbnail: null, image: path}; - } + // For local files, we won't have a thumbnail yet + if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) { + return {thumbnail: null, image: path}; + } - if (isReceiptImage) { - return {thumbnail: `${path}.1024.jpg`, image: path}; + if (isReceiptImage) { + return {thumbnail: `${path}.1024.jpg`, image: path}; + } } const {fileExtension} = FileUtils.splitExtensionFromFileName(filename) as FileNameAndExtension; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 8d24d98b19e8..f8cd86bd235d 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -822,6 +822,16 @@ function getReport(reportID) { return lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}) || {}; } +/** + * Get the notification preference given a report + * + * @param {Object} report + * @returns {String} + */ +function getReportNotificationPreference(report) { + return lodashGet(report, 'notificationPreference', ''); +} + /** * Returns whether or not the author of the action is this user * @@ -2420,7 +2430,7 @@ function buildOptimisticIOUReport(payeeAccountID, payerAccountID, total, chatRep // We don't translate reportName because the server response is always in English reportName: `${payerEmail} owes ${formattedTotal}`, - notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, parentReportID: chatReportID, }; } @@ -2459,7 +2469,7 @@ function buildOptimisticExpenseReport(chatReportID, policyID, payeeAccountID, to state: CONST.REPORT.STATE.SUBMITTED, stateNum: CONST.REPORT.STATE_NUM.PROCESSING, total: storedTotal, - notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, parentReportID: chatReportID, }; } @@ -3156,7 +3166,7 @@ function buildTransactionThread(reportAction, moneyRequestReportID) { '', undefined, undefined, - CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, reportAction.reportActionID, moneyRequestReportID, ); @@ -4217,6 +4227,7 @@ export { getDisplayNamesStringFromTooltips, getReportName, getReport, + getReportNotificationPreference, getReportIDFromLink, getRouteFromLink, getDeletedParentActionMessageForChatReport, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 2cc63e63e753..00ce8c55dbd7 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -417,6 +417,7 @@ function getValidWaypoints(waypoints: WaypointCollection, reArrangeIndexes = fal } let lastWaypointIndex = -1; + let waypointIndex = -1; return waypointValues.reduce((acc, currentWaypoint, index) => { const previousWaypoint = waypointValues[lastWaypointIndex]; @@ -431,9 +432,10 @@ function getValidWaypoints(waypoints: WaypointCollection, reArrangeIndexes = fal return acc; } - const validatedWaypoints: WaypointCollection = {...acc, [`waypoint${reArrangeIndexes ? lastWaypointIndex + 1 : index}`]: currentWaypoint}; + const validatedWaypoints: WaypointCollection = {...acc, [`waypoint${reArrangeIndexes ? waypointIndex + 1 : index}`]: currentWaypoint}; - lastWaypointIndex += 1; + lastWaypointIndex = index; + waypointIndex += 1; return validatedWaypoints; }, {}); diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 1de15c1184cb..a10e7e01da03 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -328,6 +328,10 @@ function addActions(reportID, text = '', file) { lastReadTime: currentTime, }; + if (ReportUtils.getReportNotificationPreference(ReportUtils.getReport(reportID)) === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { + optimisticReport.notificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS; + } + // Optimistically add the new actions to the store before waiting to save them to the server const optimisticReportActions = {}; if (text) { @@ -2006,7 +2010,7 @@ function openReportFromDeepLink(url, isAuthenticated) { }); return; } - Navigation.navigate(route, CONST.NAVIGATION.TYPE.PUSH); + Navigation.navigate(route, CONST.NAVIGATION.ACTION_TYPE.PUSH); }); }); } diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay.ts index 94eb168328aa..19f1050d24bd 100644 --- a/src/libs/focusComposerWithDelay.ts +++ b/src/libs/focusComposerWithDelay.ts @@ -1,4 +1,4 @@ -import {InteractionManager, TextInput} from 'react-native'; +import {TextInput} from 'react-native'; import * as EmojiPickerAction from './actions/EmojiPickerAction'; import ComposerFocusManager from './ComposerFocusManager'; @@ -14,21 +14,19 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD return (shouldDelay = false) => { // There could be other animations running while we trigger manual focus. // This prevents focus from making those animations janky. - InteractionManager.runAfterInteractions(() => { - if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) { - return; - } + if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) { + return; + } - if (!shouldDelay) { - textInput.focus(); + if (!shouldDelay) { + textInput.focus(); + return; + } + ComposerFocusManager.isReadyToFocus().then(() => { + if (!textInput) { return; } - ComposerFocusManager.isReadyToFocus().then(() => { - if (!textInput) { - return; - } - textInput.focus(); - }); + textInput.focus(); }); }; } 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/libs/setShouldShowComposeInputKeyboardAware/index.android.ts b/src/libs/setShouldShowComposeInputKeyboardAware/index.android.ts new file mode 100644 index 000000000000..68c750b05a5f --- /dev/null +++ b/src/libs/setShouldShowComposeInputKeyboardAware/index.android.ts @@ -0,0 +1,3 @@ +import setShouldShowComposeInputKeyboardAwareBuilder from './setShouldShowComposeInputKeyboardAwareBuilder'; + +export default setShouldShowComposeInputKeyboardAwareBuilder('keyboardDidHide'); diff --git a/src/libs/setShouldShowComposeInputKeyboardAware/index.ios.ts b/src/libs/setShouldShowComposeInputKeyboardAware/index.ios.ts new file mode 100644 index 000000000000..cd50938c70b9 --- /dev/null +++ b/src/libs/setShouldShowComposeInputKeyboardAware/index.ios.ts @@ -0,0 +1,5 @@ +import setShouldShowComposeInputKeyboardAwareBuilder from './setShouldShowComposeInputKeyboardAwareBuilder'; + +// On iOS, there is a visible delay in displaying input after the keyboard has been closed with the `keyboardDidHide` event +// Because of that - on iOS we can use `keyboardWillHide` that is not available on android +export default setShouldShowComposeInputKeyboardAwareBuilder('keyboardWillHide'); diff --git a/src/libs/setShouldShowComposeInputKeyboardAware/index.native.ts b/src/libs/setShouldShowComposeInputKeyboardAware/index.native.ts deleted file mode 100644 index dbfa0c6977b3..000000000000 --- a/src/libs/setShouldShowComposeInputKeyboardAware/index.native.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {EmitterSubscription, Keyboard} from 'react-native'; -import * as Composer from '@userActions/Composer'; -import SetShouldShowComposeInputKeyboardAware from './types'; - -let keyboardDidHideListener: EmitterSubscription | null = null; -const setShouldShowComposeInputKeyboardAware: SetShouldShowComposeInputKeyboardAware = (shouldShow) => { - if (keyboardDidHideListener) { - keyboardDidHideListener.remove(); - keyboardDidHideListener = null; - } - - if (!shouldShow) { - Composer.setShouldShowComposeInput(false); - return; - } - - // If keyboard is already hidden, we should show composer immediately because keyboardDidHide event won't be called - if (!Keyboard.isVisible()) { - Composer.setShouldShowComposeInput(true); - return; - } - - keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => { - Composer.setShouldShowComposeInput(true); - keyboardDidHideListener?.remove(); - }); -}; - -export default setShouldShowComposeInputKeyboardAware; diff --git a/src/libs/setShouldShowComposeInputKeyboardAware/setShouldShowComposeInputKeyboardAwareBuilder.ts b/src/libs/setShouldShowComposeInputKeyboardAware/setShouldShowComposeInputKeyboardAwareBuilder.ts new file mode 100644 index 000000000000..528b71c45ab8 --- /dev/null +++ b/src/libs/setShouldShowComposeInputKeyboardAware/setShouldShowComposeInputKeyboardAwareBuilder.ts @@ -0,0 +1,34 @@ +import {EmitterSubscription, Keyboard} from 'react-native'; +import {KeyboardEventName} from 'react-native/Libraries/Components/Keyboard/Keyboard'; +import * as Composer from '@userActions/Composer'; +import SetShouldShowComposeInputKeyboardAware from './types'; + +let keyboardEventListener: EmitterSubscription | null = null; +// On iOS, there is a visible delay in displaying input after the keyboard has been closed with the `keyboardDidHide` event +// Because of that - on iOS we can use `keyboardWillHide` that is not available on android + +const setShouldShowComposeInputKeyboardAwareBuilder: (keyboardEvent: KeyboardEventName) => SetShouldShowComposeInputKeyboardAware = + (keyboardEvent: KeyboardEventName) => (shouldShow: boolean) => { + if (keyboardEventListener) { + keyboardEventListener.remove(); + keyboardEventListener = null; + } + + if (!shouldShow) { + Composer.setShouldShowComposeInput(false); + return; + } + + // If keyboard is already hidden, we should show composer immediately because keyboardDidHide event won't be called + if (!Keyboard.isVisible()) { + Composer.setShouldShowComposeInput(true); + return; + } + + keyboardEventListener = Keyboard.addListener(keyboardEvent, () => { + Composer.setShouldShowComposeInput(true); + keyboardEventListener?.remove(); + }); + }; + +export default setShouldShowComposeInputKeyboardAwareBuilder; diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index 7f01256cc024..5dcdc41afc6d 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -126,10 +126,7 @@ function DetailsPage(props) { - + {details ? ( @@ -144,7 +141,7 @@ function DetailsPage(props) { style={[styles.noOutline]} onPress={show} accessibilityLabel={props.translate('common.details')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} > { if (!el) { return; @@ -75,8 +75,7 @@ function EditRequestDescriptionPage({defaultDescription, onSubmit}) { updateMultilineInputRange(descriptionInputRef.current); }} autoGrowHeight - containerStyles={[styles.autoGrowHeightMultilineInput]} - textAlignVertical="top" + containerStyles={[styles.autoGrowHeightMultilineInput, styles.verticalAlignTop]} submitOnEnter={!Browser.isMobile()} /> diff --git a/src/pages/EditRequestMerchantPage.js b/src/pages/EditRequestMerchantPage.js index e64fd121b4ff..af9b5c9a539e 100644 --- a/src/pages/EditRequestMerchantPage.js +++ b/src/pages/EditRequestMerchantPage.js @@ -56,7 +56,7 @@ function EditRequestMerchantPage({defaultMerchant, onSubmit}) { defaultValue={defaultMerchant} label={translate('common.merchant')} accessibilityLabel={translate('common.merchant')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} ref={(e) => (merchantInputRef.current = e)} /> diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index f1813062d0d7..e58d45b5f1c4 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -190,7 +190,7 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP containerStyles={[styles.mt4]} label={translate(fieldNameTranslationKeys.legalFirstName)} accessibilityLabel={translate(fieldNameTranslationKeys.legalFirstName)} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} defaultValue={PersonalDetails.extractFirstAndLastNameFromAvailableDetails(currentUserPersonalDetails).firstName} shouldSaveDraft /> @@ -199,7 +199,7 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP containerStyles={[styles.mt4]} label={translate(fieldNameTranslationKeys.legalLastName)} accessibilityLabel={translate(fieldNameTranslationKeys.legalLastName)} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} defaultValue={PersonalDetails.extractFirstAndLastNameFromAvailableDetails(currentUserPersonalDetails).lastName} shouldSaveDraft /> @@ -217,10 +217,10 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js index f8b041951016..22af82041f7b 100755 --- a/src/pages/NewChatPage.js +++ b/src/pages/NewChatPage.js @@ -9,7 +9,7 @@ import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useDelayedInputFocus from '@hooks/useDelayedInputFocus'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useNetwork from '@hooks/useNetwork'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; @@ -53,7 +53,6 @@ const defaultProps = { const excludedGroupEmails = _.without(CONST.EXPENSIFY_EMAILS, CONST.EMAIL.CONCIERGE); function NewChatPage({betas, isGroupChat, personalDetails, reports, translate, isSearchingForReports}) { - const optionSelectorRef = React.createRef(null); const [searchTerm, setSearchTerm] = useState(''); const [filteredRecentReports, setFilteredRecentReports] = useState([]); const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]); @@ -216,7 +215,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, translate, i setSearchTerm(text); }, []); - useDelayedInputFocus(optionSelectorRef, 600); + const {inputCallbackRef} = useAutoFocusInput(); return ( 0 ? safeAreaPaddingBottomStyle : {}]}> {isSmallScreenWidth && } diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.js b/src/pages/PrivateNotes/PrivateNotesEditPage.js index e3c2c7170ceb..f38dabee9183 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.js +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.js @@ -144,7 +144,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { > Navigation.goBack(navigateBackTo)} /> - + {hasMinimumDetails && ( @@ -172,7 +169,7 @@ function ProfilePage(props) { style={[styles.noOutline]} onPress={show} accessibilityLabel={props.translate('common.profile')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} > props.onFieldChange({city: value})} @@ -138,8 +138,8 @@ function AddressForm(props) { shouldSaveDraft={props.shouldSaveDraft} label={props.translate('common.zip')} accessibilityLabel={props.translate('common.zip')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + role={CONST.ACCESSIBILITY_ROLE.TEXT} + inputMode={CONST.INPUT_MODE.NUMERIC} value={props.values.zipCode} defaultValue={props.defaultValues.zipCode} onChangeText={(value) => props.onFieldChange({zipCode: value})} diff --git a/src/pages/ReimbursementAccount/BankAccountManualStep.js b/src/pages/ReimbursementAccount/BankAccountManualStep.js index 13155d286a5e..1612238ed8d9 100644 --- a/src/pages/ReimbursementAccount/BankAccountManualStep.js +++ b/src/pages/ReimbursementAccount/BankAccountManualStep.js @@ -102,10 +102,10 @@ function BankAccountManualStep(props) { shouldDelayFocus={shouldDelayFocus} inputID="routingNumber" label={translate('bankAccount.routingNumber')} - accessibilityLabel={translate('bankAccount.routingNumber')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + aria-label={translate('bankAccount.routingNumber')} + role={CONST.ACCESSIBILITY_ROLE.TEXT} defaultValue={props.getDefaultStateForField('routingNumber', '')} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + inputMode={CONST.INPUT_MODE.NUMERIC} disabled={shouldDisableInputs} shouldSaveDraft shouldUseDefaultValue={shouldDisableInputs} @@ -114,16 +114,16 @@ function BankAccountManualStep(props) { inputID="accountNumber" containerStyles={[styles.mt4]} label={translate('bankAccount.accountNumber')} - accessibilityLabel={translate('bankAccount.accountNumber')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + aria-label={translate('bankAccount.accountNumber')} + role={CONST.ACCESSIBILITY_ROLE.TEXT} defaultValue={props.getDefaultStateForField('accountNumber', '')} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + inputMode={CONST.INPUT_MODE.NUMERIC} disabled={shouldDisableInputs} shouldSaveDraft shouldUseDefaultValue={shouldDisableInputs} /> ( diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 24cfbf5ae4c6..41f73d1ebf8e 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -163,7 +163,7 @@ function CompanyStep({reimbursementAccount, reimbursementAccountDraft, getDefaul props.onFieldChange({firstName: value})} @@ -158,8 +158,8 @@ function IdentityForm(props) { inputID={props.inputKeys.lastName} shouldSaveDraft={props.shouldSaveDraft} label={`${props.translate('common.lastName')}`} - accessibilityLabel={props.translate('common.lastName')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + aria-label={props.translate('common.lastName')} + role={CONST.ACCESSIBILITY_ROLE.TEXT} value={props.values.lastName} defaultValue={props.defaultValues.lastName} onChangeText={(value) => props.onFieldChange({lastName: value})} @@ -183,10 +183,10 @@ function IdentityForm(props) { inputID={props.inputKeys.ssnLast4} shouldSaveDraft={props.shouldSaveDraft} label={`${props.translate('common.ssnLast4')}`} - accessibilityLabel={props.translate('common.ssnLast4')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + aria-label={props.translate('common.ssnLast4')} + role={CONST.ACCESSIBILITY_ROLE.TEXT} containerStyles={[styles.mt4]} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + inputMode={CONST.INPUT_MODE.NUMERIC} defaultValue={props.defaultValues.ssnLast4} onChangeText={(value) => props.onFieldChange({ssnLast4: value})} errorText={props.errors.ssnLast4 ? props.translate('bankAccount.error.ssnLast4') : ''} diff --git a/src/pages/ReimbursementAccount/RequestorStep.js b/src/pages/ReimbursementAccount/RequestorStep.js index 0eddb727d56d..4a3dd2f0917f 100644 --- a/src/pages/ReimbursementAccount/RequestorStep.js +++ b/src/pages/ReimbursementAccount/RequestorStep.js @@ -70,7 +70,11 @@ const validate = (values) => { return errors; }; -function RequestorStep({reimbursementAccount, shouldShowOnfido, onBackButtonPress, getDefaultStateForField}) { +/** + * Workaround for forwardRef + propTypes issue. + * See https://stackoverflow.com/questions/59716140/using-forwardref-with-proptypes-and-eslint + */ +const RequestorStep = React.forwardRef(({reimbursementAccount, shouldShowOnfido, onBackButtonPress, getDefaultStateForField}, ref) => { const {translate} = useLocalize(); const defaultValues = useMemo( @@ -108,6 +112,7 @@ function RequestorStep({reimbursementAccount, shouldShowOnfido, onBackButtonPres if (shouldShowOnfido) { return ( @@ -116,6 +121,7 @@ function RequestorStep({reimbursementAccount, shouldShowOnfido, onBackButtonPres return ( @@ -190,9 +196,9 @@ function RequestorStep({reimbursementAccount, shouldShowOnfido, onBackButtonPres ); -} +}); RequestorStep.propTypes = propTypes; RequestorStep.displayName = 'RequestorStep'; -export default React.forwardRef(RequestorStep); +export default RequestorStep; diff --git a/src/pages/ReimbursementAccount/ValidationStep.js b/src/pages/ReimbursementAccount/ValidationStep.js index 5a0149aa3ba4..343f98644766 100644 --- a/src/pages/ReimbursementAccount/ValidationStep.js +++ b/src/pages/ReimbursementAccount/ValidationStep.js @@ -157,24 +157,24 @@ function ValidationStep({reimbursementAccount, translate, onBackButtonPress, acc shouldSaveDraft containerStyles={[styles.mb1]} placeholder="1.52" - keyboardType="decimal-pad" - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + inputMode={CONST.INPUT_MODE.DECIMAL} + role={CONST.ACCESSIBILITY_ROLE.TEXT} /> {!requiresTwoFactorAuth && ( diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index ef28102cc144..de25fdc3a081 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -215,7 +215,7 @@ function ReportDetailsPage(props) { {isPolicyAdmin ? ( { Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(props.report.policyID)); diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index c2179c53126b..1ae6942c6412 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -106,10 +106,7 @@ function ReportParticipantsPage(props) { : 'common.details', )} /> - + {Boolean(participants.length) && ( { @@ -111,7 +111,7 @@ function ReportWelcomeMessagePage(props) { value={welcomeMessage} onChangeText={handleWelcomeMessageChange} autoCapitalize="none" - textAlignVertical="top" + inputStyle={[styles.verticalAlignTop]} containerStyles={[styles.autoGrowHeightMultilineInput]} /> diff --git a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js index 7b84c5bc94d1..16389d69053d 100644 --- a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js +++ b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js @@ -106,7 +106,7 @@ function IntroSchoolPrincipalPage(props) { name="firstName" label={translate('teachersUnitePage.principalFirstName')} accessibilityLabel={translate('teachersUnitePage.principalFirstName')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} maxLength={CONST.DISPLAY_NAME.MAX_LENGTH} autoCapitalize="words" /> @@ -118,7 +118,7 @@ function IntroSchoolPrincipalPage(props) { name="lastName" label={translate('teachersUnitePage.principalLastName')} accessibilityLabel={translate('teachersUnitePage.principalLastName')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} maxLength={CONST.DISPLAY_NAME.MAX_LENGTH} autoCapitalize="words" /> @@ -130,8 +130,8 @@ function IntroSchoolPrincipalPage(props) { name="partnerUserID" label={translate('teachersUnitePage.principalWorkEmail')} accessibilityLabel={translate('teachersUnitePage.principalWorkEmail')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} - keyboardType={CONST.KEYBOARD_TYPE.EMAIL_ADDRESS} + role={CONST.ACCESSIBILITY_ROLE.TEXT} + inputMode={CONST.INPUT_MODE.EMAIL} autoCapitalize="none" /> diff --git a/src/pages/TeachersUnite/KnowATeacherPage.js b/src/pages/TeachersUnite/KnowATeacherPage.js index c9d429a14596..696a9ef8b704 100644 --- a/src/pages/TeachersUnite/KnowATeacherPage.js +++ b/src/pages/TeachersUnite/KnowATeacherPage.js @@ -116,7 +116,7 @@ function KnowATeacherPage(props) { name="fname" label={translate('common.firstName')} accessibilityLabel={translate('common.firstName')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} maxLength={CONST.DISPLAY_NAME.MAX_LENGTH} autoCapitalize="words" /> @@ -128,7 +128,7 @@ function KnowATeacherPage(props) { name="lname" label={translate('common.lastName')} accessibilityLabel={translate('common.lastName')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} maxLength={CONST.DISPLAY_NAME.MAX_LENGTH} autoCapitalize="words" /> @@ -140,8 +140,8 @@ function KnowATeacherPage(props) { name="partnerUserID" label={`${translate('common.email')}/${translate('common.phoneNumber')}`} accessibilityLabel={`${translate('common.email')}/${translate('common.phoneNumber')}`} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} - keyboardType={CONST.KEYBOARD_TYPE.EMAIL_ADDRESS} + role={CONST.ACCESSIBILITY_ROLE.TEXT} + inputMode={CONST.INPUT_MODE.EMAIL} autoCapitalize="none" /> diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 36c48bd48fd2..27ee820de02f 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -184,7 +184,7 @@ function HeaderView(props) { style={[styles.LHNToggle]} accessibilityHint={props.translate('accessibilityHints.navigateToChatsList')} accessibilityLabel={props.translate('common.back')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > {shouldShowSubscript ? ( {translate('newMessages')} diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js index a31e718933ea..a8ecff7c8d82 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js @@ -208,7 +208,7 @@ function AttachmentPickerWithMenuItems({ onMouseDown={(e) => e.preventDefault()} style={styles.composerSizeButton} disabled={isBlockedFromConcierge || disabled} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={translate('reportActionCompose.collapse')} > @@ -227,7 +227,7 @@ function AttachmentPickerWithMenuItems({ onMouseDown={(e) => e.preventDefault()} style={styles.composerSizeButton} disabled={isBlockedFromConcierge || disabled} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={translate('reportActionCompose.expand')} > @@ -247,7 +247,7 @@ function AttachmentPickerWithMenuItems({ }} style={styles.composerSizeButton} disabled={isBlockedFromConcierge || disabled} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={translate('reportActionCompose.addAction')} > diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 6c661992fe20..c61633618d9f 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -588,12 +588,11 @@ function ComposerWithSuggestions({ autoFocus={shouldAutoFocus} multiline ref={setTextInputRef} - textAlignVertical="top" placeholder={inputPlaceholder} placeholderTextColor={themeColors.placeholderText} onChangeText={(commentValue) => updateComment(commentValue, true)} onKeyPress={triggerHotkeyActions} - style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.flex4]} + style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.flex4, styles.verticalAlignTop]} maxLines={maxComposerLines} onFocus={onFocus} onBlur={onBlur} diff --git a/src/pages/home/report/ReportActionCompose/SendButton.js b/src/pages/home/report/ReportActionCompose/SendButton.js index 061251d13c01..41f35b0f8d3d 100644 --- a/src/pages/home/report/ReportActionCompose/SendButton.js +++ b/src/pages/home/report/ReportActionCompose/SendButton.js @@ -42,7 +42,7 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}) { isDisabledProp || pressed || isDisabled ? undefined : styles.buttonSuccess, isDisabledProp ? styles.cursorDisabled : undefined, ]} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={translate('common.send')} > {({pressed}) => ( diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index c91a29a37eec..24e48eb3e7d0 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -442,8 +442,7 @@ function ReportActionItem(props) { onPress={() => updateHiddenState(!isHidden)} > {isHidden ? props.translate('moderation.revealMessage') : props.translate('moderation.hideMessage')} @@ -656,8 +655,8 @@ function ReportActionItem(props) { const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; return ( props.isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onSecondaryInteraction={showPopover} diff --git a/src/pages/home/report/ReportActionItemCreated.js b/src/pages/home/report/ReportActionItemCreated.js index 10ebb13302b2..a7772ad5e0fb 100644 --- a/src/pages/home/report/ReportActionItemCreated.js +++ b/src/pages/home/report/ReportActionItemCreated.js @@ -74,7 +74,7 @@ function ReportActionItemCreated(props) { onPress={() => ReportUtils.navigateToDetailsPage(props.report)} style={[styles.mh5, styles.mb3, styles.alignSelfStart]} accessibilityLabel={props.translate('common.details')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} disabled={shouldDisableDetailPage} > {convertToLTR(props.iouMessage || text)} {Boolean(props.fragment.isEdited) && ( <> diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 2acb624e28ef..db453ca38265 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -386,7 +386,7 @@ function ReportActionItemMessageEdit(props) { focus(true)} onEmojiSelected={addEmojiToTextBox} - nativeID={emojiButtonID} + id={emojiButtonID} emojiPickerID={props.action.reportActionID} /> @@ -454,7 +454,7 @@ function ReportActionItemMessageEdit(props) { style={[styles.chatItemSubmitButton, hasExceededMaxCommentLength ? {} : styles.buttonSuccess]} onPress={publishDraft} disabled={hasExceededMaxCommentLength} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={translate('common.saveChanges')} hoverDimmingValue={1} pressDimmingValue={0.2} diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index e9e1ef39e417..955e024bd7a8 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -219,7 +219,7 @@ function ReportActionItemSingle(props) { onPress={showActorDetails} disabled={shouldDisableDetailPage} accessibilityLabel={actorHint} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > {getAvatar()} @@ -233,7 +233,7 @@ function ReportActionItemSingle(props) { onPress={showActorDetails} disabled={shouldDisableDetailPage} accessibilityLabel={actorHint} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > {_.map(personArray, (fragment, index) => ( { Report.navigateToAndOpenChildReport(props.childReportID); }} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={`${props.numberOfReplies} ${replyText}`} onSecondaryInteraction={props.onSecondaryInteraction} > @@ -60,16 +60,14 @@ function ReportActionItemThread(props) { /> {`${numberOfRepliesText} ${replyText}`} {timeStamp} diff --git a/src/pages/home/sidebar/AvatarWithOptionalStatus.js b/src/pages/home/sidebar/AvatarWithOptionalStatus.js index b456788224fb..300a898b9e90 100644 --- a/src/pages/home/sidebar/AvatarWithOptionalStatus.js +++ b/src/pages/home/sidebar/AvatarWithOptionalStatus.js @@ -41,7 +41,7 @@ function AvatarWithOptionalStatus({emojiStatus, isCreateMenuOpen}) { diff --git a/src/pages/home/sidebar/GlobalNavigation/GlobalNavigationMenuItem.js b/src/pages/home/sidebar/GlobalNavigation/GlobalNavigationMenuItem.js index 93355fcfd530..5c28681a6cfa 100644 --- a/src/pages/home/sidebar/GlobalNavigation/GlobalNavigationMenuItem.js +++ b/src/pages/home/sidebar/GlobalNavigation/GlobalNavigationMenuItem.js @@ -35,7 +35,7 @@ const GlobalNavigationMenuItem = React.forwardRef(({icon, title, isFocused, onPr onPress={() => !isFocused && onPress()} style={styles.globalNavigationItemContainer} ref={ref} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.MENUITEM} + role={CONST.ACCESSIBILITY_ROLE.MENUITEM} accessibilityLabel={title} > {({pressed}) => ( diff --git a/src/pages/home/sidebar/PressableAvatarWithIndicator.js b/src/pages/home/sidebar/PressableAvatarWithIndicator.js index e798bece339f..58703e49dae4 100644 --- a/src/pages/home/sidebar/PressableAvatarWithIndicator.js +++ b/src/pages/home/sidebar/PressableAvatarWithIndicator.js @@ -45,7 +45,7 @@ function PressableAvatarWithIndicator({isCreateMenuOpen, currentUserPersonalDeta return ( diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 0e7d6aa38545..1f5a07194732 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -148,13 +148,13 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority >
{translate('globalNavigationOptions.chats')}} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} shouldShowEnvironmentBadge /> diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index cca91a45e36f..57f31a8c3e9f 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -238,7 +238,7 @@ function FloatingActionButtonAndPopover(props) { /> { diff --git a/src/pages/home/sidebar/SignInButton.js b/src/pages/home/sidebar/SignInButton.js index 9e8722e2e083..afa67bdc04cd 100644 --- a/src/pages/home/sidebar/SignInButton.js +++ b/src/pages/home/sidebar/SignInButton.js @@ -14,7 +14,7 @@ function SignInButton() { return ( diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index 425aa313a468..3b52c2ae711c 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -131,7 +131,7 @@ function MoneyRequestDescriptionPage({iou, route, selectedTab}) { defaultValue={iou.comment} label={translate('moneyRequestConfirmationList.whatsItFor')} accessibilityLabel={translate('moneyRequestConfirmationList.whatsItFor')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} ref={(el) => { if (!el) { return; @@ -140,8 +140,7 @@ function MoneyRequestDescriptionPage({iou, route, selectedTab}) { updateMultilineInputRange(inputRef.current); }} autoGrowHeight - containerStyles={[styles.autoGrowHeightMultilineInput]} - textAlignVertical="top" + containerStyles={[styles.autoGrowHeightMultilineInput, styles.verticalAlignTop]} submitOnEnter={!Browser.isMobile()} /> diff --git a/src/pages/iou/MoneyRequestMerchantPage.js b/src/pages/iou/MoneyRequestMerchantPage.js index f072c0f78535..4a609f013d95 100644 --- a/src/pages/iou/MoneyRequestMerchantPage.js +++ b/src/pages/iou/MoneyRequestMerchantPage.js @@ -115,7 +115,7 @@ function MoneyRequestMerchantPage({iou, route}) { maxLength={CONST.MERCHANT_NAME_MAX_LENGTH} label={translate('common.merchant')} accessibilityLabel={translate('common.merchant')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} ref={inputCallbackRef} /> diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index b6a15d69f02c..d1dc3682e6c9 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -208,7 +208,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { {({openPicker}) => ( { openPicker({ onPicked: (file) => { @@ -227,7 +227,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { )} {({openPicker}) => ( { @@ -258,7 +258,7 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator, s )} - + {isScanning && ( } nativeIds + * @param {Array} ids */ - const onMouseDown = (event, nativeIds) => { + const onMouseDown = (event, ids) => { const relatedTargetId = lodashGet(event, 'nativeEvent.target.id'); - if (!_.contains(nativeIds, relatedTargetId)) { + if (!_.contains(ids, relatedTargetId)) { return; } event.preventDefault(); @@ -240,7 +240,7 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu return ( onMouseDown(event, [AMOUNT_VIEW_ID])} style={[styles.flex1, styles.flexRow, styles.w100, styles.alignItemsCenter, styles.justifyContentCenter]} > @@ -279,11 +279,11 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu onMouseDown(event, [NUM_PAD_CONTAINER_VIEW_ID, NUM_PAD_VIEW_ID])} style={[styles.w100, styles.justifyContentEnd, styles.pageWrapper, styles.pt0]} - nativeID={NUM_PAD_CONTAINER_VIEW_ID} + id={NUM_PAD_CONTAINER_VIEW_ID} > {canUseTouchScreen ? ( diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.js index 8e9618036861..b09117719a8c 100644 --- a/src/pages/settings/AboutPage/AboutPage.js +++ b/src/pages/settings/AboutPage/AboutPage.js @@ -112,10 +112,7 @@ function AboutPage(props) { height={80} width={80} /> - + v{Environment.isInternalTestBuild() ? `${pkg.version} PR:${CONST.PULL_REQUEST_NUMBER}${getFlavor()}` : `${pkg.version}${getFlavor()}`} {props.translate('initialSettingsPage.aboutPage.description')} diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 8fb752d2a574..207c006a31c2 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -344,7 +344,7 @@ function InitialSettingsPage(props) { disabled={isExecuting} onPress={singleExecution(openProfileSettings)} accessibilityLabel={translate('common.profile')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > (loginInputRef.current = el)} inputID="phoneOrEmail" autoCapitalize="none" - returnKeyType="go" + enterKeyHint="done" maxLength={CONST.LOGIN_CHARACTER_LIMIT} /> diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js index 26f66e3f0294..6fbbe770591b 100644 --- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js @@ -192,7 +192,7 @@ function BaseValidateCodeForm(props) { underlayColor={themeColors.componentBG} hoverDimmingValue={1} pressDimmingValue={0.2} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={props.translate('validateCodeForm.magicCodeNotReceived')} > {props.translate('validateCodeForm.magicCodeNotReceived')} diff --git a/src/pages/settings/Profile/CustomStatus/StatusSetPage.js b/src/pages/settings/Profile/CustomStatus/StatusSetPage.js index 1d26c0e6dec4..ffe2d06b304a 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusSetPage.js +++ b/src/pages/settings/Profile/CustomStatus/StatusSetPage.js @@ -66,7 +66,7 @@ function StatusSetPage({draftStatus, currentUserPersonalDetails}) { @@ -74,8 +74,8 @@ function StatusSetPage({draftStatus, currentUserPersonalDetails}) { InputComponent={TextInput} inputID={INPUT_IDS.STATUS_TEXT} label={translate('statusPage.message')} - accessibilityLabel={INPUT_IDS.STATUS_TEXT} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + aria-label={INPUT_IDS.STATUS_TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} defaultValue={defaultText} maxLength={CONST.STATUS_TEXT_MAX_LENGTH} autoFocus diff --git a/src/pages/settings/Profile/DisplayNamePage.js b/src/pages/settings/Profile/DisplayNamePage.js index 379b5f225310..a7d87e08789f 100644 --- a/src/pages/settings/Profile/DisplayNamePage.js +++ b/src/pages/settings/Profile/DisplayNamePage.js @@ -100,8 +100,8 @@ function DisplayNamePage(props) { inputID="firstName" name="fname" label={props.translate('common.firstName')} - accessibilityLabel={props.translate('common.firstName')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + aria-label={props.translate('common.firstName')} + role={CONST.ACCESSIBILITY_ROLE.TEXT} defaultValue={lodashGet(currentUserDetails, 'firstName', '')} maxLength={CONST.DISPLAY_NAME.MAX_LENGTH} spellCheck={false} @@ -113,8 +113,8 @@ function DisplayNamePage(props) { inputID="lastName" name="lname" label={props.translate('common.lastName')} - accessibilityLabel={props.translate('common.lastName')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + aria-label={props.translate('common.lastName')} + role={CONST.ACCESSIBILITY_ROLE.TEXT} defaultValue={lodashGet(currentUserDetails, 'lastName', '')} maxLength={CONST.DISPLAY_NAME.MAX_LENGTH} spellCheck={false} diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index e44b00920544..b86d646794bd 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -212,8 +212,8 @@ function AddressPage({privatePersonalDetails, route}) { @@ -120,11 +120,11 @@ function CloseAccountPage(props) { inputID="phoneOrEmail" autoCapitalize="none" label={props.translate('closeAccountPage.enterDefaultContact')} - accessibilityLabel={props.translate('closeAccountPage.enterDefaultContact')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + aria-label={props.translate('closeAccountPage.enterDefaultContact')} + role={CONST.ACCESSIBILITY_ROLE.TEXT} containerStyles={[styles.mt5]} autoCorrect={false} - keyboardType={Str.isValidEmail(userEmailOrPhone) ? CONST.KEYBOARD_TYPE.EMAIL_ADDRESS : CONST.KEYBOARD_TYPE.DEFAULT} + inputMode={Str.isValidEmail(userEmailOrPhone) ? CONST.INPUT_MODE.EMAIL : CONST.INPUT_MODE.TEXT} /> (nameOnCardRef.current = ref)} spellCheck={false} /> @@ -157,10 +157,10 @@ function DebitCardPage(props) { @@ -177,9 +177,9 @@ function DebitCardPage(props) { {translate('paymentMethodList.addFirstPaymentMethod')}; + const renderListEmptyComponent = () => {translate('paymentMethodList.addFirstPaymentMethod')}; const renderListFooterComponent = useCallback( () => ( diff --git a/src/pages/signin/ChangeExpensifyLoginLink.js b/src/pages/signin/ChangeExpensifyLoginLink.js index f5e526964333..d776dafc1aa6 100755 --- a/src/pages/signin/ChangeExpensifyLoginLink.js +++ b/src/pages/signin/ChangeExpensifyLoginLink.js @@ -37,7 +37,7 @@ function ChangeExpensifyLoginLink(props) { diff --git a/src/pages/signin/EmailDeliveryFailurePage.js b/src/pages/signin/EmailDeliveryFailurePage.js index a7b690a6151a..7bbfe7d52ec5 100644 --- a/src/pages/signin/EmailDeliveryFailurePage.js +++ b/src/pages/signin/EmailDeliveryFailurePage.js @@ -75,7 +75,7 @@ function EmailDeliveryFailurePage(props) { redirectToSignIn()} - accessibilityRole="button" + role="button" accessibilityLabel={translate('common.back')} // disable hover dim for switch hoverDimmingValue={1} diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index 2c96de33557e..b239ce6d3a86 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -221,18 +221,18 @@ function LoginForm(props) { ref={input} label={translate('loginForm.phoneOrEmail')} accessibilityLabel={translate('loginForm.phoneOrEmail')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} value={login} returnKeyType="go" autoCompleteType="username" textContentType="username" - nativeID="username" + id="username" name="username" onChangeText={onTextInput} onSubmitEditing={validateAndSubmitForm} autoCapitalize="none" autoCorrect={false} - keyboardType={CONST.KEYBOARD_TYPE.EMAIL_ADDRESS} + inputMode={CONST.INPUT_MODE.EMAIL} errorText={formErrorText} hasError={hasError} maxLength={CONST.LOGIN_CHARACTER_LIMIT} diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.js b/src/pages/signin/SignInPageLayout/BackgroundImage/index.js index 2dc95bd28215..b0022c32c565 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.js +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.js @@ -18,13 +18,11 @@ const propTypes = { function BackgroundImage(props) { return props.isSmallScreen ? ( ) : ( diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 2d8f0e98e03c..351a32dfca48 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -341,7 +341,7 @@ function BaseValidateCodeForm(props) { hoverDimmingValue={1} pressDimmingValue={0.2} disabled={isValidateCodeFormSubmitting} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={props.isUsingRecoveryCode ? props.translate('recoveryCodeForm.use2fa') : props.translate('recoveryCodeForm.useRecoveryCode')} > {props.isUsingRecoveryCode ? props.translate('recoveryCodeForm.use2fa') : props.translate('recoveryCodeForm.useRecoveryCode')} @@ -376,7 +376,7 @@ function BaseValidateCodeForm(props) { disabled={shouldDisableResendValidateCode} hoverDimmingValue={1} pressDimmingValue={0.2} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={props.translate('validateCodeForm.magicCodeNotReceived')} > diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.js index 0d6f03006263..c25beba384ad 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.js +++ b/src/pages/tasks/NewTaskDescriptionPage.js @@ -78,7 +78,7 @@ function NewTaskDescriptionPage(props) { inputID="taskDescription" label={props.translate('newTaskPage.descriptionOptional')} accessibilityLabel={props.translate('newTaskPage.descriptionOptional')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} ref={(el) => { inputCallbackRef(el); updateMultilineInputRange(el); @@ -86,7 +86,7 @@ function NewTaskDescriptionPage(props) { autoGrowHeight submitOnEnter={!Browser.isMobile()} containerStyles={[styles.autoGrowHeightMultilineInput]} - textAlignVertical="top" + inputStyle={[styles.verticalAlignTop]} /> diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js index 0ab3771c28f2..87e0e7e430a9 100644 --- a/src/pages/tasks/NewTaskDetailsPage.js +++ b/src/pages/tasks/NewTaskDetailsPage.js @@ -99,7 +99,7 @@ function NewTaskDetailsPage(props) { setTaskDescription(value)} /> diff --git a/src/pages/tasks/NewTaskTitlePage.js b/src/pages/tasks/NewTaskTitlePage.js index e7be9a239e5d..c522ec35bcef 100644 --- a/src/pages/tasks/NewTaskTitlePage.js +++ b/src/pages/tasks/NewTaskTitlePage.js @@ -91,7 +91,7 @@ function NewTaskTitlePage(props) { { const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getFilteredOptions( @@ -208,7 +209,6 @@ function TaskAssigneeSelectorModal(props) { return ( optionRef.current && optionRef.current.textInput.focus()} testID={TaskAssigneeSelectorModal.displayName} > {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( @@ -229,7 +229,7 @@ function TaskAssigneeSelectorModal(props) { textInputLabel={props.translate('optionsSelector.nameEmailOrPhoneNumber')} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} autoFocus={false} - ref={optionRef} + ref={inputCallbackRef} /> diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js index 5d496fbca6c1..992e7c9b582b 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.js @@ -93,7 +93,7 @@ function TaskDescriptionPage(props) { diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.js index cd0d8166770e..e3f992ea9b5a 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.js +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.js @@ -1,6 +1,6 @@ /* eslint-disable es/no-optional-chaining */ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -8,6 +8,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useNetwork from '@hooks/useNetwork'; import * as Report from '@libs/actions/Report'; import compose from '@libs/compose'; @@ -51,8 +52,9 @@ function TaskShareDestinationSelectorModal(props) { const [searchValue, setSearchValue] = useState(''); const [headerMessage, setHeaderMessage] = useState(''); const [filteredRecentReports, setFilteredRecentReports] = useState([]); + + const {inputCallbackRef} = useAutoFocusInput(); const {isSearchingForReports} = props; - const optionRef = useRef(); const {isOffline} = useNetwork(); const filteredReports = useMemo(() => { @@ -127,7 +129,6 @@ function TaskShareDestinationSelectorModal(props) { return ( optionRef.current && optionRef.current.textInput.focus()} testID={TaskShareDestinationSelectorModal.displayName} > {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( @@ -151,7 +152,7 @@ function TaskShareDestinationSelectorModal(props) { textInputAlert={isOffline ? `${props.translate('common.youAppearToBeOffline')} ${props.translate('search.resultsAreLimited')}` : ''} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} autoFocus={false} - ref={optionRef} + ref={inputCallbackRef} isLoadingNewOptions={isSearchingForReports} /> diff --git a/src/pages/tasks/TaskTitlePage.js b/src/pages/tasks/TaskTitlePage.js index 375a23cc3012..b4dd1c7c9507 100644 --- a/src/pages/tasks/TaskTitlePage.js +++ b/src/pages/tasks/TaskTitlePage.js @@ -89,7 +89,7 @@ function TaskTitlePage(props) { openEditor(policy.id)))} accessibilityLabel={props.translate('workspace.common.settings')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > openEditor(policy.id)))} accessibilityLabel={props.translate('workspace.common.settings')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > (this.welcomeMessageInputRef = el)} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} inputID="welcomeMessage" label={this.props.translate('workspace.inviteMessage.personalMessagePrompt')} accessibilityLabel={this.props.translate('workspace.inviteMessage.personalMessagePrompt')} autoCompleteType="off" autoCorrect={false} autoGrowHeight - textAlignVertical="top" + inputStyle={[styles.verticalAlignTop]} containerStyles={[styles.autoGrowHeightMultilineInput]} defaultValue={this.state.welcomeNote} value={this.state.welcomeNote} diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index afb0c55e7d4e..ee6e2d826198 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -85,49 +85,36 @@ function WorkspaceInvitePage(props) { const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(props.policyMembers, props.personalDetails), [props.policyMembers, props.personalDetails]); useEffect(() => { - let emails = _.compact( - searchTerm - .trim() - .replace(/\s*,\s*/g, ',') - .split(','), - ); - - if (emails.length === 0) { - emails = ['']; - } - const newUsersToInviteDict = {}; const newPersonalDetailsDict = {}; const newSelectedOptionsDict = {}; - _.each(emails, (email) => { - const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, email, excludedUsers); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers); - // Update selectedOptions with the latest personalDetails and policyMembers information - const detailsMap = {}; - _.each(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail))); + // Update selectedOptions with the latest personalDetails and policyMembers information + const detailsMap = {}; + _.each(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail))); - const newSelectedOptions = []; - _.each(selectedOptions, (option) => { - newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option); - }); + const newSelectedOptions = []; + _.each(selectedOptions, (option) => { + newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option); + }); - const userToInvite = inviteOptions.userToInvite; + const userToInvite = inviteOptions.userToInvite; - // Only add the user to the invites list if it is valid - if (userToInvite) { - newUsersToInviteDict[userToInvite.accountID] = userToInvite; - } + // Only add the user to the invites list if it is valid + if (userToInvite) { + newUsersToInviteDict[userToInvite.accountID] = userToInvite; + } - // Add all personal details to the new dict - _.each(inviteOptions.personalDetails, (details) => { - newPersonalDetailsDict[details.accountID] = details; - }); + // Add all personal details to the new dict + _.each(inviteOptions.personalDetails, (details) => { + newPersonalDetailsDict[details.accountID] = details; + }); - // Add all selected options to the new dict - _.each(newSelectedOptions, (option) => { - newSelectedOptionsDict[option.accountID] = option; - }); + // Add all selected options to the new dict + _.each(newSelectedOptions, (option) => { + newSelectedOptionsDict[option.accountID] = option; }); // Strip out dictionary keys and update arrays diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 271dc45026c7..55c42d26716d 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -13,7 +13,7 @@ import Text from '@components/Text'; import TextInput from '@components/TextInput'; import ValuePicker from '@components/ValuePicker'; import withNavigationFocus from '@components/withNavigationFocus'; -import useDelayedInputFocus from '@hooks/useDelayedInputFocus'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -161,10 +161,7 @@ function WorkspaceNewRoomPage(props) { [translate], ); - const roomNameInputRef = useRef(null); - - // use a 600ms delay for delayed focus on the room name input field so that it works consistently on native iOS / Android - useDelayedInputFocus(roomNameInputRef, 600); + const {inputCallbackRef} = useAutoFocusInput(); return ( (roomNameInputRef.current = el)} + ref={inputCallbackRef} inputID="roomName" isFocused={props.isFocused} shouldDelayFocus @@ -211,7 +208,7 @@ function WorkspaceNewRoomPage(props) { inputID="welcomeMessage" label={translate('welcomeMessagePage.welcomeMessageOptional')} accessibilityLabel={translate('welcomeMessagePage.welcomeMessageOptional')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} autoGrowHeight maxLength={CONST.MAX_COMMENT_LENGTH} autoCapitalize="none" diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index 2ec17294b17a..b78e593e8c8a 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -142,7 +142,7 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { this.setState({rate: value})} diff --git a/src/setup/platformSetup/index.desktop.js b/src/setup/platformSetup/index.desktop.js index ab485b1855f1..fab7dc3f5b93 100644 --- a/src/setup/platformSetup/index.desktop.js +++ b/src/setup/platformSetup/index.desktop.js @@ -9,6 +9,7 @@ import ELECTRON_EVENTS from '../../../desktop/ELECTRON_EVENTS'; export default function () { AppRegistry.runApplication(Config.APP_NAME, { rootTag: document.getElementById('root'), + mode: 'legacy', }); // Send local notification when update is downloaded diff --git a/src/setup/platformSetup/index.website.js b/src/setup/platformSetup/index.website.js index d26268cd94bf..bdc64e769e09 100644 --- a/src/setup/platformSetup/index.website.js +++ b/src/setup/platformSetup/index.website.js @@ -56,6 +56,7 @@ const webUpdater = () => ({ export default function () { AppRegistry.runApplication(Config.APP_NAME, { rootTag: document.getElementById('root'), + mode: 'legacy', }); // When app loads, get current version (production only) diff --git a/src/stories/Composer.stories.js b/src/stories/Composer.stories.js index fc817ef2c86d..2db1011d1b3a 100644 --- a/src/stories/Composer.stories.js +++ b/src/stories/Composer.stories.js @@ -36,16 +36,15 @@ function Default(args) { // eslint-disable-next-line react/jsx-props-no-spreading {...args} multiline - textAlignVertical="top" onChangeText={setComment} onPasteFile={setPastedFile} - style={[styles.textInputCompose, styles.w100]} + style={[styles.textInputCompose, styles.w100, styles.verticalAlignTop]} /> Entered Comment (Drop Enabled) {comment} diff --git a/src/stories/Form.stories.js b/src/stories/Form.stories.js index 8bcbaf31b600..d385cf0613e6 100644 --- a/src/stories/Form.stories.js +++ b/src/stories/Form.stories.js @@ -46,7 +46,7 @@ function Template(args) { {}, onChangeText: () => {}, diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index eda4c3309cbf..42b7860ee263 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; @@ -522,9 +522,9 @@ function getBadgeColorStyle(isSuccess: boolean, isError: boolean, isPressed = fa function getButtonBackgroundColorStyle(buttonState: ButtonStateName = CONST.BUTTON_STATES.DEFAULT, isMenuItem = false): ViewStyle { switch (buttonState) { case CONST.BUTTON_STATES.PRESSED: - return isMenuItem ? {backgroundColor: themeColors.border} : {backgroundColor: themeColors.buttonPressedBG}; + return {backgroundColor: themeColors.buttonPressedBG}; case CONST.BUTTON_STATES.ACTIVE: - return isMenuItem ? {backgroundColor: themeColors.highlightBG} : {backgroundColor: themeColors.buttonHoveredBG}; + return isMenuItem ? {backgroundColor: themeColors.border} : {backgroundColor: themeColors.buttonHoveredBG}; case CONST.BUTTON_STATES.DISABLED: case CONST.BUTTON_STATES.DEFAULT: default: @@ -652,7 +652,7 @@ function getMiniReportActionContextMenuWrapperStyle(isReportActionItemGrouped: b ...positioning.r4, ...styles.cursorDefault, position: 'absolute', - zIndex: 1, + zIndex: 8, }; } @@ -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; } /** @@ -1072,7 +1071,7 @@ function getEmojiReactionCounterTextStyle(hasUserReacted: boolean): TextStyle { */ function getDirectionStyle(direction: ValueOf): ViewStyle { if (direction === CONST.DIRECTION.LEFT) { - return {transform: [{rotate: '180deg'}]}; + return {transform: 'rotate(180deg)'}; } return {}; @@ -1098,7 +1097,7 @@ function getGoogleListViewStyle(shouldDisplayBorder: boolean): ViewStyle { } return { - transform: [{scale: 0}], + transform: 'scale(0)', }; } diff --git a/src/styles/getModalStyles.ts b/src/styles/getModalStyles.ts index 55f822693b3e..984bf018e42d 100644 --- a/src/styles/getModalStyles.ts +++ b/src/styles/getModalStyles.ts @@ -73,15 +73,7 @@ export default function getModalStyles( }, }; modalContainerStyle = { - // Shadow Styles - shadowColor: themeColors.shadow, - shadowOffset: { - width: 0, - height: 0, - }, - shadowOpacity: 0.1, - shadowRadius: 5, - + boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', borderRadius: 12, overflow: 'hidden', width: variables.sideBarWidth, @@ -105,15 +97,7 @@ export default function getModalStyles( }, }; modalContainerStyle = { - // Shadow Styles - shadowColor: themeColors.shadow, - shadowOffset: { - width: 0, - height: 0, - }, - shadowOpacity: 0.1, - shadowRadius: 5, - + boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', flex: 1, marginTop: isSmallScreenWidth ? 0 : 20, marginBottom: isSmallScreenWidth ? 0 : 20, @@ -140,15 +124,7 @@ export default function getModalStyles( }, }; modalContainerStyle = { - // Shadow Styles - shadowColor: themeColors.shadow, - shadowOffset: { - width: 0, - height: 0, - }, - shadowOpacity: 0.1, - shadowRadius: 5, - + boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', flex: 1, marginTop: isSmallScreenWidth ? 0 : 20, marginBottom: isSmallScreenWidth ? 0 : 20, @@ -173,15 +149,7 @@ export default function getModalStyles( }, }; modalContainerStyle = { - // Shadow Styles - shadowColor: themeColors.shadow, - shadowOffset: { - width: 0, - height: 0, - }, - shadowOpacity: 0.1, - shadowRadius: 5, - + boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', borderRadius: 12, borderWidth: 0, }; diff --git a/src/styles/pointerEventsBoxNone/index.native.ts b/src/styles/pointerEventsBoxNone/index.native.ts new file mode 100644 index 000000000000..05ad2c07db39 --- /dev/null +++ b/src/styles/pointerEventsBoxNone/index.native.ts @@ -0,0 +1,5 @@ +import PointerEventsBoxNone from './types'; + +const pointerEventsBoxNone: PointerEventsBoxNone = {}; + +export default pointerEventsBoxNone; diff --git a/src/styles/pointerEventsBoxNone/index.ts b/src/styles/pointerEventsBoxNone/index.ts new file mode 100644 index 000000000000..0e63e2deda09 --- /dev/null +++ b/src/styles/pointerEventsBoxNone/index.ts @@ -0,0 +1,7 @@ +import PointerEventsBoxNone from './types'; + +const pointerEventsNone: PointerEventsBoxNone = { + pointerEvents: 'box-none', +}; + +export default pointerEventsNone; diff --git a/src/styles/pointerEventsBoxNone/types.ts b/src/styles/pointerEventsBoxNone/types.ts new file mode 100644 index 000000000000..25e85812f4e0 --- /dev/null +++ b/src/styles/pointerEventsBoxNone/types.ts @@ -0,0 +1,5 @@ +import {ViewStyle} from 'react-native'; + +type PointerEventsBoxNone = Pick; + +export default PointerEventsBoxNone; diff --git a/src/styles/styles.ts b/src/styles/styles.ts index 73f9aa823f40..cdfb049e2dce 100644 --- a/src/styles/styles.ts +++ b/src/styles/styles.ts @@ -17,6 +17,7 @@ import getPopOverVerticalOffset from './getPopOverVerticalOffset'; import optionAlternateTextPlatformStyles from './optionAlternateTextPlatformStyles'; import overflowXHidden from './overflowXHidden'; import pointerEventsAuto from './pointerEventsAuto'; +import pointerEventsBoxNone from './pointerEventsBoxNone'; import pointerEventsNone from './pointerEventsNone'; import defaultTheme from './themes/default'; import {ThemeColors} from './themes/types'; @@ -330,6 +331,14 @@ const styles = (theme: ThemeColors) => textDecorationLine: 'underline', }, + verticalAlignMiddle: { + verticalAlign: 'middle', + }, + + verticalAlignTop: { + verticalAlign: 'top', + }, + label: { fontSize: variables.fontSizeLabel, lineHeight: variables.lineHeightLarge, @@ -1050,7 +1059,7 @@ const styles = (theme: ThemeColors) => paddingRight: 12, paddingTop: 10, paddingBottom: 10, - textAlignVertical: 'center', + verticalAlign: 'middle', }, textInputPrefixWrapper: { @@ -1069,7 +1078,7 @@ const styles = (theme: ThemeColors) => color: theme.text, fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeNormal, - textAlignVertical: 'center', + verticalAlign: 'middle', }, pickerContainer: { @@ -1651,7 +1660,7 @@ const styles = (theme: ThemeColors) => chatContentScrollView: { flexGrow: 1, - justifyContent: 'flex-start', + justifyContent: 'flex-end', paddingBottom: 16, }, @@ -1795,13 +1804,13 @@ const styles = (theme: ThemeColors) => ...overflowXHidden, // On Android, multiline TextInput with height: 'auto' will show extra padding unless they are configured with - // paddingVertical: 0, alignSelf: 'center', and textAlignVertical: 'center' + // paddingVertical: 0, alignSelf: 'center', and verticalAlign: 'middle' paddingHorizontal: variables.avatarChatSpacing, paddingTop: 0, paddingBottom: 0, alignSelf: 'center', - textAlignVertical: 'center', + verticalAlign: 'middle', }, 0, ), @@ -1810,7 +1819,7 @@ const styles = (theme: ThemeColors) => alignSelf: 'stretch', flex: 1, maxHeight: '100%', - textAlignVertical: 'top', + verticalAlign: 'top', }, // composer padding should not be modified unless thoroughly tested against the cases in this PR: #12669 @@ -2141,6 +2150,8 @@ const styles = (theme: ThemeColors) => pointerEventsAuto, + pointerEventsBoxNone, + headerBar: { overflow: 'hidden', justifyContent: 'center', @@ -2477,7 +2488,7 @@ const styles = (theme: ThemeColors) => }, flipUpsideDown: { - transform: [{rotate: '180deg'}], + transform: `rotate(180deg)`, }, navigationScreenCardStyle: { @@ -2778,7 +2789,7 @@ const styles = (theme: ThemeColors) => alignItems: 'center', flexDirection: 'row', justifyContent: 'space-between', - shadowColor: theme.shadow, + boxShadow: `${theme.shadow}`, ...spacing.p5, }, @@ -2877,7 +2888,7 @@ const styles = (theme: ThemeColors) => }, text: { color: theme.textSupporting, - textAlignVertical: 'center', + verticalAlign: 'middle', fontSize: variables.fontSizeLabel, }, errorDot: { @@ -3218,11 +3229,11 @@ const styles = (theme: ThemeColors) => miniQuickEmojiReactionText: { fontSize: 15, lineHeight: 20, - textAlignVertical: 'center', + verticalAlign: 'middle', }, emojiReactionBubbleText: { - textAlignVertical: 'center', + verticalAlign: 'middle', }, reactionCounterText: { @@ -3420,7 +3431,6 @@ const styles = (theme: ThemeColors) => linkPreviewImage: { flex: 1, - resizeMode: 'contain', borderRadius: 8, marginTop: 8, }, @@ -3788,7 +3798,7 @@ const styles = (theme: ThemeColors) => }, rotate90: { - transform: [{rotate: '90deg'}], + transform: 'rotate(90deg)', }, emojiStatusLHN: { 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/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 48d3e8c558af..76098d72f52e 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -78,6 +78,8 @@ describe('actions/IOU', () => { const iouReport = iouReports[0]; iouReportID = iouReport.reportID; + expect(iouReport.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + // They should be linked together expect(chatReport.participantAccountIDs).toEqual([CARLOS_ACCOUNT_ID]); expect(chatReport.iouReportID).toBe(iouReport.reportID); @@ -243,6 +245,8 @@ describe('actions/IOU', () => { const iouReport = _.find(allReports, (report) => report.type === CONST.REPORT.TYPE.IOU); iouReportID = iouReport.reportID; + expect(iouReport.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + // They should be linked together expect(chatReport.iouReportID).toBe(iouReportID); expect(chatReport.hasOutstandingIOU).toBe(true); @@ -572,6 +576,8 @@ describe('actions/IOU', () => { const iouReport = iouReports[0]; iouReportID = iouReport.reportID; + expect(iouReport.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + // They should be linked together expect(chatReport.participantAccountIDs).toEqual([CARLOS_ACCOUNT_ID]); expect(chatReport.iouReportID).toBe(iouReport.reportID); @@ -982,6 +988,7 @@ describe('actions/IOU', () => { expect(carlosChatReport.hasOutstandingIOU).toBe(true); expect(carlosChatReport.iouReportID).toBe(carlosIOUReport.reportID); expect(carlosIOUReport.chatReportID).toBe(carlosChatReport.reportID); + expect(carlosIOUReport.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); expect(julesChatReport.hasOutstandingIOU).toBe(true); expect(julesChatReport.iouReportID).toBe(julesIOUReport.reportID); @@ -990,6 +997,7 @@ describe('actions/IOU', () => { expect(vitChatReport.hasOutstandingIOU).toBe(true); expect(vitChatReport.iouReportID).toBe(vitIOUReport.reportID); expect(vitIOUReport.chatReportID).toBe(vitChatReport.reportID); + expect(carlosIOUReport.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); resolve(); }, @@ -2171,6 +2179,8 @@ describe('actions/IOU', () => { // Given a transaction thread thread = ReportUtils.buildTransactionThread(createIOUAction, IOU_REPORT_ID); + expect(thread.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, callback: (val) => (reportActions = val), @@ -2248,6 +2258,9 @@ describe('actions/IOU', () => { // Given a transaction thread thread = ReportUtils.buildTransactionThread(createIOUAction); + + expect(thread.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); jest.advanceTimersByTime(10); Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); @@ -2332,6 +2345,8 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); thread = ReportUtils.buildTransactionThread(createIOUAction, IOU_REPORT_ID); + expect(thread.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, callback: (val) => (reportActions = val), @@ -2545,6 +2560,8 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); thread = ReportUtils.buildTransactionThread(createIOUAction, IOU_REPORT_ID); + expect(thread.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, callback: (val) => (reportActions = val),